Merge branch 'main' into docs_consolidation

This commit is contained in:
Hussein Farran 2022-01-09 14:50:35 -05:00
commit b94d401d09
28 changed files with 434 additions and 133 deletions

View File

@ -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 = []

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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():

View File

@ -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.

View File

@ -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!

View File

@ -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"
]
}
]
}
]
} }
] ]

View File

@ -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 = () => {

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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 = {}

View File

View File

@ -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)

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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}

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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")