Merge branch 'main' into docs_consolidation
# Conflicts: # WebHostLib/static/assets/tutorial/timespinner/setup_en.md
This commit is contained in:
commit
a722ec1c37
|
@ -155,4 +155,7 @@ Archipelago.zip
|
|||
|
||||
#minecraft server stuff
|
||||
jdk*/
|
||||
minecraft*/
|
||||
minecraft*/
|
||||
|
||||
#pyenv
|
||||
.python-version
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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
|
||||
|
|
11
SNIClient.py
11
SNIClient.py
|
@ -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}")
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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=(',', ': '))
|
||||
|
|
|
@ -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!
|
|
@ -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!**
|
|
@ -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
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 %}
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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}
|
|
@ -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()}
|
|
@ -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"
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"):
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)}
|
||||
|
|
Loading…
Reference in New Issue