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):
def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None):
def __init__(self, name: str, type_: RegionType, hint, player: int, world: Optional[MultiWorld] = None):
self.name = name
self.type = type
self.type = type_
self.entrances = []
self.exits = []
self.locations = []

View File

@ -23,6 +23,7 @@ import Options
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
categories = set(AutoWorldRegister.world_types)
@ -148,7 +149,7 @@ def main(args=None, callback=ERmain):
if category_name is None:
weights_cache[path][key] = option
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:
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:
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"]:
if "name" not in option_set:
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:
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__
for i, option_set in enumerate(triggers):
try:

View File

@ -125,6 +125,8 @@ class Toggle(Option):
def get_option_name(cls, value):
return ["No", "Yes"][int(value)]
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class DefaultOnToggle(Toggle):
default = 1
@ -184,6 +186,8 @@ class Choice(Option):
else:
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):
range_start = 0

View File

@ -23,7 +23,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.2.2"
__version__ = "0.2.3"
version_tuple = tuplize_version(__version__)
from yaml import load, dump, safe_load

View File

@ -193,6 +193,15 @@ def discord():
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 . 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
return network_data_package
@api_endpoints.route('/datapackage_version')
@cache.cached()
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');
gameDiv.appendChild(expandButton);
const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings);
gameDiv.appendChild(optionsDiv);
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
gameDiv.appendChild(weightedSettingsDiv);
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
gameDiv.appendChild(itemsDiv);
gamesWrapper.appendChild(gameDiv);
collapseButton.addEventListener('click', () => {
collapseButton.classList.add('invisible');
optionsDiv.classList.add('invisible');
weightedSettingsDiv.classList.add('invisible');
itemsDiv.classList.add('invisible');
expandButton.classList.remove('invisible');
});
expandButton.addEventListener('click', () => {
collapseButton.classList.remove('invisible');
optionsDiv.classList.remove('invisible');
weightedSettingsDiv.classList.remove('invisible');
itemsDiv.classList.remove('invisible');
expandButton.classList.add('invisible');
});
});
@ -207,10 +212,10 @@ const buildGameChoice = (games) => {
gameChoiceDiv.appendChild(table);
};
const buildOptionsDiv = (game, settings) => {
const buildWeightedSettingsDiv = (game, settings) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const optionsWrapper = document.createElement('div');
optionsWrapper.classList.add('settings-wrapper');
const settingsWrapper = document.createElement('div');
settingsWrapper.classList.add('settings-wrapper');
Object.keys(settings).forEach((settingName) => {
const setting = settings[settingName];
@ -268,27 +273,6 @@ const buildOptionsDiv = (game, settings) => {
break;
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 rangeTbody = document.createElement('tbody');
@ -324,6 +308,79 @@ const buildOptionsDiv = (game, settings) => {
rangeTbody.appendChild(tr);
}
} else {
const hintText = document.createElement('p');
hintText.classList.add('hint-text');
hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
`below, then press the "Add" button to add a weight for it.<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) => {
if (currentSettings[game][settingName][option] > 0) {
const tr = document.createElement('tr');
@ -403,58 +460,6 @@ const buildOptionsDiv = (game, settings) => {
rangeTable.appendChild(rangeTbody);
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;
default:
@ -462,10 +467,158 @@ const buildOptionsDiv = (game, settings) => {
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 = () => {

View File

@ -90,6 +90,38 @@ html{
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{
display: flex;
flex-direction: row;

View File

@ -9,7 +9,7 @@
{% include 'header/grassHeader.html' %}
<div id="games">
<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>
<p>
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>

View File

@ -31,7 +31,7 @@
<tr>
<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>>={{ room.seed.slots|length }}</td>
<td>{{ room.seed.slots|length }}</td>
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
</tr>
@ -56,7 +56,7 @@
{% for seed in seeds %}
<tr>
<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>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
</tr>

View File

@ -100,7 +100,7 @@ def uploads():
if file.filename == '':
flash('No selected file')
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:
res = upload_zip_to_db(zfile)
if type(res) == str:
@ -108,12 +108,13 @@ def uploads():
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
file.seek(0) # offset from is_zipfile check
# noinspection PyBroadException
try:
multidata = file.read()
MultiServer.Context.decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
else:
seed = Seed(multidata=multidata, owner=session["_id"])
flush() # place into DB and generate ids

View File

@ -64,7 +64,7 @@ generator:
# 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
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"
# Create a spoiler file
# 0 -> None

View File

@ -9,6 +9,7 @@ Utils.local_path.cached_path = file_path
from BaseClasses import MultiWorld, CollectionState
from worlds.alttp.Items import ItemFactory
class TestBase(unittest.TestCase):
world: MultiWorld
_state_cache = {}

View File

View File

@ -1,4 +1,4 @@
from typing import NamedTuple
from typing import NamedTuple, List
import unittest
from worlds.AutoWorld import World
from Fill import FillError, fill_restrictive
@ -28,8 +28,8 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
class PlayerDefinition(NamedTuple):
id: int
menu: Region
locations: list[Location]
prog_items: list[Item]
locations: List[Location]
prog_items: List[Item]
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)
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 = []
for i in range(count):
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
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 = []
for i in range(count):
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.EntranceShuffle import link_entrances
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.Regions import create_regions
from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase
from worlds import AutoWorld
class TestMinor(TestBase):
def setUp(self):
self.world = MultiWorld(1)
@ -30,8 +30,10 @@ class TestMinor(TestBase):
self.world.worlds[1].create_items()
self.world.required_medallions[1] = ['Ether', 'Quake']
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 2', 1).item = None
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 worlds.alttp.Items import ItemFactory
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):

View File

@ -95,7 +95,7 @@ class ShopPriceModifier(Range):
"""Percentage modifier for shuffled item prices in shops"""
range_start = 0
default = 100
range_end = 10000
range_end = 400
class WorldState(Choice):
option_standard = 1

View File

@ -264,7 +264,7 @@ def ShopSlotFill(world):
price = world.random.randrange(8, 56)
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)
if 'P' in world.shop_shuffle[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()
for player in world.get_game_players("A Link to the Past"):
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)
if not world.ganonstower_vanilla[player] or \
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
# TODO: this might be worthwhile to introduce as generic option for various games and then optimize it
if standard_keyshuffle_players:
viable = []
viable = {}
for location in world.get_locations():
if location.player in standard_keyshuffle_players \
and location.item is None \
and location.can_reach(world.state):
viable.append(location)
world.random.shuffle(viable)
viable.setdefault(location.player, []).append(location)
for player in standard_keyshuffle_players:
loc = world.random.choice(viable[player])
key = world.create_item("Small Key (Hyrule Castle)", player)
loc = viable.pop()
loc.place_locked_item(key)
fill_locations.remove(loc)
world.random.shuffle(fill_locations)
# TODO: investigate not creating the key in the first place
progitempool[:] = [item for item in progitempool if
item.player not in standard_keyshuffle_players or
item.name != "Small Key (Hyrule Castle)"]
if __debug__:
# keeping this here while I'm not sure we caught all instances of multiple HC small keys in the pool
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:
locations_mapping = {player: [] for player in trash_counts}

View File

@ -68,16 +68,12 @@ class HKWorld(World):
self.world.itempool += pool
def set_rules(self):
set_rules(self.world, self.player)
def create_regions(self):
create_regions(self.world, self.player)
def generate_output(self):
pass # Hollow Knight needs no output files
def fill_slot_data(self):
slot_data = {}
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)
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:
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 .Hints import hint_dist_keys, get_hint_area, buildWorldGossipHints
from .HintList import getRequiredHints
from .SaveContext import SaveContext
from Utils import get_options, output_path
from BaseClasses import MultiWorld, CollectionState, RegionType
@ -471,13 +472,16 @@ class OOTWorld(World):
self.remove_from_start_inventory.remove(item.name)
removed_items.append(item.name)
else:
self.starting_items[item.name] += 1
if item.type == 'Song':
self.starting_songs = True
# 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 item.name not in SaveContext.giveable_items:
raise Exception(f"Invalid OoT starting item: {item.name}")
else:
self.starting_items[item.name] += 1
if item.type == 'Song':
self.starting_songs = True
# 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:
self.starting_items['Deku Sticks'] = 30
self.starting_items['Deku Nuts'] = 40
@ -718,7 +722,6 @@ class OOTWorld(World):
impa = self.world.get_location("Song from Impa", self.player)
if self.skip_child_zelda:
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.player == self.player and item.name in SaveContext.giveable_items))
impa.place_locked_item(item_to_place)

View File

@ -44,6 +44,7 @@ class SMWorld(World):
itemManager: ItemManager
locations = {}
hint_blacklist = {'Nothing', 'NoEnergy'}
Logic.factory('vanilla')
@ -85,6 +86,7 @@ class SMWorld(World):
# 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
self.world.local_items[self.player].value.add('Nothing')
self.world.local_items[self.player].value.add('NoEnergy')
if (self.variaRando.args.morphPlacement == "early"):
self.world.local_items[self.player].value.add('Morph')
@ -126,7 +128,7 @@ class SMWorld(World):
weaponCount[2] += 1
else:
isAdvancement = False
elif item.Type == 'Nothing':
elif item.Category == 'Nothing':
isAdvancement = False
itemClass = ItemManager.Items[item.Type].Class

View File

@ -16,6 +16,7 @@ from utils.doorsmanager import DoorsManager
from logic.logic import Logic
import utils.log
from worlds.sm.Options import StartLocation
# we need to know the logic before doing anything else
def getLogic():
@ -498,10 +499,12 @@ class VariaRandomizer:
sys.exit(-1)
args.startLocation = random.choice(possibleStartAPs)
elif args.startLocation not in possibleStartAPs:
optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
dumpErrorMsgs(args.output, optErrMsgs)
sys.exit(-1)
args.startLocation = 'Landing Site'
world.start_location[player] = StartLocation(StartLocation.default)
#optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation]))
#optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs))
#dumpErrorMsgs(args.output, optErrMsgs)
#sys.exit(-1)
ap = getAccessPoint(args.startLocation)
if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True:
forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location")