Merge branch 'main' into docs_consolidation

# Conflicts:
#	WebHostLib/static/assets/tutorial/timespinner/setup_en.md
This commit is contained in:
Hussein Farran 2022-01-04 17:20:13 -05:00
commit a722ec1c37
37 changed files with 1829 additions and 104 deletions

5
.gitignore vendored
View File

@ -155,4 +155,7 @@ Archipelago.zip
#minecraft server stuff
jdk*/
minecraft*/
minecraft*/
#pyenv
.python-version

View File

@ -491,7 +491,9 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
for option_key, option in world_type.options.items():
handle_option(ret, game_weights, option_key, option)
for option_key, option in Options.per_game_common_options.items():
handle_option(ret, game_weights, option_key, option)
# skip setting this option if already set from common_options, defaulting to root option
if not (option_key in Options.common_options and option_key not in game_weights):
handle_option(ret, game_weights, option_key, option)
if "items" in plando_options:
ret.plando_items = roll_item_plando(world_type, game_weights)
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":

View File

@ -235,11 +235,11 @@ class Context:
with open(multidatapath, 'rb') as f:
data = f.read()
self._load(self._decompress(data), use_embedded_server_options)
self._load(self.decompress(data), use_embedded_server_options)
self.data_filename = multidatapath
@staticmethod
def _decompress(data: bytes) -> dict:
def decompress(data: bytes) -> dict:
format_version = data[0]
if format_version != 1:
raise Exception("Incompatible multidata.")

View File

@ -12,6 +12,7 @@ from worlds.oot.Cosmetics import patch_cosmetics
from worlds.oot.Options import cosmetic_options, sfx_options
from worlds.oot.Rom import Rom, compress_rom_file
from worlds.oot.N64Patch import apply_patch_file
from worlds.oot.Utils import data_path
from Utils import local_path
logger = logging.getLogger('OoTAdjuster')
@ -211,9 +212,11 @@ def adjust(args):
ootworld.logic_rules = 'glitched' if args.is_glitched else 'glitchless'
ootworld.death_link = args.deathlink
delete_zootdec = False
if os.path.splitext(args.rom)[-1] in ['.z64', '.n64']:
# Load up the ROM
rom = Rom(file=args.rom, force_use=True)
delete_zootdec = True
elif os.path.splitext(args.rom)[-1] == '.apz5':
# Load vanilla ROM
rom = Rom(file=args.vanilla_rom, force_use=True)
@ -222,15 +225,21 @@ def adjust(args):
else:
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
# Call patch_cosmetics
patch_cosmetics(ootworld, rom)
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
# Output new file
path_pieces = os.path.splitext(args.rom)
decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
comp_path = path_pieces[0] + '-adjusted.n64'
rom.write_to_file(decomp_path)
compress_rom_file(decomp_path, comp_path)
os.remove(decomp_path)
try:
patch_cosmetics(ootworld, rom)
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
# Output new file
path_pieces = os.path.splitext(args.rom)
decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
comp_path = path_pieces[0] + '-adjusted.n64'
rom.write_to_file(decomp_path)
os.chdir(data_path("Compress"))
compress_rom_file(decomp_path, comp_path)
os.remove(decomp_path)
finally:
if delete_zootdec:
os.chdir(os.path.split(__file__)[0])
os.remove("ZOOTDEC.z64")
return comp_path
if __name__ == '__main__':

View File

@ -14,6 +14,7 @@ Currently, the following games are supported:
* Super Metroid
* Secret of Evermore
* Final Fantasy
* Rogue Legacy
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@ -523,10 +523,13 @@ def launch_sni(ctx: Context):
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
if os.path.isdir(sni_path):
for file in os.listdir(sni_path):
lower_file = file.lower()
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or lower_file == "sni":
sni_path = os.path.join(sni_path, file)
dir_entry: os.DirEntry
for dir_entry in os.scandir(sni_path):
if dir_entry.is_file():
lower_file = dir_entry.name.lower()
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or (lower_file == "sni"):
sni_path = dir_entry.path
break
if os.path.isfile(sni_path):
snes_logger.info(f"Attempting to start {sni_path}")

View File

@ -89,6 +89,11 @@ def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):

View File

@ -76,7 +76,7 @@ class WebHostContext(Context):
else:
self.port = get_random_port()
return self._load(self._decompress(room.seed.multidata), True)
return self._load(self.decompress(room.seed.multidata), True)
@db_session
def init_save(self, enabled: bool = True):

View File

@ -37,8 +37,6 @@ def create():
}
for game_name, world in AutoWorldRegister.world_types.items():
if (world.hidden):
continue
all_options = {**world.options, **Options.per_game_common_options}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
@ -99,12 +97,14 @@ def create():
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
json.dump(player_settings, f, indent=2, separators=(',', ': '))
weighted_settings["baseOptions"]["game"][game_name] = 0
weighted_settings["games"][game_name] = {}
weighted_settings["games"][game_name]["gameOptions"] = game_options
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_name_to_id.keys())
if not world.hidden:
weighted_settings["baseOptions"]["game"][game_name] = 0
weighted_settings["games"][game_name] = {}
weighted_settings["games"][game_name]["gameSettings"] = game_options
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
f.write(json.dumps(weighted_settings, indent=2, separators=(',', ': ')))
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))

View File

@ -0,0 +1,22 @@
# Rogue Legacy (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?
You are not able to buy skill upgrades in the manor upgrade screen, and instead, need to find them in order to level up
your character to make fighting the 5 bosses easier.
## What items and locations get shuffled?
All the skill upgrades, class upgrades, runes packs, and equipment packs are shuffled in the manor upgrade screen,
diary checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the
finding of stats less of a chore. Runes and Equipment are also grouped together.
## 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, your character will hold the item above their head and display it to the world. It's
good for business!

View File

@ -0,0 +1,47 @@
# Rogue Legacy Randomizer Setup Guide
## Required Software
- [Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/releases)
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
each player to enjoy an experience customized for their taste, and different players in the same multiworld
can all have different options.
### Where do I get a YAML file?
you can customize your settings by visiting the <a href="/games/Rogue Legacy/player-settings">rogue legacy settings page here</a>.
### Connect to the MultiServer
Once in game, press the start button and the AP connection screen should appear. You will fill out the hostname, port,
slot name, and password (if applicable). You should only need to fill out hostname, port, and password if the server
provides an alternative one to the default values.
### Play the game
Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen.
Now you're off to start your legacy!
## Manual Installation
In order to run Rogue Legacy Randomizer you will need to have Rogue Legacy installed on your local machine. Extract the
Randomizer release into a desired folder **outside** of your Rogue Legacy install. Copy the following files from your
Rogue Legacy install into the main directory of your Rogue Legacy Randomizer install:
- DS2DEngine.dll
- InputSystem.dll
- Nuclex.Input.dll
- SpriteSystem.dll
- Tweener.dll
And copy the directory from your Rogue Legacy install as well into the main directory of your Rogue Legacy Randomizer
install:
- Content/
Then copy the contents of the CustomContent directory in your Rogue Legacy Randomizer into the newly copied Content
directory and overwrite all files.
**BE SURE YOU ARE REPLACING THE COPIED FILES IN YOUR ROGUE LEGACY RANDOMIZER DIRECTORY AND NOT REPLACING YOUR ROGUE
LEGACY FILES!**

View File

@ -14,64 +14,20 @@ randomization of the items.
## Installation Procedures
Download latest version of Timespinner randomizer you can find the .zip files on the releases page, download the zip for
your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run
TsRandomizer.exe instead of Timespinner.exe to start the game in randomized mode, for more info see the Timespinner
randomizer readme.
Timespinner Randomizer downloads
page: [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases)
Timespinner Randomizer readme page: [Timespinner Randomizer GitHub](https://github.com/JarnoWesthof/TsRandomizer)
Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you can find the .zip files on the releases page, download the zip for your current platform. Then extract the zip to the folder where your Timespinner game is installed. Then just run TsRandomizer.exe (on windows) or TsRandomizerItemTracker.bin.x86_64 (on linux) or TsRandomizerItemTracker.bin.osx (on mac) instead of Timespinner.exe to start the game in randomized mode, for more info see the [ReadMe for TsRandomizer](https://github.com/JarnoWesthof/TsRandomizer)
## Joining a MultiWorld Game
1. Run TsRandomizer.exe
2. Select "New Game"
3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard
3. Switch "<< Select Seed >>" to "<< Archiplago >>" by pressing left on the controller or keyboard
4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails
* NOTE: the input fields support Ctrl + V pasting of values
* NOTE: the input fields support Ctrl + V pasting of values
5. Select "Connect"
6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a
difficulty
6. If all went well you will be taken back the difficulty selection menu and the game will start as soon as you select a difficulty
## YAML Settings
## Where do I get a config file?
The [Timespinner Player Settings Page](https://archipelago.gg/games/Timespinner/player-settings) on the website allows you to configure your personal settings and export a config file from them.
An example YAML would look like this:
```yaml
description: Default Timespinner Template
name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
game:
Timespinner: 1
requires:
version: 0.1.8
Timespinner:
StartWithJewelryBox: # Start with Jewelry Box unlocked
false: 50
true: 0
DownloadableItems: # With the tablet you will be able to download items at terminals
false: 50
true: 50
FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors
false: 50
true: 0
StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer
false: 50
true: 50
QuickSeed: # Start with Talaria Attachment, Nyoom!
false: 50
true: 0
SpecificKeycards: # Keycards can only open corresponding doors
false: 0
true: 50
Inverted: # Start in the past
false: 50
true: 50
```
* All Options are either enabled or not, if values are specified for both true & false the generator will select one
based on weight
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported
on Archipelago generated seeds
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds

View File

@ -371,5 +371,24 @@
]
}
]
},
{
"gameTitle": "Rogue Legacy",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "rogue-legacy/rogue-legacy_en.md",
"link": "rogue-legacy/rogue-legacy/en",
"authors": [
"Phar"
]
}
]
}
]
}
]

View File

@ -0,0 +1,588 @@
window.addEventListener('load', () => {
fetchSettingData().then((results) => {
let settingHash = localStorage.getItem('weighted-settings-hash');
if (!settingHash) {
// If no hash data has been set before, set it now
localStorage.setItem('weighted-settings-hash', md5(results));
localStorage.removeItem('weighted-settings');
settingHash = md5(results);
}
if (settingHash !== md5(results)) {
const userMessage = document.getElementById('user-message');
userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
"them all to default.";
userMessage.style.display = "block";
userMessage.addEventListener('click', resetSettings);
}
// Page setup
createDefaultSettings(results);
buildUI(results);
updateVisibleGames();
adjustHeaderWidth();
// Event listeners
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
const weightedSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const nameInput = document.getElementById('player-name');
nameInput.setAttribute('data-type', 'data');
nameInput.setAttribute('data-setting', 'name');
nameInput.addEventListener('keyup', updateBaseSetting);
nameInput.value = weightedSettings.name;
});
});
const resetSettings = () => {
localStorage.removeItem('weighted-settings');
localStorage.removeItem('weighted-settings-hash')
window.location.reload();
};
const fetchSettingData = () => new Promise((resolve, reject) => {
fetch(new Request(`${window.location.origin}/static/generated/weighted-settings.json`)).then((response) => {
try{ resolve(response.json()); }
catch(error){ reject(error); }
});
});
const createDefaultSettings = (settingData) => {
if (!localStorage.getItem('weighted-settings')) {
const newSettings = {};
// Transfer base options directly
for (let baseOption of Object.keys(settingData.baseOptions)){
newSettings[baseOption] = settingData.baseOptions[baseOption];
}
// Set options per game
for (let game of Object.keys(settingData.games)) {
// Initialize game object
newSettings[game] = {};
// Transfer game settings
for (let gameSetting of Object.keys(settingData.games[game].gameSettings)){
newSettings[game][gameSetting] = {};
const setting = settingData.games[game].gameSettings[gameSetting];
switch(setting.type){
case 'select':
setting.options.forEach((option) => {
newSettings[game][gameSetting][option.value] =
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === option.value) ? 25 : 0;
});
break;
case 'range':
for (let i = setting.min; i <= setting.max; ++i){
newSettings[game][gameSetting][i] =
(setting.hasOwnProperty('defaultValue') && setting.defaultValue === i) ? 25 : 0;
}
newSettings[game][gameSetting]['random'] = 0;
newSettings[game][gameSetting]['random-low'] = 0;
newSettings[game][gameSetting]['random-high'] = 0;
break;
default:
console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
}
}
newSettings[game].start_inventory = [];
newSettings[game].exclude_locations = [];
newSettings[game].local_items = [];
newSettings[game].non_local_items = [];
newSettings[game].start_hints = [];
}
localStorage.setItem('weighted-settings', JSON.stringify(newSettings));
}
};
// TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints
// TODO: Include location configs: exclude_locations
const buildUI = (settingData) => {
// Build the game-choice div
buildGameChoice(settingData.games);
const gamesWrapper = document.getElementById('games-wrapper');
Object.keys(settingData.games).forEach((game) => {
// Create game div, invisible by default
const gameDiv = document.createElement('div');
gameDiv.setAttribute('id', `${game}-div`);
gameDiv.classList.add('game-div');
gameDiv.classList.add('invisible');
const gameHeader = document.createElement('h2');
gameHeader.innerText = game;
gameDiv.appendChild(gameHeader);
const collapseButton = document.createElement('a');
collapseButton.innerText = '(Collapse)';
gameDiv.appendChild(collapseButton);
const expandButton = document.createElement('a');
expandButton.innerText = '(Expand)';
expandButton.classList.add('invisible');
gameDiv.appendChild(expandButton);
const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings);
gameDiv.appendChild(optionsDiv);
gamesWrapper.appendChild(gameDiv);
collapseButton.addEventListener('click', () => {
collapseButton.classList.add('invisible');
optionsDiv.classList.add('invisible');
expandButton.classList.remove('invisible');
});
expandButton.addEventListener('click', () => {
collapseButton.classList.remove('invisible');
optionsDiv.classList.remove('invisible');
expandButton.classList.add('invisible');
});
});
};
const buildGameChoice = (games) => {
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
const gameChoiceDiv = document.getElementById('game-choice');
const h2 = document.createElement('h2');
h2.innerText = 'Game Select';
gameChoiceDiv.appendChild(h2);
const gameSelectDescription = document.createElement('p');
gameSelectDescription.classList.add('setting-description');
gameSelectDescription.innerText = 'Choose which games you might be required to play.';
gameChoiceDiv.appendChild(gameSelectDescription);
const hintText = document.createElement('p');
hintText.classList.add('hint-text');
hintText.innerText = 'If a game\'s value is greater than zero, you can click it\'s name to jump ' +
'to that section.'
gameChoiceDiv.appendChild(hintText);
// Build the game choice table
const table = document.createElement('table');
const tbody = document.createElement('tbody');
Object.keys(games).forEach((game) => {
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
const span = document.createElement('span');
span.innerText = game;
span.setAttribute('id', `${game}-game-option`)
tdLeft.appendChild(span);
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.setAttribute('data-type', 'weight');
range.setAttribute('data-setting', 'game');
range.setAttribute('data-option', game);
range.value = settings.game[game];
range.addEventListener('change', (evt) => {
updateBaseSetting(evt);
updateVisibleGames(); // Show or hide games based on the new settings
});
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `game-${game}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
tbody.appendChild(tr);
});
table.appendChild(tbody);
gameChoiceDiv.appendChild(table);
};
const buildOptionsDiv = (game, settings) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const optionsWrapper = document.createElement('div');
optionsWrapper.classList.add('settings-wrapper');
Object.keys(settings).forEach((settingName) => {
const setting = settings[settingName];
const settingWrapper = document.createElement('div');
settingWrapper.classList.add('setting-wrapper');
const settingNameHeader = document.createElement('h4');
settingNameHeader.innerText = setting.displayName;
settingWrapper.appendChild(settingNameHeader);
const settingDescription = document.createElement('p');
settingDescription.classList.add('setting-description');
settingDescription.innerText = setting.description.replace(/(\n)/g, ' ');
settingWrapper.appendChild(settingDescription);
switch(setting.type){
case 'select':
const optionTable = document.createElement('table');
const tbody = document.createElement('tbody');
// Add a weight range for each option
setting.options.forEach((option) => {
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = option.name;
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('data-game', game);
range.setAttribute('data-setting', settingName);
range.setAttribute('data-option', option.value);
range.setAttribute('data-type', setting.type);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.value = currentSettings[game][settingName][option.value];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${option.value}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
tbody.appendChild(tr);
});
optionTable.appendChild(tbody);
settingWrapper.appendChild(optionTable);
break;
case 'range':
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');
if (((setting.max - setting.min) + 1) < 11) {
for (let i=setting.min; i <= setting.max; ++i) {
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = i;
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('id', `${game}-${settingName}-${i}-range`);
range.setAttribute('data-game', game);
range.setAttribute('data-setting', settingName);
range.setAttribute('data-option', i);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.value = currentSettings[game][settingName][i];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${i}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
rangeTbody.appendChild(tr);
}
} else {
Object.keys(currentSettings[game][settingName]).forEach((option) => {
if (currentSettings[game][settingName][option] > 0) {
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);
}
});
}
['random', 'random-low', 'random-high'].forEach((option) => {
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][option];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
rangeTbody.appendChild(tr);
});
rangeTable.appendChild(rangeTbody);
settingWrapper.appendChild(rangeTable);
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:
console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`);
return;
}
optionsWrapper.appendChild(settingWrapper);
});
return optionsWrapper;
};
const updateVisibleGames = () => {
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
Object.keys(settings.game).forEach((game) => {
const gameDiv = document.getElementById(`${game}-div`);
const gameOption = document.getElementById(`${game}-game-option`);
if (parseInt(settings.game[game], 10) > 0) {
gameDiv.classList.remove('invisible');
gameOption.classList.add('jump-link');
gameOption.addEventListener('click', () => {
const gameDiv = document.getElementById(`${game}-div`);
if (gameDiv.classList.contains('invisible')) { return; }
gameDiv.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
});
} else {
gameDiv.classList.add('invisible');
gameOption.classList.remove('jump-link');
}
});
};
const updateBaseSetting = (event) => {
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
const setting = event.target.getAttribute('data-setting');
const option = event.target.getAttribute('data-option');
const type = event.target.getAttribute('data-type');
switch(type){
case 'weight':
settings[setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
document.getElementById(`${setting}-${option}`).innerText = event.target.value;
break;
case 'data':
settings[setting] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10);
break;
}
localStorage.setItem('weighted-settings', JSON.stringify(settings));
};
const updateGameSetting = (event) => {
const options = JSON.parse(localStorage.getItem('weighted-settings'));
const game = event.target.getAttribute('data-game');
const setting = event.target.getAttribute('data-setting');
const option = event.target.getAttribute('data-option');
const type = event.target.getAttribute('data-type');
document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value;
options[game][setting][option] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
localStorage.setItem('weighted-settings', JSON.stringify(options));
};
const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') {
const userMessage = document.getElementById('user-message');
userMessage.innerText = 'You forgot to set your player name at the top of the page!';
userMessage.classList.add('visible');
window.scrollTo(0, 0);
return;
}
// Clean up the settings output
Object.keys(settings.game).forEach((game) => {
// Remove any disabled games
if (settings.game[game] === 0) {
delete settings.game[game];
delete settings[game];
return;
}
// Remove any disabled options
Object.keys(settings[game]).forEach((setting) => {
Object.keys(settings[game][setting]).forEach((option) => {
if (settings[game][setting][option] === 0) {
delete settings[game][setting][option];
}
});
});
});
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const generateGame = (raceMode = false) => {
axios.post('/api/generate', {
weights: { player: localStorage.getItem('weighted-settings') },
presetData: { player: localStorage.getItem('weighted-settings') },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage.innerText += ' ' + error.response.data.text;
}
userMessage.classList.add('visible');
window.scrollTo(0, 0);
console.error(error);
});
};

View File

@ -0,0 +1,191 @@
html{
background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
scroll-padding-top: 90px;
}
#weighted-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#weighted-settings #games-wrapper{
width: 100%;
}
#weighted-settings .setting-wrapper{
width: 100%;
margin-bottom: 2rem;
}
#weighted-settings .setting-wrapper .add-option-div{
display: flex;
flex-direction: row;
justify-content: flex-start;
margin-bottom: 1rem;
}
#weighted-settings .setting-wrapper .add-option-div button{
width: auto;
height: auto;
margin: 0 0 0 0.15rem;
padding: 0 0.25rem;
border-radius: 4px;
cursor: default;
}
#weighted-settings .setting-wrapper .add-option-div button:active{
margin-bottom: 1px;
}
#weighted-settings p.setting-description{
font-weight: bold;
margin: 0 0 1rem;
}
#weighted-settings p.hint-text{
margin: 0 0 1rem;
font-style: italic;
}
#weighted-settings .jump-link{
color: #ffef00;
cursor: pointer;
text-decoration: underline;
}
#weighted-settings table{
width: 100%;
}
#weighted-settings table .td-left{
padding-right: 1rem;
width: 200px;
}
#weighted-settings table .td-middle{
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding-right: 1rem;
}
#weighted-settings table .td-right{
width: 4rem;
text-align: right;
}
#weighted-settings table .td-delete{
width: 50px;
text-align: right;
}
#weighted-settings table .range-option-delete{
cursor: pointer;
}
#weighted-settings #weighted-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#weighted-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#weighted-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#weighted-settings #user-message.visible{
display: block;
}
#weighted-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#weighted-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#weighted-settings a{
color: #ffef00;
cursor: pointer;
}
#weighted-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#weighted-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#weighted-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#weighted-settings .game-options, #weighted-settings .rom-options{
display: flex;
flex-direction: column;
}
#weighted-settings .invisible{
display: none;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#weighted-settings .game-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#game-options table label{
display: block;
min-width: 200px;
}
}

View File

@ -0,0 +1,42 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>{{ game }} Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weighted-settings.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weighted-settings.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="weighted-settings" data-game="{{ game }}">
<div id="user-message"></div>
<h1>Weighted Settings</h1>
<p>Choose the games and options you would like to play with! You may generate a single-player game from
this page, or download a settings file you can use to participate in a MultiWorld.</p>
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
items if you are playing in a MultiWorld.</label><br />
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
</p>
<div id="game-choice">
<!-- User chooses games by weight -->
</div>
<!-- To be generated and populated per-game with weight > 0 -->
<div id="games-wrapper">
</div>
<div id="weighted-settings-button-row">
<button id="export-settings">Export Settings</button>
<button id="generate-game">Generate Game</button>
<button id="generate-race">Generate Race</button>
</div>
</div>
{% endblock %}

View File

@ -252,7 +252,7 @@ def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None)
if result:
return result
multidata = Context._decompress(room.seed.multidata)
multidata = Context.decompress(room.seed.multidata)
# in > 100 players this can take a bit of time and is the main reason for the cache
locations: Dict[int, Dict[int, Tuple[int, int]]] = multidata['locations']
names: Dict[int, Dict[int, str]] = multidata["names"]

View File

@ -67,7 +67,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
multidata = None
if multidata:
decompressed_multidata = MultiServer.Context._decompress(multidata)
decompressed_multidata = MultiServer.Context.decompress(multidata)
player_names = {slot.player_name for slot in slots}
leftover_names = [(name, index) for index, name in
enumerate((name for name in decompressed_multidata["names"][0]), start=1)]
@ -100,7 +100,7 @@ def uploads():
if file.filename == '':
flash('No selected file')
elif file and allowed_file(file.filename):
if file.filename.endswith(".zip"):
if zipfile.is_zipfile(file.filename):
with zipfile.ZipFile(file, 'r') as zfile:
res = upload_zip_to_db(zfile)
if type(res) == str:
@ -108,12 +108,12 @@ def uploads():
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
# noinspection PyBroadException
try:
multidata = file.read()
MultiServer.Context._decompress(multidata)
MultiServer.Context.decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
raise
else:
seed = Seed(multidata=multidata, owner=session["_id"])
flush() # place into DB and generate ids

View File

@ -55,13 +55,13 @@ Sent to clients when they connect to an Archipelago server.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
| version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
| password | bool | Denoted whether a password is required to join this room.|
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
| permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" and "remaining". |
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. |
| games | list\[str\] | sorted list of game names for the players, so first player's game will be games\[0\]. Matches game names in datapackage. |
| datapackage_version | int | Data version of the [data package](#Data-Package-Contents) the server will send. Used to update the client's (optional) local cache. |
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. |
@ -114,7 +114,7 @@ Sent to clients when the connection handshake is successfully completed.
| ---- | ---- | ----- |
| team | int | Your team number. See [NetworkPlayer](#NetworkPlayer) for more info on team number. |
| slot | int | Your slot number on your team. See [NetworkPlayer](#NetworkPlayer) for more info on the slot number. |
| players | list\[NetworkPlayer\] | List denoting other players in the multiworld, whether connected or not. See [NetworkPlayer](#NetworkPlayer) for info on the format. |
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | List denoting other players in the multiworld, whether connected or not. |
| missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. |
| checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. |
| slot_data | dict | Contains a json object for slot related data, differs per game. Empty if not required. |
@ -125,14 +125,14 @@ Sent to clients when they receive an item.
| Name | Type | Notes |
| ---- | ---- | ----- |
| index | int | The next empty slot in the list of items for the receiving client. |
| items | list\[NetworkItem\] | The items which the client is receiving. See [NetworkItem](#NetworkItem) for more details. |
| items | list\[[NetworkItem](#NetworkItem)\] | The items which the client is receiving. |
### LocationInfo
Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) packet and responds with the item in the location(s) being scouted.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| locations | list\[NetworkItem\] | Contains list of item(s) in the location(s) scouted. See [NetworkItem](#NetworkItem) for more details. |
| locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. |
### RoomUpdate
Sent when there is a need to update information about the present game session. Generally useful for async games.
@ -143,7 +143,7 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
| Name | Type | Notes |
| ---- | ---- | ----- |
| hint_points | int | New argument. The client's current hint points. |
| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. |
| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Changed argument. Always sends all players, whether connected or not. |
| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. |
@ -161,10 +161,10 @@ Sent to clients purely to display a message to the player. This packet differs f
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | list\[JSONMessagePart\] | See [JSONMessagePart](#JSONMessagePart) for more details on this type. |
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
### DataPackage
@ -173,7 +173,7 @@ Sent to clients to provide what is known as a 'data package' which contains info
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | DataPackageObject | The data package as a JSON object. More details on its contents may be found at [Data Package Contents](#Data-Package-Contents) |
| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. |
### Bounced
Sent to clients after a client requested this message be sent to them, more info in the Bounce package.
@ -213,7 +213,7 @@ Sent by the client to initiate a connection to an Archipelago game session.
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
| name | str | The player name for this client. |
| uuid | str | Unique identifier for player client. |
| version | NetworkVersion | An object representing the Archipelago version this client supports. |
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
#### Authentication

View File

@ -91,6 +91,11 @@ class ShopItemSlots(Range):
range_start = 0
range_end = 30
class ShopPriceModifier(Range):
"""Percentage modifier for shuffled item prices in shops"""
range_start = 0
default = 100
range_end = 10000
class WorldState(Choice):
option_standard = 1
@ -306,6 +311,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
"killable_thieves": KillableThieves,
"bush_shuffle": BushShuffle,
"shop_item_slots": ShopItemSlots,
"shop_price_modifier": ShopPriceModifier,
"tile_shuffle": TileShuffle,
"ow_palettes": OWPalette,
"uw_palettes": UWPalette,

View File

@ -247,7 +247,12 @@ def ShopSlotFill(world):
item_name = location.item.name
if location.item.game != "A Link to the Past":
price = world.random.randrange(1, 28)
if location.item.advancement:
price = world.random.randrange(8, 56)
elif location.item.never_exclude:
price = world.random.randrange(4, 28)
else:
price = world.random.randrange(2, 14)
elif any(x in item_name for x in
['Compass', 'Map', 'Single Bomb', 'Single Arrow', 'Piece of Heart']):
price = world.random.randrange(1, 7)
@ -258,7 +263,8 @@ def ShopSlotFill(world):
else:
price = world.random.randrange(8, 56)
shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
shop.push_inventory(location.shop_slot, item_name,
min(int(price * 5 * world.shop_price_modifier[location.player] / 100), 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

@ -312,7 +312,7 @@ def get_hint_area(spot):
spot_queue.extend(list(filter(lambda ent: ent not in already_checked, parent_region.entrances)))
raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.world.id))
raise HintAreaNotFound('No hint area could be found for %s [World %d]' % (spot, spot.player))
else:
return spot.name

View File

@ -191,8 +191,8 @@ world_options: typing.Dict[str, type(Option)] = {
"owl_drops": OwlDrops,
"warp_songs": WarpSongs,
"spawn_positions": SpawnPositions,
"mix_entrance_pools": MixEntrancePools,
"decouple_entrances": DecoupleEntrances,
# "mix_entrance_pools": MixEntrancePools,
# "decouple_entrances": DecoupleEntrances,
"triforce_hunt": TriforceHunt,
"triforce_goal": TriforceGoal,
"extra_triforce_percentage": ExtraTriforces,

View File

@ -282,7 +282,7 @@ class Rom(BigStream):
def compress_rom_file(input_file, output_file):
compressor_path = data_path("Compress")
compressor_path = "."
if platform.system() == 'Windows':
executable_path = "Compress.exe"

View File

@ -186,6 +186,8 @@ class OOTWorld(World):
self.mq_dungeons_random = False # this will be a deprecated option later
self.ocarina_songs = False # just need to pull in the OcarinaSongs module
self.big_poe_count = 1 # disabled due to client-side issues for now
self.mix_entrance_pools = False
self.decouple_entrances = False
# Set internal names used by the OoT generator
self.keysanity = self.shuffle_smallkeys in ['keysanity', 'remove', 'any_dungeon', 'overworld']
@ -827,7 +829,12 @@ class OOTWorld(World):
or (loc.player in item_hint_players and loc.name in world.worlds[loc.player].added_hint_types['item'])):
autoworld.major_item_locations.append(loc)
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or loc.item.type == 'Song'):
if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or
(loc.item.type == 'Song' or
(loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or
(loc.item.type == 'FortressSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or
(loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or
(loc.item.type == 'GanonBossKey' and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))):
if loc.player in barren_hint_players:
hint_area = get_hint_area(loc)
items_by_region[loc.player][hint_area]['weight'] += 1

View File

@ -0,0 +1 @@
0 1 2 3 4 5 6 7 8 9 15 16 17 18 19 20 21 22 23 24 25 26 942 944 946 948 950 952 954 956 958 960 962 964 966 968 970 972 974 976 978 980 982 984 986 988 990 992 994 996 998 1000 1002 1004 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525

View File

@ -0,0 +1,129 @@
import typing
from BaseClasses import Item
from .Names import ItemName
class ItemData(typing.NamedTuple):
code: typing.Optional[int]
progression: bool
quantity: int = 1
event: bool = False
class LegacyItem(Item):
game: str = "Rogue Legacy"
def __init__(self, name, advancement: bool = False, code: int = None, player: int = None):
super(LegacyItem, self).__init__(name, advancement, code, player)
# Separate tables for each type of item.
vendors_table = {
ItemName.blacksmith: ItemData(90000, True),
ItemName.enchantress: ItemData(90001, True),
ItemName.architect: ItemData(90002, False),
}
static_classes_table = {
ItemName.knight: ItemData(90080, True),
ItemName.paladin: ItemData(90081, True),
ItemName.mage: ItemData(90082, True),
ItemName.archmage: ItemData(90083, True),
ItemName.barbarian: ItemData(90084, True),
ItemName.barbarian_king: ItemData(90085, True),
ItemName.knave: ItemData(90086, True),
ItemName.assassin: ItemData(90087, True),
ItemName.shinobi: ItemData(90088, True),
ItemName.hokage: ItemData(90089, True),
ItemName.miner: ItemData(90090, True),
ItemName.spelunker: ItemData(90091, True),
ItemName.lich: ItemData(90092, True),
ItemName.lich_king: ItemData(90093, True),
ItemName.spellthief: ItemData(90094, True),
ItemName.spellsword: ItemData(90095, True),
ItemName.dragon: ItemData(90096, True),
ItemName.traitor: ItemData(90097, True),
}
progressive_classes_table = {
ItemName.progressive_knight: ItemData(90003, True, 2),
ItemName.progressive_mage: ItemData(90004, True, 2),
ItemName.progressive_barbarian: ItemData(90005, True, 2),
ItemName.progressive_knave: ItemData(90006, True, 2),
ItemName.progressive_shinobi: ItemData(90007, True, 2),
ItemName.progressive_miner: ItemData(90008, True, 2),
ItemName.progressive_lich: ItemData(90009, True, 2),
ItemName.progressive_spellthief: ItemData(90010, True, 2),
}
skill_unlocks_table = {
ItemName.health: ItemData(90013, True, 15),
ItemName.mana: ItemData(90014, True, 15),
ItemName.attack: ItemData(90015, True, 15),
ItemName.magic_damage: ItemData(90016, True, 15),
ItemName.armor: ItemData(90017, True, 10),
ItemName.equip: ItemData(90018, True, 10),
ItemName.crit_chance: ItemData(90019, False, 5),
ItemName.crit_damage: ItemData(90020, False, 5),
ItemName.down_strike: ItemData(90021, False),
ItemName.gold_gain: ItemData(90022, False),
ItemName.potion_efficiency: ItemData(90023, False),
ItemName.invulnerability_time: ItemData(90024, False),
ItemName.mana_cost_down: ItemData(90025, False),
ItemName.death_defiance: ItemData(90026, False),
ItemName.haggling: ItemData(90027, False),
ItemName.random_children: ItemData(90028, False),
}
blueprints_table = {
ItemName.squire_blueprints: ItemData(90040, True),
ItemName.silver_blueprints: ItemData(90041, True),
ItemName.guardian_blueprints: ItemData(90042, True),
ItemName.imperial_blueprints: ItemData(90043, True),
ItemName.royal_blueprints: ItemData(90044, True),
ItemName.knight_blueprints: ItemData(90045, True),
ItemName.ranger_blueprints: ItemData(90046, True),
ItemName.sky_blueprints: ItemData(90047, True),
ItemName.dragon_blueprints: ItemData(90048, True),
ItemName.slayer_blueprints: ItemData(90049, True),
ItemName.blood_blueprints: ItemData(90050, True),
ItemName.sage_blueprints: ItemData(90051, True),
ItemName.retribution_blueprints: ItemData(90052, True),
ItemName.holy_blueprints: ItemData(90053, True),
ItemName.dark_blueprints: ItemData(90054, True),
}
runes_table = {
ItemName.vault_runes: ItemData(90060, True),
ItemName.sprint_runes: ItemData(90061, True),
ItemName.vampire_runes: ItemData(90062, True),
ItemName.sky_runes: ItemData(90063, True),
ItemName.siphon_runes: ItemData(90064, True),
ItemName.retaliation_runes: ItemData(90065, True),
ItemName.bounty_runes: ItemData(90066, True),
ItemName.haste_runes: ItemData(90067, True),
ItemName.curse_runes: ItemData(90068, True),
ItemName.grace_runes: ItemData(90069, True),
ItemName.balance_runes: ItemData(90070, True),
}
misc_items_table = {
ItemName.trip_stat_increase: ItemData(90030, False),
ItemName.gold_1000: ItemData(90031, False),
ItemName.gold_3000: ItemData(90032, False),
ItemName.gold_5000: ItemData(90033, False),
}
# Complete item table.
item_table = {
**vendors_table,
**static_classes_table,
**progressive_classes_table,
**skill_unlocks_table,
**blueprints_table,
**runes_table,
**misc_items_table,
}
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code}

View File

@ -0,0 +1,85 @@
import typing
from BaseClasses import Location
from .Names import LocationName
class LegacyLocation(Location):
game: str = "Rogue Legacy"
base_location_table = {
# Manor Renovations
LocationName.manor_ground_base: 91000,
LocationName.manor_main_base: 91001,
LocationName.manor_main_bottom_window: 91002,
LocationName.manor_main_top_window: 91003,
LocationName.manor_main_roof: 91004,
LocationName.manor_left_wing_base: 91005,
LocationName.manor_left_wing_window: 91006,
LocationName.manor_left_wing_roof: 91007,
LocationName.manor_left_big_base: 91008,
LocationName.manor_left_big_upper1: 91009,
LocationName.manor_left_big_upper2: 91010,
LocationName.manor_left_big_windows: 91011,
LocationName.manor_left_big_roof: 91012,
LocationName.manor_left_far_base: 91013,
LocationName.manor_left_far_roof: 91014,
LocationName.manor_left_extension: 91015,
LocationName.manor_left_tree1: 91016,
LocationName.manor_left_tree2: 91017,
LocationName.manor_right_wing_base: 91018,
LocationName.manor_right_wing_window: 91019,
LocationName.manor_right_wing_roof: 91020,
LocationName.manor_right_big_base: 91021,
LocationName.manor_right_big_upper: 91022,
LocationName.manor_right_big_roof: 91023,
LocationName.manor_right_high_base: 91024,
LocationName.manor_right_high_upper: 91025,
LocationName.manor_right_high_tower: 91026,
LocationName.manor_right_extension: 91027,
LocationName.manor_right_tree: 91028,
LocationName.manor_observatory_base: 91029,
LocationName.manor_observatory_scope: 91030,
# Boss Rewards
LocationName.boss_khindr: 91100,
LocationName.boss_alexander: 91102,
LocationName.boss_leon: 91104,
LocationName.boss_herodotus: 91106,
# Special Rooms
LocationName.special_jukebox: 91200,
# Special Locations
LocationName.castle: None,
LocationName.garden: None,
LocationName.tower: None,
LocationName.dungeon: None,
LocationName.fountain: None,
}
diary_location_table = {f"{LocationName.diary} {i + 1}": i + 91300 for i in range(0, 25)}
fairy_chest_location_table = {
**{f"{LocationName.castle} - Fairy Chest {i + 1}": i + 91400 for i in range(0, 50)},
**{f"{LocationName.garden} - Fairy Chest {i + 1}": i + 91450 for i in range(0, 50)},
**{f"{LocationName.tower} - Fairy Chest {i + 1}": i + 91500 for i in range(0, 50)},
**{f"{LocationName.dungeon} - Fairy Chest {i + 1}": i + 91550 for i in range(0, 50)},
}
chest_location_table = {
**{f"{LocationName.castle} - Chest {i + 1}": i + 91600 for i in range(0, 100)},
**{f"{LocationName.garden} - Chest {i + 1}": i + 91700 for i in range(0, 100)},
**{f"{LocationName.tower} - Chest {i + 1}": i + 91800 for i in range(0, 100)},
**{f"{LocationName.dungeon} - Chest {i + 1}": i + 91900 for i in range(0, 100)},
}
location_table = {
**base_location_table,
**diary_location_table,
**fairy_chest_location_table,
**chest_location_table,
}
lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in location_table.items()}

View File

@ -0,0 +1,95 @@
# Vendor Definitions
blacksmith = "Blacksmith"
enchantress = "Enchantress"
architect = "Architect"
# Progressive Class Definitions
progressive_knight = "Progressive Knights"
progressive_mage = "Progressive Mages"
progressive_barbarian = "Progressive Barbarians"
progressive_knave = "Progressive Knaves"
progressive_shinobi = "Progressive Shinobis"
progressive_miner = "Progressive Miners"
progressive_lich = "Progressive Liches"
progressive_spellthief = "Progressive Spellthieves"
# Static Class Definitions
knight = "Knights"
paladin = "Paladins"
mage = "Mages"
archmage = "Archmages"
barbarian = "Barbarians"
barbarian_king = "Barbarian Kings"
knave = "Knaves"
assassin = "Assassins"
shinobi = "Shinobis"
hokage = "Hokages"
miner = "Miners"
spelunker = "Spelunkers"
lich = "Lichs"
lich_king = "Lich Kings"
spellthief = "Spellthieves"
spellsword = "Spellswords"
dragon = "Dragons"
traitor = "Traitors"
# Skill Unlock Definitions
health = "Health Up"
mana = "Mana Up"
attack = "Attack Up"
magic_damage = "Magic Damage Up"
armor = "Armor Up"
equip = "Equip Up"
crit_chance = "Crit Chance Up"
crit_damage = "Crit Damage Up"
down_strike = "Down Strike Up"
gold_gain = "Gold Gain Up"
potion_efficiency = "Potion Efficiency Up"
invulnerability_time = "Invulnerability Time Up"
mana_cost_down = "Mana Cost Down"
death_defiance = "Death Defiance"
haggling = "Haggling"
random_children = "Randomize Children"
# Misc. Definitions
trip_stat_increase = "Triple Stat Increase"
gold_1000 = "1000 Gold"
gold_3000 = "3000 Gold"
gold_5000 = "5000 Gold"
# Blueprint Definitions
squire_blueprints = "Squire Armor Blueprints"
silver_blueprints = "Silver Armor Blueprints"
guardian_blueprints = "Guardian Armor Blueprints"
imperial_blueprints = "Imperial Armor Blueprints"
royal_blueprints = "Royal Armor Blueprints"
knight_blueprints = "Knight Armor Blueprints"
ranger_blueprints = "Ranger Armor Blueprints"
sky_blueprints = "Sky Armor Blueprints"
dragon_blueprints = "Dragon Armor Blueprints"
slayer_blueprints = "Slayer Armor Blueprints"
blood_blueprints = "Blood Armor Blueprints"
sage_blueprints = "Sage Armor Blueprints"
retribution_blueprints = "Retribution Armor Blueprints"
holy_blueprints = "Holy Armor Blueprints"
dark_blueprints = "Dark Armor Blueprints"
# Rune Definitions
vault_runes = "Vault Runes"
sprint_runes = "Sprint Runes"
vampire_runes = "Vampire Runes"
sky_runes = "Sky Runes"
siphon_runes = "Siphon Runes"
retaliation_runes = "Retaliation Runes"
bounty_runes = "Bounty Runes"
haste_runes = "Haste Runes"
curse_runes = "Curse Runes"
grace_runes = "Grace Runes"
balance_runes = "Balance Runes"
# Event Definitions
boss_khindr = "Defeat Khindr"
boss_alexander = "Defeat Alexander"
boss_leon = "Defeat Ponce de Leon"
boss_herodotus = "Defeat Herodotus"
boss_fountain = "Defeat The Fountain"

View File

@ -0,0 +1,52 @@
# Manor Piece Definitions
manor_ground_base = "Manor Renovation - Ground Road"
manor_main_base = "Manor Renovation - Main Base"
manor_main_bottom_window = "Manor Renovation - Main Bottom Window"
manor_main_top_window = "Manor Renovation - Main Top Window"
manor_main_roof = "Manor Renovation - Main Rooftop"
manor_left_wing_base = "Manor Renovation - Left Wing Base"
manor_left_wing_window = "Manor Renovation - Left Wing Window"
manor_left_wing_roof = "Manor Renovation - Left Wing Rooftop"
manor_left_big_base = "Manor Renovation - Left Big Base"
manor_left_big_upper1 = "Manor Renovation - Left Big Upper 1"
manor_left_big_upper2 = "Manor Renovation - Left Big Upper 2"
manor_left_big_windows = "Manor Renovation - Left Big Windows"
manor_left_big_roof = "Manor Renovation - Left Big Rooftop"
manor_left_far_base = "Manor Renovation - Left Far Base"
manor_left_far_roof = "Manor Renovation - Left Far Roof"
manor_left_extension = "Manor Renovation - Left Extension"
manor_left_tree1 = "Manor Renovation - Left Tree 1"
manor_left_tree2 = "Manor Renovation - Left Tree 2"
manor_right_wing_base = "Manor Renovation - Right Wing Base"
manor_right_wing_window = "Manor Renovation - Right Wing Window"
manor_right_wing_roof = "Manor Renovation - Right Wing Rooftop"
manor_right_big_base = "Manor Renovation - Right Big Base"
manor_right_big_upper = "Manor Renovation - Right Big Upper"
manor_right_big_roof = "Manor Renovation - Right Big Rooftop"
manor_right_high_base = "Manor Renovation - Right High Base"
manor_right_high_upper = "Manor Renovation - Right High Upper"
manor_right_high_tower = "Manor Renovation - Right High Tower"
manor_right_extension = "Manor Renovation - Right Extension"
manor_right_tree = "Manor Renovation - Right Tree"
manor_observatory_base = "Manor Renovation - Observatory Base"
manor_observatory_scope = "Manor Renovation - Observatory Telescope"
# Boss Chest Definitions
boss_khindr = "Khindr's Boss Chest"
boss_alexander = "Alexander's Boss Chest"
boss_leon = "Ponce de Leon's Boss Chest"
boss_herodotus = "Herodotus's Boss Chest"
# Special Room Definitions
special_jukebox = "Jukebox"
# Shorthand Definitions
diary = "Diary"
# Region Definitions
outside = "Outside Castle Hamson"
castle = "Castle Hamson"
garden = "Forest Abkhazia"
tower = "The Maya"
dungeon = "The Land of Darkness"
fountain = "Fountain Room"

View File

@ -0,0 +1,128 @@
import typing
from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle
class StartingGender(Choice):
"""
Determines the gender of your initial 'Sir Lee' character.
"""
displayname = "Starting Gender"
option_sir = 0
option_lady = 1
alias_male = 0
alias_female = 1
default = 0
class StartingClass(Choice):
"""
Determines the starting class of your initial 'Sir Lee' character.
"""
displayname = "Starting Class"
option_knight = 0
option_mage = 1
option_barbarian = 2
option_knave = 3
default = 0
class NewGamePlus(Choice):
"""
Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not
recommended for those inexperienced to Rogue Legacy!
"""
displayname = "New Game Plus"
option_normal = 0
option_new_game_plus = 1
option_new_game_plus_2 = 2
alias_hard = 1
alias_brutal = 2
default = 0
class FairyChestsPerZone(Range):
"""
Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat
bonuses can be found in Fairy Chests.
"""
displayname = "Fairy Chests Per Zone"
range_start = 5
range_end = 15
default = 5
class ChestsPerZone(Range):
"""
Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only
gold or stat bonuses can be found in Chests.
"""
displayname = "Chests Per Zone"
range_start = 15
range_end = 30
default = 15
class Vendors(Choice):
"""
Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked).
"""
displayname = "Vendors"
option_start_unlocked = 0
option_early = 1
option_normal = 2
option_anywhere = 3
default = 1
class DisableCharon(Toggle):
"""
Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool.
"""
displayname = "Disable Charon"
class RequirePurchasing(DefaultOnToggle):
"""
Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before
equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account.
"""
displayname = "Require Purchasing"
class GoldGainMultiplier(Choice):
"""
Adjusts the multiplier for gaining gold from all sources.
"""
displayname = "Gold Gain Multiplier"
option_normal = 0
option_quarter = 1
option_half = 2
option_double = 3
option_quadruple = 4
default = 0
class NumberOfChildren(Range):
"""
Determines the number of offspring you can choose from on the lineage screen after a death.
"""
displayname = "Number of Children"
range_start = 1
range_end = 5
default = 3
legacy_options: typing.Dict[str, type(Option)] = {
"starting_gender": StartingGender,
"starting_class": StartingClass,
"new_game_plus": NewGamePlus,
"fairy_chests_per_zone": FairyChestsPerZone,
"chests_per_zone": ChestsPerZone,
"vendors": Vendors,
"disable_charon": DisableCharon,
"require_purchasing": RequirePurchasing,
"gold_gain_multiplier": GoldGainMultiplier,
"number_of_children": NumberOfChildren,
"death_link": DeathLink,
}

View File

@ -0,0 +1,60 @@
import typing
from BaseClasses import MultiWorld, Region, Entrance
from .Items import LegacyItem
from .Locations import LegacyLocation, diary_location_table, location_table, base_location_table
from .Names import LocationName, ItemName
def create_regions(world, player: int):
locations: typing.List[str] = []
# Add required locations.
locations += [location for location in base_location_table]
locations += [location for location in diary_location_table]
# Add chests per settings.
fairies = int(world.fairy_chests_per_zone[player])
for i in range(0, fairies):
locations += [f"{LocationName.castle} - Fairy Chest {i + 1}"]
locations += [f"{LocationName.garden} - Fairy Chest {i + 1}"]
locations += [f"{LocationName.tower} - Fairy Chest {i + 1}"]
locations += [f"{LocationName.dungeon} - Fairy Chest {i + 1}"]
chests = int(world.chests_per_zone[player])
for i in range(0, chests):
locations += [f"{LocationName.castle} - Chest {i + 1}"]
locations += [f"{LocationName.garden} - Chest {i + 1}"]
locations += [f"{LocationName.tower} - Chest {i + 1}"]
locations += [f"{LocationName.dungeon} - Chest {i + 1}"]
# Set up the regions correctly.
world.regions += [
create_region(world, player, "Menu", None, [LocationName.outside]),
create_region(world, player, LocationName.castle, locations),
]
# Connect entrances and set up events.
world.get_entrance(LocationName.outside, player).connect(world.get_region(LocationName.castle, player))
world.get_location(LocationName.castle, player).place_locked_item(LegacyItem(ItemName.boss_khindr, True, None, player))
world.get_location(LocationName.garden, player).place_locked_item(LegacyItem(ItemName.boss_alexander, True, None, player))
world.get_location(LocationName.tower, player).place_locked_item(LegacyItem(ItemName.boss_leon, True, None, player))
world.get_location(LocationName.dungeon, player).place_locked_item(LegacyItem(ItemName.boss_herodotus, True, None, player))
world.get_location(LocationName.fountain, player).place_locked_item(LegacyItem(ItemName.boss_fountain, True, None, player))
def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None):
# Shamelessly stolen from the ROR2 definition, lol
ret = Region(name, None, name, player)
ret.world = world
if locations:
for location in locations:
loc_id = location_table.get(location, 0)
location = LegacyLocation(player, location, loc_id, ret)
ret.locations.append(location)
if exits:
for exit in exits:
ret.exits.append(Entrance(player, exit, ret))
return ret

View File

@ -0,0 +1,131 @@
from BaseClasses import MultiWorld
from .Names import LocationName, ItemName
from ..AutoWorld import LogicMixin
from ..generic.Rules import set_rule
class LegacyLogic(LogicMixin):
def _legacy_has_any_vendors(self, player: int) -> bool:
return self.has_any({ItemName.blacksmith, ItemName.enchantress}, player)
def _legacy_has_all_vendors(self, player: int) -> bool:
return self.has_all({ItemName.blacksmith, ItemName.enchantress}, player)
def _legacy_has_stat_upgrades(self, player: int, amount: int) -> bool:
count: int = self.item_count(ItemName.health, player) + self.item_count(ItemName.mana, player) + \
self.item_count(ItemName.attack, player) + self.item_count(ItemName.magic_damage, player) + \
self.item_count(ItemName.armor, player) + self.item_count(ItemName.equip, player)
return count >= amount
def set_rules(world: MultiWorld, player: int):
# Chests
for i in range(0, world.chests_per_zone[player]):
set_rule(world.get_location(f"{LocationName.garden} - Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(f"{LocationName.tower} - Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(f"{LocationName.dungeon} - Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_leon, player))
# Fairy Chests
for i in range(0, world.fairy_chests_per_zone[player]):
set_rule(world.get_location(f"{LocationName.garden} - Fairy Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(f"{LocationName.tower} - Fairy Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(f"{LocationName.dungeon} - Fairy Chest {i + 1}", player),
lambda state: state.has(ItemName.boss_leon, player))
# Vendors
if world.vendors[player] == "early":
set_rule(world.get_location(LocationName.castle, player),
lambda state: state._legacy_has_all_vendors(player))
elif world.vendors[player] == "normal":
set_rule(world.get_location(LocationName.garden, player),
lambda state: state._legacy_has_any_vendors(player))
elif world.vendors[player] == "anywhere":
pass # it can be anywhere, so no rule for this!
# Diaries
for i in range(0, 5):
set_rule(world.get_location(f"Diary {i + 6}", player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(f"Diary {i + 11}", player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(f"Diary {i + 16}", player),
lambda state: state.has(ItemName.boss_leon, player))
set_rule(world.get_location(f"Diary {i + 21}", player),
lambda state: state.has(ItemName.boss_herodotus, player))
# Scale each manor location.
set_rule(world.get_location(LocationName.manor_left_wing_window, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_left_wing_roof, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_right_wing_window, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_right_wing_roof, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_left_big_base, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_right_big_base, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_left_tree1, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_left_tree2, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_right_tree, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.manor_left_big_upper1, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_left_big_upper2, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_left_big_windows, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_left_big_roof, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_left_far_base, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_left_far_roof, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_left_extension, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_right_big_upper, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_right_big_roof, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_right_extension, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.manor_right_high_base, player),
lambda state: state.has(ItemName.boss_leon, player))
set_rule(world.get_location(LocationName.manor_right_high_upper, player),
lambda state: state.has(ItemName.boss_leon, player))
set_rule(world.get_location(LocationName.manor_right_high_tower, player),
lambda state: state.has(ItemName.boss_leon, player))
set_rule(world.get_location(LocationName.manor_observatory_base, player),
lambda state: state.has(ItemName.boss_leon, player))
set_rule(world.get_location(LocationName.manor_observatory_scope, player),
lambda state: state.has(ItemName.boss_leon, player))
# Standard Zone Progression
set_rule(world.get_location(LocationName.garden, player),
lambda state: state._legacy_has_stat_upgrades(player, 10) and state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.tower, player),
lambda state: state._legacy_has_stat_upgrades(player, 25) and state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.dungeon, player),
lambda state: state._legacy_has_stat_upgrades(player, 40) and state.has(ItemName.boss_leon, player))
# Bosses
set_rule(world.get_location(LocationName.boss_khindr, player),
lambda state: state.has(ItemName.boss_khindr, player))
set_rule(world.get_location(LocationName.boss_alexander, player),
lambda state: state.has(ItemName.boss_alexander, player))
set_rule(world.get_location(LocationName.boss_leon, player),
lambda state: state.has(ItemName.boss_leon, player))
set_rule(world.get_location(LocationName.boss_herodotus, player),
lambda state: state.has(ItemName.boss_herodotus, player))
set_rule(world.get_location(LocationName.fountain, player),
lambda state: state._legacy_has_stat_upgrades(player, 50) and state.has(ItemName.boss_herodotus, player))
world.completion_condition[player] = lambda state: state.has(ItemName.boss_fountain, player)

View File

@ -0,0 +1,105 @@
import typing
from BaseClasses import Item, MultiWorld
from .Items import LegacyItem, ItemData, item_table, vendors_table, static_classes_table, progressive_classes_table, \
skill_unlocks_table, blueprints_table, runes_table, misc_items_table
from .Locations import LegacyLocation, location_table, base_location_table
from .Options import legacy_options
from .Regions import create_regions
from .Rules import set_rules
from .Names import ItemName
from ..AutoWorld import World
class LegacyWorld(World):
"""
Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed
you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf.
But that's OK, because no one is perfect, and you don't have to be to succeed.
"""
game: str = "Rogue Legacy"
options = legacy_options
topology_present = False
data_version = 1
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = location_table
def _get_slot_data(self):
return {
"starting_gender": self.world.starting_gender[self.player],
"starting_class": self.world.starting_class[self.player],
"new_game_plus": self.world.new_game_plus[self.player],
"fairy_chests_per_zone": self.world.fairy_chests_per_zone[self.player],
"chests_per_zone": self.world.chests_per_zone[self.player],
"vendors": self.world.vendors[self.player],
"disable_charon": self.world.disable_charon[self.player],
"require_purchasing": self.world.require_purchasing[self.player],
"gold_gain_multiplier": self.world.gold_gain_multiplier[self.player],
"number_of_children": self.world.number_of_children[self.player],
"death_link": self.world.death_link[self.player],
}
def _create_items(self, name: str):
data = item_table[name]
return [self.create_item(name)] * data.quantity
def fill_slot_data(self) -> dict:
slot_data = self._get_slot_data()
for option_name in legacy_options:
option = getattr(self.world, option_name)[self.player]
slot_data[option_name] = option.value
return slot_data
def generate_basic(self):
itempool: typing.List[LegacyItem] = []
total_required_locations = 61 + (self.world.chests_per_zone[self.player] * 4) + (self.world.fairy_chests_per_zone[self.player] * 4)
# Fill item pool with all required items
for item in {**skill_unlocks_table, **blueprints_table, **runes_table}:
# if Haggling, do not add if Disable Charon.
if item == ItemName.haggling and self.world.disable_charon[self.player] == 1:
continue
itempool += self._create_items(item)
# Add specific classes into the pool. Eventually, will be able to shuffle the starting ones, but until then...
itempool += [
self.create_item(ItemName.paladin),
self.create_item(ItemName.archmage),
self.create_item(ItemName.barbarian_king),
self.create_item(ItemName.assassin),
self.create_item(ItemName.dragon),
self.create_item(ItemName.traitor),
*self._create_items(ItemName.progressive_shinobi),
*self._create_items(ItemName.progressive_miner),
*self._create_items(ItemName.progressive_lich),
*self._create_items(ItemName.progressive_spellthief),
]
# Check if we need to start with these vendors or put them in the pool.
if self.world.vendors[self.player] == "start_unlocked":
self.world.push_precollected(self.world.create_item(ItemName.blacksmith, self.player))
self.world.push_precollected(self.world.create_item(ItemName.enchantress, self.player))
else:
itempool += [self.create_item(ItemName.blacksmith), self.create_item(ItemName.enchantress)]
# Add Arcitect.
itempool += [self.create_item(ItemName.architect)]
# Fill item pool with the remaining
for _ in range(len(itempool), total_required_locations):
item = self.world.random.choice(list(misc_items_table.keys()))
itempool += [self.create_item(item)]
self.world.itempool += itempool
def create_regions(self):
create_regions(self.world, self.player)
def create_item(self, name: str) -> Item:
data = item_table[name]
return LegacyItem(name, data.progression, data.code, self.player)
def set_rules(self):
set_rules(self.world, self.player)

View File

@ -98,7 +98,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData('Emperors tower', 'Dad\'s courtyard chest', 1337079, lambda state: state._timespinner_has_upwarddash(world, player)),
LocationData('Emperors tower', 'Galactic sage room', 1337080),
LocationData('Emperors tower', 'Bottom of Dad\'s right tower', 1337081),
LocationData('Emperors tower', 'Wayyyy up there', 1337082),
LocationData('Emperors tower', 'Wayyyy up there', 1337082, lambda state: state._timespinner_has_doublejump_of_npc(world, player)),
LocationData('Emperors tower', 'Dad\'s left tower balcony', 1337083),
LocationData('Emperors tower', 'Dad\'s Chambers chest', 1337084),
LocationData('Emperors tower', 'Dad\'s Chambers pedestal', 1337085),
@ -217,7 +217,34 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
LocationData('Left Side forest Caves', 'Cantoran', 1337176),
)
# 1337177 - 1337236 Reserved for future use
# 1337177 - 1337198 Lore Checks
if not world or is_option_enabled(world, player, "LoreChecks"):
location_table += (
LocationData('Lower lake desolation', 'Memory - Coyote Jump (Time Messenger)', 1337177),
LocationData('Library', 'Memory - Waterway (A Message)', 1337178),
LocationData('Library top', 'Memory - Library Gap (Lachiemi Sun)', 1337179),
LocationData('Library top', 'Memory - Mr. Hat Portrait (Moonlit Night)', 1337180),
LocationData('Varndagroth tower left', 'Memory - Left Elevator (Nomads)', 1337181, lambda state: state.has('Elevator Keycard', player)),
LocationData('Varndagroth tower right (lower)', 'Memory - Siren Elevator (Childhood)', 1337182, lambda state: state._timespinner_has_keycard_B(world, player)),
LocationData('Varndagroth tower right (lower)', 'Memory - Varndagroth Right Bottom (Faron)', 1337183),
LocationData('Military Fortress', 'Memory - Bomber Climb (A Solution)', 1337184, lambda state: state.has('Timespinner Wheel', player) and state._timespinner_has_doublejump_of_npc(world, player)),
LocationData('The lab', 'Memory - Genza\'s Secret Stash 1 (An Old Friend)', 1337185, lambda state: state._timespinner_can_break_walls(world, player)),
LocationData('The lab', 'Memory - Genza\'s Secret Stash 2 (Twilight Dinner)', 1337186, lambda state: state._timespinner_can_break_walls(world, player)),
LocationData('Emperors tower', 'Memory - Way Up There (Final Circle)', 1337187, lambda state: state._timespinner_has_doublejump_of_npc(world, player)),
LocationData('Forest', 'Journal - Forest Rats (Lachiem Expedition)', 1337188),
LocationData('Forest', 'Journal - Forest Bat Jump Ledge (Peace Treaty)', 1337189, lambda state: state._timespinner_has_doublejump_of_npc(world, player) or state._timespinner_has_forwarddash_doublejump(world, player) or state._timespinner_has_fastjump_on_npc(world, player)),
LocationData('Castle Ramparts', 'Journal - Floating in Moat (Prime Edicts)', 1337190),
LocationData('Castle Ramparts', 'Journal - Archer + Knight (Declaration of Independence)', 1337191),
LocationData('Castle Keep', 'Journal - Under the Twins (Letter of Reference)', 1337192),
LocationData('Castle Keep', 'Journal - Castle Loop Giantess (Political Advice)', 1337193),
LocationData('Royal towers (lower)', 'Journal - Aleana\'s Room (Diplomatic Missive)', 1337194, lambda state: state._timespinner_has_pink(world, player)),
LocationData('Royal towers (upper)', 'Journal - Top Struggle Juggle Base (War of the Sisters)', 1337195),
LocationData('Royal towers (upper)', 'Journal - Aleana Boss (Stained Letter)', 1337196),
LocationData('Royal towers', 'Journal - Near Bottom Struggle Juggle (Mission Findings)', 1337197, lambda state: state._timespinner_has_doublejump_of_npc(world, player)),
LocationData('Caves of Banishment (Maw)', 'Journal - Lower Left Maw Caves (Naivety)', 1337198)
)
# 1337199 - 1337236 Reserved for future use
# 1337237 - 1337245 GyreArchives
if not world or is_option_enabled(world, player, "GyreArchives"):

View File

@ -50,6 +50,10 @@ class Cantoran(Toggle):
"Cantoran's fight and check are available upon revisiting his room"
display_name = "Cantoran"
class LoreChecks(Toggle):
"Memories and journal entries contain items."
display_name = "Lore Checks"
class DamageRando(Toggle):
"Each orb has a high chance of having lower base damage and a low chance of having much higher base damage."
display_name = "Damage Rando"
@ -68,6 +72,7 @@ timespinner_options: Dict[str, Toggle] = {
#"StinkyMaw": StinkyMaw,
"GyreArchives": GyreArchives,
"Cantoran": Cantoran,
"LoreChecks": LoreChecks,
"DamageRando": DamageRando,
"DeathLink": DeathLink,
}

View File

@ -18,7 +18,7 @@ class TimespinnerWorld(World):
game = "Timespinner"
topology_present = True
remote_items = False
data_version = 5
data_version = 6
item_name_to_id = {name: data.code for name, data in item_table.items()}
location_name_to_id = {location.name: location.code for location in get_locations(None, None)}