Merge branch 'main' into docs_consolidation
This commit is contained in:
commit
b94d401d09
|
@ -782,10 +782,9 @@ class RegionType(int, Enum):
|
||||||
|
|
||||||
|
|
||||||
class Region(object):
|
class Region(object):
|
||||||
|
def __init__(self, name: str, type_: RegionType, hint, player: int, world: Optional[MultiWorld] = None):
|
||||||
def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.type = type
|
self.type = type_
|
||||||
self.entrances = []
|
self.entrances = []
|
||||||
self.exits = []
|
self.exits = []
|
||||||
self.locations = []
|
self.locations = []
|
||||||
|
|
|
@ -23,6 +23,7 @@ import Options
|
||||||
from worlds.alttp import Bosses
|
from worlds.alttp import Bosses
|
||||||
from worlds.alttp.Text import TextTable
|
from worlds.alttp.Text import TextTable
|
||||||
from worlds.AutoWorld import AutoWorldRegister
|
from worlds.AutoWorld import AutoWorldRegister
|
||||||
|
import copy
|
||||||
|
|
||||||
categories = set(AutoWorldRegister.world_types)
|
categories = set(AutoWorldRegister.world_types)
|
||||||
|
|
||||||
|
@ -148,7 +149,7 @@ def main(args=None, callback=ERmain):
|
||||||
if category_name is None:
|
if category_name is None:
|
||||||
weights_cache[path][key] = option
|
weights_cache[path][key] = option
|
||||||
elif category_name not in weights_cache[path]:
|
elif category_name not in weights_cache[path]:
|
||||||
raise Exception(f"Meta: Category {category_name} is not present in {path}.")
|
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
|
||||||
else:
|
else:
|
||||||
weights_cache[path][category_name][key] = option
|
weights_cache[path][category_name][key] = option
|
||||||
|
|
||||||
|
@ -330,7 +331,7 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
|
||||||
|
|
||||||
|
|
||||||
def roll_linked_options(weights: dict) -> dict:
|
def roll_linked_options(weights: dict) -> dict:
|
||||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||||
for option_set in weights["linked_options"]:
|
for option_set in weights["linked_options"]:
|
||||||
if "name" not in option_set:
|
if "name" not in option_set:
|
||||||
raise ValueError("One of your linked options does not have a name.")
|
raise ValueError("One of your linked options does not have a name.")
|
||||||
|
@ -352,7 +353,7 @@ def roll_linked_options(weights: dict) -> dict:
|
||||||
|
|
||||||
|
|
||||||
def roll_triggers(weights: dict, triggers: list) -> dict:
|
def roll_triggers(weights: dict, triggers: list) -> dict:
|
||||||
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
|
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
|
||||||
weights["_Generator_Version"] = Utils.__version__
|
weights["_Generator_Version"] = Utils.__version__
|
||||||
for i, option_set in enumerate(triggers):
|
for i, option_set in enumerate(triggers):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -125,6 +125,8 @@ class Toggle(Option):
|
||||||
def get_option_name(cls, value):
|
def get_option_name(cls, value):
|
||||||
return ["No", "Yes"][int(value)]
|
return ["No", "Yes"][int(value)]
|
||||||
|
|
||||||
|
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||||
|
|
||||||
|
|
||||||
class DefaultOnToggle(Toggle):
|
class DefaultOnToggle(Toggle):
|
||||||
default = 1
|
default = 1
|
||||||
|
@ -184,6 +186,8 @@ class Choice(Option):
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
|
||||||
|
|
||||||
|
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
|
||||||
|
|
||||||
|
|
||||||
class Range(Option, int):
|
class Range(Option, int):
|
||||||
range_start = 0
|
range_start = 0
|
||||||
|
|
2
Utils.py
2
Utils.py
|
@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
|
||||||
build: int
|
build: int
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.2.2"
|
__version__ = "0.2.3"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
from yaml import load, dump, safe_load
|
from yaml import load, dump, safe_load
|
||||||
|
|
|
@ -193,6 +193,15 @@ def discord():
|
||||||
return redirect("https://discord.gg/archipelago")
|
return redirect("https://discord.gg/archipelago")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/datapackage')
|
||||||
|
@cache.cached()
|
||||||
|
def get_datapackge():
|
||||||
|
"""A pretty print version of /api/datapackage"""
|
||||||
|
from worlds import network_data_package
|
||||||
|
import json
|
||||||
|
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
|
||||||
|
|
||||||
|
|
||||||
from WebHostLib.customserver import run_server_process
|
from WebHostLib.customserver import run_server_process
|
||||||
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@ def get_datapackge():
|
||||||
from worlds import network_data_package
|
from worlds import network_data_package
|
||||||
return network_data_package
|
return network_data_package
|
||||||
|
|
||||||
|
|
||||||
@api_endpoints.route('/datapackage_version')
|
@api_endpoints.route('/datapackage_version')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def get_datapackge_versions():
|
def get_datapackge_versions():
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Slay the Spire (PC)
|
||||||
|
|
||||||
|
## Where is the settings page?
|
||||||
|
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
|
||||||
|
you need to configure and export a config file.
|
||||||
|
|
||||||
|
## What does randomization do to this game?
|
||||||
|
Every non-boss relic drop, every boss relic and rare card drop, and every other card draw is replaced with an
|
||||||
|
archipelago item. In heart runs, the blue key is also disconnected from the Archipelago item, so you can gather both.
|
||||||
|
|
||||||
|
## What items and locations get shuffled?
|
||||||
|
15 card packs, 10 relics, and 3 boss relics and rare card drops are shuffled into the item pool and can be found at any
|
||||||
|
location that would normally give you these items, except for card packs, which are found at every other normal enemy
|
||||||
|
encounter.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## When the player receives an item, what happens?
|
||||||
|
When the player receives an item, you will see the counter in the top right corner with the Archipelago symbol increment
|
||||||
|
by one. By clicking on this icon, it'll open a menu that lists all the items you received, but have not yet accepted.
|
||||||
|
You can take any relics and card packs sent to you and add them to your current run. It is advised that you do not open
|
||||||
|
this menu until you are outside an encounter or event to prevent the game from soft-locking.
|
||||||
|
|
||||||
|
## What happens if a player dies in a run?
|
||||||
|
When a player dies, they will be taken back to the main menu and will need to reconnect to start climbing the spire
|
||||||
|
from the beginning, but they will have access to all the items ever sent to them in the Archipelago menu in the top
|
||||||
|
right. Any items found in an earlier run will not be sent again if you encounter them in the same location.
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Slay the Spire Setup Guide
|
||||||
|
|
||||||
|
## Required Software
|
||||||
|
|
||||||
|
For steam-based installation, subscribe to the following mods:
|
||||||
|
|
||||||
|
- ModTheSpire from the [Slay the Spire Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=1605060445)
|
||||||
|
- BaseMod from the [Slay the Spire Workshop](https://steamcommunity.com/workshop/filedetails/?id=1605833019)
|
||||||
|
- Archipelago Multiworld Randomizer Mod from the [Slay the Spire Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2596397288)
|
||||||
|
|
||||||
|
## 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 settings by visiting the [Slay the Spire Settings Page](/games/Slay%20the%20Spire/player-settings).
|
||||||
|
|
||||||
|
### Connect to the MultiServer
|
||||||
|
For Steam-based installations, if you are subscribed to ModTheSpire, when you launch the game, you should have the
|
||||||
|
option to launch the game with mods. On the mod loader screen, ensure you only have the following mods enabled and then
|
||||||
|
start the game:
|
||||||
|
|
||||||
|
- BaseMod
|
||||||
|
- Archipelago Multiworld Randomizer
|
||||||
|
|
||||||
|
Once you are in-game, you will be able to click the **Archipelago** menu option and enter the ip and port (separated by
|
||||||
|
a colon) in the hostname field and enter your player slot name in the Slot Name field. Then click connect, and now you
|
||||||
|
are ready to climb the spire!
|
|
@ -390,5 +390,24 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gameTitle": "Slay the Spire",
|
||||||
|
"tutorials": [
|
||||||
|
{
|
||||||
|
"name": "Multiworld Setup Guide",
|
||||||
|
"description": "A guide to setting up Slay the Spire for Archipelago. This guide covers single-player, multiworld, and related software.",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"language": "English",
|
||||||
|
"filename": "slay-the-spire/slay-the-spire_en.md",
|
||||||
|
"link": "slay-the-spire/slay-the-spire/en",
|
||||||
|
"authors": [
|
||||||
|
"Phar"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -128,19 +128,24 @@ const buildUI = (settingData) => {
|
||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
gameDiv.appendChild(expandButton);
|
gameDiv.appendChild(expandButton);
|
||||||
|
|
||||||
const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings);
|
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
|
||||||
gameDiv.appendChild(optionsDiv);
|
gameDiv.appendChild(weightedSettingsDiv);
|
||||||
|
|
||||||
|
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
|
||||||
|
gameDiv.appendChild(itemsDiv);
|
||||||
gamesWrapper.appendChild(gameDiv);
|
gamesWrapper.appendChild(gameDiv);
|
||||||
|
|
||||||
collapseButton.addEventListener('click', () => {
|
collapseButton.addEventListener('click', () => {
|
||||||
collapseButton.classList.add('invisible');
|
collapseButton.classList.add('invisible');
|
||||||
optionsDiv.classList.add('invisible');
|
weightedSettingsDiv.classList.add('invisible');
|
||||||
|
itemsDiv.classList.add('invisible');
|
||||||
expandButton.classList.remove('invisible');
|
expandButton.classList.remove('invisible');
|
||||||
});
|
});
|
||||||
|
|
||||||
expandButton.addEventListener('click', () => {
|
expandButton.addEventListener('click', () => {
|
||||||
collapseButton.classList.remove('invisible');
|
collapseButton.classList.remove('invisible');
|
||||||
optionsDiv.classList.remove('invisible');
|
weightedSettingsDiv.classList.remove('invisible');
|
||||||
|
itemsDiv.classList.remove('invisible');
|
||||||
expandButton.classList.add('invisible');
|
expandButton.classList.add('invisible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -207,10 +212,10 @@ const buildGameChoice = (games) => {
|
||||||
gameChoiceDiv.appendChild(table);
|
gameChoiceDiv.appendChild(table);
|
||||||
};
|
};
|
||||||
|
|
||||||
const buildOptionsDiv = (game, settings) => {
|
const buildWeightedSettingsDiv = (game, settings) => {
|
||||||
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
|
||||||
const optionsWrapper = document.createElement('div');
|
const settingsWrapper = document.createElement('div');
|
||||||
optionsWrapper.classList.add('settings-wrapper');
|
settingsWrapper.classList.add('settings-wrapper');
|
||||||
|
|
||||||
Object.keys(settings).forEach((settingName) => {
|
Object.keys(settings).forEach((settingName) => {
|
||||||
const setting = settings[settingName];
|
const setting = settings[settingName];
|
||||||
|
@ -268,27 +273,6 @@ const buildOptionsDiv = (game, settings) => {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'range':
|
case 'range':
|
||||||
const hintText = document.createElement('p');
|
|
||||||
hintText.classList.add('hint-text');
|
|
||||||
hintText.innerHTML = 'This is a range option. You may enter valid numerical values in the text box below, ' +
|
|
||||||
`then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
|
|
||||||
`Maximum value: ${setting.max}`;
|
|
||||||
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')); }
|
|
||||||
});
|
|
||||||
|
|
||||||
const rangeTable = document.createElement('table');
|
const rangeTable = document.createElement('table');
|
||||||
const rangeTbody = document.createElement('tbody');
|
const rangeTbody = document.createElement('tbody');
|
||||||
|
|
||||||
|
@ -324,6 +308,79 @@ const buildOptionsDiv = (game, settings) => {
|
||||||
rangeTbody.appendChild(tr);
|
rangeTbody.appendChild(tr);
|
||||||
}
|
}
|
||||||
} else {
|
} 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.<br />Minimum value: ${setting.min}<br />` +
|
||||||
|
`Maximum value: ${setting.max}`;
|
||||||
|
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', updateGameSetting);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
Object.keys(currentSettings[game][settingName]).forEach((option) => {
|
Object.keys(currentSettings[game][settingName]).forEach((option) => {
|
||||||
if (currentSettings[game][settingName][option] > 0) {
|
if (currentSettings[game][settingName][option] > 0) {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
|
@ -403,58 +460,6 @@ const buildOptionsDiv = (game, settings) => {
|
||||||
|
|
||||||
rangeTable.appendChild(rangeTbody);
|
rangeTable.appendChild(rangeTbody);
|
||||||
settingWrapper.appendChild(rangeTable);
|
settingWrapper.appendChild(rangeTable);
|
||||||
|
|
||||||
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', updateGameSetting);
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
@ -462,10 +467,158 @@ const buildOptionsDiv = (game, settings) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
optionsWrapper.appendChild(settingWrapper);
|
settingsWrapper.appendChild(settingWrapper);
|
||||||
});
|
});
|
||||||
|
|
||||||
return optionsWrapper;
|
return settingsWrapper;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildItemsDiv = (game, items) => {
|
||||||
|
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.sort().forEach((item) => {
|
||||||
|
const itemDiv = buildItemDiv(game, item);
|
||||||
|
|
||||||
|
if (currentSettings[game].start_inventory.includes(item)){
|
||||||
|
itemDiv.setAttribute('data-setting', 'start_inventory');
|
||||||
|
startInventory.appendChild(itemDiv);
|
||||||
|
} else if (currentSettings[game].local_items.includes(item)) {
|
||||||
|
itemDiv.setAttribute('data-setting', 'local_items');
|
||||||
|
localItems.appendChild(itemDiv);
|
||||||
|
} else if (currentSettings[game].non_local_items.includes(item)) {
|
||||||
|
itemDiv.setAttribute('data-setting', 'non_local_items');
|
||||||
|
nonLocalItems.appendChild(itemDiv);
|
||||||
|
} else {
|
||||||
|
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 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 itemDiv = buildItemDiv(game, item);
|
||||||
|
|
||||||
|
const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null;
|
||||||
|
const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null;
|
||||||
|
|
||||||
|
if (oldSetting) {
|
||||||
|
if (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 (!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 updateVisibleGames = () => {
|
const updateVisibleGames = () => {
|
||||||
|
|
|
@ -90,6 +90,38 @@ html{
|
||||||
cursor: pointer;
|
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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .items-wrapper .item-container{
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 2px;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .items-wrapper .item-container .item-div{
|
||||||
|
padding: 0.15rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#weighted-settings .items-wrapper .item-container .item-div:hover{
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
#weighted-settings #weighted-settings-button-row{
|
#weighted-settings #weighted-settings-button-row{
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
{% include 'header/grassHeader.html' %}
|
{% include 'header/grassHeader.html' %}
|
||||||
<div id="games">
|
<div id="games">
|
||||||
<h1>Currently Supported Games</h1>
|
<h1>Currently Supported Games</h1>
|
||||||
{% for game, description in worlds.items() %}
|
{% for game, description in worlds.items() | sort %}
|
||||||
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
|
||||||
<p>
|
<p>
|
||||||
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
|
||||||
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
|
||||||
<td>>={{ room.seed.slots|length }}</td>
|
<td>{{ room.seed.slots|length }}</td>
|
||||||
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
{% for seed in seeds %}
|
{% for seed in seeds %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
|
||||||
<td>{% if seed.multidata %}>={{ seed.slots|length }}{% else %}1{% endif %}
|
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -100,7 +100,7 @@ def uploads():
|
||||||
if file.filename == '':
|
if file.filename == '':
|
||||||
flash('No selected file')
|
flash('No selected file')
|
||||||
elif file and allowed_file(file.filename):
|
elif file and allowed_file(file.filename):
|
||||||
if zipfile.is_zipfile(file.filename):
|
if zipfile.is_zipfile(file):
|
||||||
with zipfile.ZipFile(file, 'r') as zfile:
|
with zipfile.ZipFile(file, 'r') as zfile:
|
||||||
res = upload_zip_to_db(zfile)
|
res = upload_zip_to_db(zfile)
|
||||||
if type(res) == str:
|
if type(res) == str:
|
||||||
|
@ -108,12 +108,13 @@ def uploads():
|
||||||
elif res:
|
elif res:
|
||||||
return redirect(url_for("view_seed", seed=res.id))
|
return redirect(url_for("view_seed", seed=res.id))
|
||||||
else:
|
else:
|
||||||
|
file.seek(0) # offset from is_zipfile check
|
||||||
# noinspection PyBroadException
|
# noinspection PyBroadException
|
||||||
try:
|
try:
|
||||||
multidata = file.read()
|
multidata = file.read()
|
||||||
MultiServer.Context.decompress(multidata)
|
MultiServer.Context.decompress(multidata)
|
||||||
except:
|
except Exception as e:
|
||||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
|
||||||
else:
|
else:
|
||||||
seed = Seed(multidata=multidata, owner=session["_id"])
|
seed = Seed(multidata=multidata, owner=session["_id"])
|
||||||
flush() # place into DB and generate ids
|
flush() # place into DB and generate ids
|
||||||
|
|
|
@ -64,7 +64,7 @@ generator:
|
||||||
# general weights file, within the stated player_files_path location
|
# general weights file, within the stated player_files_path location
|
||||||
# gets used if players is higher than the amount of per-player files found to fill remaining slots
|
# gets used if players is higher than the amount of per-player files found to fill remaining slots
|
||||||
weights_file_path: "weights.yaml"
|
weights_file_path: "weights.yaml"
|
||||||
# Meta file name, within the stated player_files_path location, TODO: re-implement this
|
# Meta file name, within the stated player_files_path location
|
||||||
meta_file_path: "meta.yaml"
|
meta_file_path: "meta.yaml"
|
||||||
# Create a spoiler file
|
# Create a spoiler file
|
||||||
# 0 -> None
|
# 0 -> None
|
||||||
|
|
|
@ -9,6 +9,7 @@ Utils.local_path.cached_path = file_path
|
||||||
from BaseClasses import MultiWorld, CollectionState
|
from BaseClasses import MultiWorld, CollectionState
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
|
|
||||||
|
|
||||||
class TestBase(unittest.TestCase):
|
class TestBase(unittest.TestCase):
|
||||||
world: MultiWorld
|
world: MultiWorld
|
||||||
_state_cache = {}
|
_state_cache = {}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple, List
|
||||||
import unittest
|
import unittest
|
||||||
from worlds.AutoWorld import World
|
from worlds.AutoWorld import World
|
||||||
from Fill import FillError, fill_restrictive
|
from Fill import FillError, fill_restrictive
|
||||||
|
@ -28,8 +28,8 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
|
||||||
class PlayerDefinition(NamedTuple):
|
class PlayerDefinition(NamedTuple):
|
||||||
id: int
|
id: int
|
||||||
menu: Region
|
menu: Region
|
||||||
locations: list[Location]
|
locations: List[Location]
|
||||||
prog_items: list[Item]
|
prog_items: List[Item]
|
||||||
|
|
||||||
|
|
||||||
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition:
|
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition:
|
||||||
|
@ -40,7 +40,7 @@ def generate_player_data(multi_world: MultiWorld, player_id: int, location_count
|
||||||
return PlayerDefinition(player_id, menu, locations, prog_items)
|
return PlayerDefinition(player_id, menu, locations, prog_items)
|
||||||
|
|
||||||
|
|
||||||
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> list[Location]:
|
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> List[Location]:
|
||||||
locations = []
|
locations = []
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
name = "player" + str(player_id) + "_location" + str(i)
|
name = "player" + str(player_id) + "_location" + str(i)
|
||||||
|
@ -50,7 +50,7 @@ def generate_locations(count: int, player_id: int, address: int = None, region:
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
|
|
||||||
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> list[Location]:
|
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
|
||||||
items = []
|
items = []
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
name = "player" + str(player_id) + "_item" + str(i)
|
name = "player" + str(player_id) + "_item" + str(i)
|
|
@ -4,15 +4,15 @@ from BaseClasses import MultiWorld
|
||||||
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
|
||||||
from worlds.alttp.EntranceShuffle import link_entrances
|
from worlds.alttp.EntranceShuffle import link_entrances
|
||||||
from worlds.alttp.InvertedRegions import mark_dark_world_regions
|
from worlds.alttp.InvertedRegions import mark_dark_world_regions
|
||||||
from worlds.alttp.ItemPool import difficulties, generate_itempool
|
from worlds.alttp.ItemPool import difficulties
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
from worlds.alttp.Regions import create_regions
|
from worlds.alttp.Regions import create_regions
|
||||||
from worlds.alttp.Shops import create_shops
|
from worlds.alttp.Shops import create_shops
|
||||||
from worlds.alttp.Rules import set_rules
|
|
||||||
from test.TestBase import TestBase
|
from test.TestBase import TestBase
|
||||||
|
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
class TestMinor(TestBase):
|
class TestMinor(TestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world = MultiWorld(1)
|
self.world = MultiWorld(1)
|
||||||
|
@ -30,8 +30,10 @@ class TestMinor(TestBase):
|
||||||
self.world.worlds[1].create_items()
|
self.world.worlds[1].create_items()
|
||||||
self.world.required_medallions[1] = ['Ether', 'Quake']
|
self.world.required_medallions[1] = ['Ether', 'Quake']
|
||||||
self.world.itempool.extend(get_dungeon_item_pool(self.world))
|
self.world.itempool.extend(get_dungeon_item_pool(self.world))
|
||||||
self.world.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.world.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.world.get_location('Agahnim 1', 1).item = None
|
self.world.get_location('Agahnim 1', 1).item = None
|
||||||
self.world.get_location('Agahnim 2', 1).item = None
|
self.world.get_location('Agahnim 2', 1).item = None
|
||||||
mark_dark_world_regions(self.world, 1)
|
mark_dark_world_regions(self.world, 1)
|
||||||
self.world.worlds[1].set_rules()
|
self.world.worlds[1].set_rules()
|
||||||
|
|
|
@ -3,7 +3,7 @@ from worlds.alttp.Bosses import BossFactory
|
||||||
from Fill import fill_restrictive
|
from Fill import fill_restrictive
|
||||||
from worlds.alttp.Items import ItemFactory
|
from worlds.alttp.Items import ItemFactory
|
||||||
from worlds.alttp.Regions import lookup_boss_drops
|
from worlds.alttp.Regions import lookup_boss_drops
|
||||||
from worlds.alttp.Options import smallkey_shuffle
|
from worlds.alttp.Options import smallkey_shuffle
|
||||||
|
|
||||||
|
|
||||||
def create_dungeons(world, player):
|
def create_dungeons(world, player):
|
||||||
|
|
|
@ -95,7 +95,7 @@ class ShopPriceModifier(Range):
|
||||||
"""Percentage modifier for shuffled item prices in shops"""
|
"""Percentage modifier for shuffled item prices in shops"""
|
||||||
range_start = 0
|
range_start = 0
|
||||||
default = 100
|
default = 100
|
||||||
range_end = 10000
|
range_end = 400
|
||||||
|
|
||||||
class WorldState(Choice):
|
class WorldState(Choice):
|
||||||
option_standard = 1
|
option_standard = 1
|
||||||
|
|
|
@ -264,7 +264,7 @@ def ShopSlotFill(world):
|
||||||
price = world.random.randrange(8, 56)
|
price = world.random.randrange(8, 56)
|
||||||
|
|
||||||
shop.push_inventory(location.shop_slot, item_name,
|
shop.push_inventory(location.shop_slot, item_name,
|
||||||
min(int(price * 5 * world.shop_price_modifier[location.player] / 100), 9999), 1,
|
min(int(price * world.shop_price_modifier[location.player] / 100) * 5, 9999), 1,
|
||||||
location.item.player if location.item.player != location.player else 0)
|
location.item.player if location.item.player != location.player else 0)
|
||||||
if 'P' in world.shop_shuffle[location.player]:
|
if 'P' in world.shop_shuffle[location.player]:
|
||||||
price_to_funny_price(shop.inventory[location.shop_slot], world, location.player)
|
price_to_funny_price(shop.inventory[location.shop_slot], world, location.player)
|
||||||
|
|
|
@ -336,7 +336,8 @@ class ALTTPWorld(World):
|
||||||
standard_keyshuffle_players = set()
|
standard_keyshuffle_players = set()
|
||||||
for player in world.get_game_players("A Link to the Past"):
|
for player in world.get_game_players("A Link to the Past"):
|
||||||
if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \
|
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_universal and \
|
||||||
|
world.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons:
|
||||||
standard_keyshuffle_players.add(player)
|
standard_keyshuffle_players.add(player)
|
||||||
if not world.ganonstower_vanilla[player] or \
|
if not world.ganonstower_vanilla[player] or \
|
||||||
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
|
||||||
|
@ -350,23 +351,31 @@ class ALTTPWorld(World):
|
||||||
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
|
||||||
# TODO: this might be worthwhile to introduce as generic option for various games and then optimize it
|
# TODO: this might be worthwhile to introduce as generic option for various games and then optimize it
|
||||||
if standard_keyshuffle_players:
|
if standard_keyshuffle_players:
|
||||||
viable = []
|
viable = {}
|
||||||
for location in world.get_locations():
|
for location in world.get_locations():
|
||||||
if location.player in standard_keyshuffle_players \
|
if location.player in standard_keyshuffle_players \
|
||||||
and location.item is None \
|
and location.item is None \
|
||||||
and location.can_reach(world.state):
|
and location.can_reach(world.state):
|
||||||
viable.append(location)
|
viable.setdefault(location.player, []).append(location)
|
||||||
world.random.shuffle(viable)
|
|
||||||
for player in standard_keyshuffle_players:
|
for player in standard_keyshuffle_players:
|
||||||
|
loc = world.random.choice(viable[player])
|
||||||
key = world.create_item("Small Key (Hyrule Castle)", player)
|
key = world.create_item("Small Key (Hyrule Castle)", player)
|
||||||
loc = viable.pop()
|
|
||||||
loc.place_locked_item(key)
|
loc.place_locked_item(key)
|
||||||
fill_locations.remove(loc)
|
fill_locations.remove(loc)
|
||||||
world.random.shuffle(fill_locations)
|
world.random.shuffle(fill_locations)
|
||||||
# TODO: investigate not creating the key in the first place
|
# TODO: investigate not creating the key in the first place
|
||||||
progitempool[:] = [item for item in progitempool if
|
if __debug__:
|
||||||
item.player not in standard_keyshuffle_players or
|
# keeping this here while I'm not sure we caught all instances of multiple HC small keys in the pool
|
||||||
item.name != "Small Key (Hyrule Castle)"]
|
count = len(progitempool)
|
||||||
|
progitempool[:] = [item for item in progitempool if
|
||||||
|
item.player not in standard_keyshuffle_players or
|
||||||
|
item.name != "Small Key (Hyrule Castle)"]
|
||||||
|
assert len(progitempool) + len(standard_keyshuffle_players) == count
|
||||||
|
else:
|
||||||
|
progitempool[:] = [item for item in progitempool if
|
||||||
|
item.player not in standard_keyshuffle_players or
|
||||||
|
item.name != "Small Key (Hyrule Castle)"]
|
||||||
|
|
||||||
if trash_counts:
|
if trash_counts:
|
||||||
locations_mapping = {player: [] for player in trash_counts}
|
locations_mapping = {player: [] for player in trash_counts}
|
||||||
|
|
|
@ -68,16 +68,12 @@ class HKWorld(World):
|
||||||
|
|
||||||
self.world.itempool += pool
|
self.world.itempool += pool
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
set_rules(self.world, self.player)
|
set_rules(self.world, self.player)
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
create_regions(self.world, self.player)
|
create_regions(self.world, self.player)
|
||||||
|
|
||||||
def generate_output(self):
|
|
||||||
pass # Hollow Knight needs no output files
|
|
||||||
|
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
slot_data = {}
|
slot_data = {}
|
||||||
for option_name in self.options:
|
for option_name in self.options:
|
||||||
|
|
|
@ -140,6 +140,11 @@ def set_rules(ootworld):
|
||||||
location = world.get_location('Sheik in Ice Cavern', player)
|
location = world.get_location('Sheik in Ice Cavern', player)
|
||||||
add_item_rule(location, lambda item: item.player == player and item.type == 'Song')
|
add_item_rule(location, lambda item: item.player == player and item.type == 'Song')
|
||||||
|
|
||||||
|
if ootworld.skip_child_zelda:
|
||||||
|
# If skip child zelda is on, the item at Song from Impa must be giveable by the save context.
|
||||||
|
location = world.get_location('Song from Impa', player)
|
||||||
|
add_item_rule(location, lambda item: item in SaveContext.giveable_items)
|
||||||
|
|
||||||
for name in ootworld.always_hints:
|
for name in ootworld.always_hints:
|
||||||
add_rule(world.get_location(name, player), guarantee_hint)
|
add_rule(world.get_location(name, player), guarantee_hint)
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ from .N64Patch import create_patch_file
|
||||||
from .Cosmetics import patch_cosmetics
|
from .Cosmetics import patch_cosmetics
|
||||||
from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
|
from .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
|
||||||
from .HintList import getRequiredHints
|
from .HintList import getRequiredHints
|
||||||
|
from .SaveContext import SaveContext
|
||||||
|
|
||||||
from Utils import get_options, output_path
|
from Utils import get_options, output_path
|
||||||
from BaseClasses import MultiWorld, CollectionState, RegionType
|
from BaseClasses import MultiWorld, CollectionState, RegionType
|
||||||
|
@ -471,13 +472,16 @@ class OOTWorld(World):
|
||||||
self.remove_from_start_inventory.remove(item.name)
|
self.remove_from_start_inventory.remove(item.name)
|
||||||
removed_items.append(item.name)
|
removed_items.append(item.name)
|
||||||
else:
|
else:
|
||||||
self.starting_items[item.name] += 1
|
if item.name not in SaveContext.giveable_items:
|
||||||
if item.type == 'Song':
|
raise Exception(f"Invalid OoT starting item: {item.name}")
|
||||||
self.starting_songs = True
|
else:
|
||||||
# Call the junk fill and get a replacement
|
self.starting_items[item.name] += 1
|
||||||
if item in self.itempool:
|
if item.type == 'Song':
|
||||||
self.itempool.remove(item)
|
self.starting_songs = True
|
||||||
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
|
# Call the junk fill and get a replacement
|
||||||
|
if item in self.itempool:
|
||||||
|
self.itempool.remove(item)
|
||||||
|
self.itempool.append(self.create_item(*get_junk_item(pool=junk_pool)))
|
||||||
if self.start_with_consumables:
|
if self.start_with_consumables:
|
||||||
self.starting_items['Deku Sticks'] = 30
|
self.starting_items['Deku Sticks'] = 30
|
||||||
self.starting_items['Deku Nuts'] = 40
|
self.starting_items['Deku Nuts'] = 40
|
||||||
|
@ -718,7 +722,6 @@ class OOTWorld(World):
|
||||||
impa = self.world.get_location("Song from Impa", self.player)
|
impa = self.world.get_location("Song from Impa", self.player)
|
||||||
if self.skip_child_zelda:
|
if self.skip_child_zelda:
|
||||||
if impa.item is None:
|
if impa.item is None:
|
||||||
from .SaveContext import SaveContext
|
|
||||||
item_to_place = self.world.random.choice(list(item for item in self.world.itempool if
|
item_to_place = self.world.random.choice(list(item for item in self.world.itempool if
|
||||||
item.player == self.player and item.name in SaveContext.giveable_items))
|
item.player == self.player and item.name in SaveContext.giveable_items))
|
||||||
impa.place_locked_item(item_to_place)
|
impa.place_locked_item(item_to_place)
|
||||||
|
|
|
@ -44,6 +44,7 @@ class SMWorld(World):
|
||||||
itemManager: ItemManager
|
itemManager: ItemManager
|
||||||
|
|
||||||
locations = {}
|
locations = {}
|
||||||
|
hint_blacklist = {'Nothing', 'NoEnergy'}
|
||||||
|
|
||||||
Logic.factory('vanilla')
|
Logic.factory('vanilla')
|
||||||
|
|
||||||
|
@ -85,6 +86,7 @@ class SMWorld(World):
|
||||||
# keeps Nothing items local so no player will ever pickup Nothing
|
# keeps Nothing items local so no player will ever pickup Nothing
|
||||||
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
|
# doing so reduces contribution of this world to the Multiworld the more Nothing there is though
|
||||||
self.world.local_items[self.player].value.add('Nothing')
|
self.world.local_items[self.player].value.add('Nothing')
|
||||||
|
self.world.local_items[self.player].value.add('NoEnergy')
|
||||||
|
|
||||||
if (self.variaRando.args.morphPlacement == "early"):
|
if (self.variaRando.args.morphPlacement == "early"):
|
||||||
self.world.local_items[self.player].value.add('Morph')
|
self.world.local_items[self.player].value.add('Morph')
|
||||||
|
@ -126,7 +128,7 @@ class SMWorld(World):
|
||||||
weaponCount[2] += 1
|
weaponCount[2] += 1
|
||||||
else:
|
else:
|
||||||
isAdvancement = False
|
isAdvancement = False
|
||||||
elif item.Type == 'Nothing':
|
elif item.Category == 'Nothing':
|
||||||
isAdvancement = False
|
isAdvancement = False
|
||||||
|
|
||||||
itemClass = ItemManager.Items[item.Type].Class
|
itemClass = ItemManager.Items[item.Type].Class
|
||||||
|
|
|
@ -16,6 +16,7 @@ from utils.doorsmanager import DoorsManager
|
||||||
from logic.logic import Logic
|
from logic.logic import Logic
|
||||||
|
|
||||||
import utils.log
|
import utils.log
|
||||||
|
from worlds.sm.Options import StartLocation
|
||||||
|
|
||||||
# we need to know the logic before doing anything else
|
# we need to know the logic before doing anything else
|
||||||
def getLogic():
|
def getLogic():
|
||||||
|
@ -498,10 +499,12 @@ class VariaRandomizer:
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
args.startLocation = random.choice(possibleStartAPs)
|
args.startLocation = random.choice(possibleStartAPs)
|
||||||
elif args.startLocation not in possibleStartAPs:
|
elif args.startLocation not in possibleStartAPs:
|
||||||
optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
|
args.startLocation = 'Landing Site'
|
||||||
optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
|
world.start_location[player] = StartLocation(StartLocation.default)
|
||||||
dumpErrorMsgs(args.output, optErrMsgs)
|
#optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
|
||||||
sys.exit(-1)
|
#optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
|
||||||
|
#dumpErrorMsgs(args.output, optErrMsgs)
|
||||||
|
#sys.exit(-1)
|
||||||
ap = getAccessPoint(args.startLocation)
|
ap = getAccessPoint(args.startLocation)
|
||||||
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
|
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
|
||||||
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")
|
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")
|
||||||
|
|
Loading…
Reference in New Issue