From 06dc76a78b5d36051bdc1bde4378062f9253e0a6 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Fri, 31 Dec 2021 14:42:04 -0500 Subject: [PATCH 01/24] Added locations to generated weighted-settings.json. In-progress /weighted-settings page available on WebHost, currently non-functional as I work on JS backend stuff --- WebHostLib/__init__.py | 5 + WebHostLib/options.py | 1 + WebHostLib/static/assets/weighted-settings.js | 228 ++++++++++++++++++ .../static/styles/weighted-settings.css | 149 ++++++++++++ WebHostLib/templates/weighted-settings.html | 44 ++++ 5 files changed, 427 insertions(+) create mode 100644 WebHostLib/static/assets/weighted-settings.js create mode 100644 WebHostLib/static/styles/weighted-settings.css create mode 100644 WebHostLib/templates/weighted-settings.html diff --git a/WebHostLib/__init__.py b/WebHostLib/__init__.py index 235b4f52..085cfe56 100644 --- a/WebHostLib/__init__.py +++ b/WebHostLib/__init__.py @@ -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//player-settings') def player_settings(game): diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 8411e138..9ade10e8 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -105,6 +105,7 @@ def create(): 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()) + weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_name_to_id.keys()) with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f: f.write(json.dumps(weighted_settings, indent=2, separators=(',', ': '))) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js new file mode 100644 index 00000000..21902eca --- /dev/null +++ b/WebHostLib/static/assets/weighted-settings.js @@ -0,0 +1,228 @@ +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); + 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.addEventListener('keyup', (event) => updateBaseSetting(event)); + 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 options + for (let gameOption of Object.keys(settingData.games[game].gameOptions)){ + newSettings[game][gameOption] = settingData.games[game].gameOptions[gameOption].defaultValue; + } + + 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: Update this function for use with weighted-settings +// TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints +// TODO: Include location configs: exclude_locations +const buildUI = (settingData) => { + // Game Options + const leftGameOpts = {}; + const rightGameOpts = {}; + Object.keys(settingData.gameOptions).forEach((key, index) => { + if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; } + else { rightGameOpts[key] = settingData.gameOptions[key]; } + }); + document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts)); + document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts)); +}; + +const buildOptionsTable = (settings, romOpts = false) => { + const currentSettings = JSON.parse(localStorage.getItem(gameName)); + const table = document.createElement('table'); + const tbody = document.createElement('tbody'); + + Object.keys(settings).forEach((setting) => { + const tr = document.createElement('tr'); + + // td Left + const tdl = document.createElement('td'); + const label = document.createElement('label'); + label.setAttribute('for', setting); + label.setAttribute('data-tooltip', settings[setting].description); + label.innerText = `${settings[setting].displayName}:`; + tdl.appendChild(label); + tr.appendChild(tdl); + + // td Right + const tdr = document.createElement('td'); + let element = null; + + switch(settings[setting].type){ + case 'select': + element = document.createElement('div'); + element.classList.add('select-container'); + let select = document.createElement('select'); + select.setAttribute('id', setting); + select.setAttribute('data-key', setting); + if (romOpts) { select.setAttribute('data-romOpt', '1'); } + settings[setting].options.forEach((opt) => { + const option = document.createElement('option'); + option.setAttribute('value', opt.value); + option.innerText = opt.name; + if ((isNaN(currentSettings[gameName][setting]) && + (parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) || + (opt.value === currentSettings[gameName][setting])) + { + option.selected = true; + } + select.appendChild(option); + }); + select.addEventListener('change', (event) => updateGameSetting(event)); + element.appendChild(select); + break; + + case 'range': + element = document.createElement('div'); + element.classList.add('range-container'); + + let range = document.createElement('input'); + range.setAttribute('type', 'range'); + range.setAttribute('data-key', setting); + range.setAttribute('min', settings[setting].min); + range.setAttribute('max', settings[setting].max); + range.value = currentSettings[gameName][setting]; + range.addEventListener('change', (event) => { + document.getElementById(`${setting}-value`).innerText = event.target.value; + updateGameSetting(event); + }); + element.appendChild(range); + + let rangeVal = document.createElement('span'); + rangeVal.classList.add('range-value'); + rangeVal.setAttribute('id', `${setting}-value`); + rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue; + element.appendChild(rangeVal); + break; + + default: + console.error(`Unknown setting type: ${settings[setting].type}`); + console.error(setting); + return; + } + + tdr.appendChild(element); + tr.appendChild(tdr); + tbody.appendChild(tr); + }); + + table.appendChild(tbody); + return table; +}; + +const updateBaseSetting = (event) => { + const options = JSON.parse(localStorage.getItem(gameName)); + options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ? + event.target.value : parseInt(event.target.value); + localStorage.setItem(gameName, JSON.stringify(options)); +}; + +const updateGameSetting = (event) => { + const options = JSON.parse(localStorage.getItem(gameName)); + options[gameName][event.target.getAttribute('data-key')] = isNaN(event.target.value) ? + event.target.value : parseInt(event.target.value, 10); + localStorage.setItem(gameName, JSON.stringify(options)); +}; + +const exportSettings = () => { + const settings = JSON.parse(localStorage.getItem(gameName)); + if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; } + 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(gameName) }, + presetData: { player: localStorage.getItem(gameName) }, + 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); + }); +}; diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css new file mode 100644 index 00000000..3a56604b --- /dev/null +++ b/WebHostLib/static/styles/weighted-settings.css @@ -0,0 +1,149 @@ +html{ + background-image: url('../static/backgrounds/grass/grass-0007-large.png'); + background-repeat: repeat; + background-size: 650px 650px; +} + +#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 #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; +} + +#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: row; +} + +#weighted-settings .left, #weighted-settings .right{ + flex-grow: 1; +} + +#weighted-settings table .select-container{ + display: flex; + flex-direction: row; +} + +#weighted-settings table .select-container select{ + min-width: 200px; + flex-grow: 1; +} + +#weighted-settings table .range-container{ + display: flex; + flex-direction: row; +} + +#weighted-settings table .range-container input[type=range]{ + flex-grow: 1; +} + +#weighted-settings table .range-value{ + min-width: 20px; + margin-left: 0.25rem; +} + +#weighted-settings table label{ + display: block; + min-width: 200px; + margin-right: 4px; + cursor: default; +} + +@media all and (max-width: 1000px), all and (orientation: portrait){ + #weighted-settings .game-options{ + justify-content: flex-start; + flex-wrap: wrap; + } + + #weighted-settings .left, #weighted-settings .right{ + flex-grow: unset; + } + + #game-options table label{ + display: block; + min-width: 200px; + } +} diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html new file mode 100644 index 00000000..09cd4771 --- /dev/null +++ b/WebHostLib/templates/weighted-settings.html @@ -0,0 +1,44 @@ +{% extends 'pageWrapper.html' %} + +{% block head %} + {{ game }} Settings + + + + + +{% endblock %} + +{% block body %} + {% include 'header/grassHeader.html' %} +
+
+

Weighted Settings

+

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.

+ +

A list of all games you have generated can be found here.

+ +


+ +

+ +
+ +
+ + +

(Game Name) Options

+
+
+
+
+ +
+ + + +
+
+{% endblock %} From 6b852d6e1a68dab2d757e14689916193e7086f68 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jan 2022 03:12:32 +0100 Subject: [PATCH 02/24] WebHost Options: hidden games should remain functional, just hidden. --- WebHostLib/options.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 9ade10e8..3b742068 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -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( @@ -101,11 +99,12 @@ def create(): with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f: f.write(json.dumps(player_settings, 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()) - weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_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]["gameOptions"] = 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=(',', ': '))) From 93ac01840040341f639df22aa0e5485819685f1f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jan 2022 15:46:08 +0100 Subject: [PATCH 03/24] SNIClient: make SNI finder a bit smarter --- SNIClient.py | 11 +++++++---- WebHostLib/options.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index f6509938..6215ffad 100644 --- a/SNIClient.py +++ b/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}") diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 3b742068..a2041339 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -97,7 +97,7 @@ 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=(',', ': ')) if not world.hidden: weighted_settings["baseOptions"]["game"][game_name] = 0 @@ -107,4 +107,4 @@ def create(): 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=(',', ': ')) From f8893a7ed3026701f6f1a03d3ccfd3b72c32c8d7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 1 Jan 2022 17:18:48 +0100 Subject: [PATCH 04/24] WebHost: check uploads against zip magic number instead of .zip --- MultiServer.py | 4 ++-- WebHostLib/customserver.py | 2 +- WebHostLib/tracker.py | 2 +- WebHostLib/upload.py | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index f1fbce85..f92077ad 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -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.") diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index cfc5de81..a91ee51e 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -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): diff --git a/WebHostLib/tracker.py b/WebHostLib/tracker.py index 228bf375..d530f9e0 100644 --- a/WebHostLib/tracker.py +++ b/WebHostLib/tracker.py @@ -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"] diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 7095d7d0..cdd7e315 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -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 From a5d2046a871fb58a5f74bfd2deab7daa1210d532 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sat, 1 Jan 2022 20:29:38 +0100 Subject: [PATCH 05/24] [Docs] More Links (#179) * [Docs] More Links * [Docs] Moved link for data package object --- docs/network protocol.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 355bfdfd..7919a34e 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -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 From 411f0e40b61310cae9719f69f1e75309422a1646 Mon Sep 17 00:00:00 2001 From: Colin Lenzen <32756996+TriumphantBass@users.noreply.github.com> Date: Sat, 1 Jan 2022 13:44:45 -0600 Subject: [PATCH 06/24] Timespinner - Add Lore Checks checks (#171) --- worlds/timespinner/Locations.py | 29 ++++++++++++++++++++++++++++- worlds/timespinner/Options.py | 5 +++++ worlds/timespinner/__init__.py | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 74cf4c42..a1f474de 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -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), + 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), + 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"): diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index da3cff08..9bdc689e 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -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, } diff --git a/worlds/timespinner/__init__.py b/worlds/timespinner/__init__.py index cba17c95..0e9d7a36 100644 --- a/worlds/timespinner/__init__.py +++ b/worlds/timespinner/__init__.py @@ -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)} From 0431c3fce00bd56033f6b5966b631367edccf8b0 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sat, 1 Jan 2022 16:59:58 -0500 Subject: [PATCH 07/24] Much more work on weighted-setting page. Still needs support for range options and item/location settings. --- WebHostLib/options.py | 2 +- WebHostLib/static/assets/weighted-settings.js | 304 ++++++++++++------ .../static/styles/weighted-settings.css | 70 ++-- WebHostLib/templates/weighted-settings.html | 8 +- 4 files changed, 244 insertions(+), 140 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 9ade10e8..a1c7b0df 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -103,7 +103,7 @@ def create(): 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]["gameSettings"] = game_options weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_name_to_id.keys()) weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_name_to_id.keys()) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 21902eca..26744c76 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -18,7 +18,8 @@ window.addEventListener('load', () => { // Page setup createDefaultSettings(results); - // buildUI(results); + buildUI(results); + updateVisibleGames(); adjustHeaderWidth(); // Event listeners @@ -29,7 +30,9 @@ window.addEventListener('load', () => { // Name input field const weightedSettings = JSON.parse(localStorage.getItem('weighted-settings')); const nameInput = document.getElementById('player-name'); - nameInput.addEventListener('keyup', (event) => updateBaseSetting(event)); + nameInput.setAttribute('data-type', 'data'); + nameInput.setAttribute('data-setting', 'name'); + nameInput.addEventListener('keyup', updateBaseSetting); nameInput.value = weightedSettings.name; }); }); @@ -61,9 +64,27 @@ const createDefaultSettings = (settingData) => { // Initialize game object newSettings[game] = {}; - // Transfer game options - for (let gameOption of Object.keys(settingData.games[game].gameOptions)){ - newSettings[game][gameOption] = settingData.games[game].gameOptions[gameOption].defaultValue; + // 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; + } + break; + default: + console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`); + } } newSettings[game].start_inventory = []; @@ -77,121 +98,212 @@ const createDefaultSettings = (settingData) => { } }; -// TODO: Update this function for use with weighted-settings // TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints // TODO: Include location configs: exclude_locations const buildUI = (settingData) => { - // Game Options - const leftGameOpts = {}; - const rightGameOpts = {}; - Object.keys(settingData.gameOptions).forEach((key, index) => { - if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; } - else { rightGameOpts[key] = settingData.gameOptions[key]; } + // 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); + + gameDiv.appendChild(buildOptionsDiv(game, settingData.games[game].gameSettings)); + gamesWrapper.appendChild(gameDiv); }); - document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts)); - document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts)); }; -const buildOptionsTable = (settings, romOpts = false) => { - const currentSettings = JSON.parse(localStorage.getItem(gameName)); +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); + + // Build the game choice table const table = document.createElement('table'); const tbody = document.createElement('tbody'); - Object.keys(settings).forEach((setting) => { + Object.keys(games).forEach((game) => { const tr = document.createElement('tr'); + const tdLeft = document.createElement('td'); + tdLeft.classList.add('td-left'); + tdLeft.innerText = game; + tr.appendChild(tdLeft); - // td Left - const tdl = document.createElement('td'); - const label = document.createElement('label'); - label.setAttribute('for', setting); - label.setAttribute('data-tooltip', settings[setting].description); - label.innerText = `${settings[setting].displayName}:`; - tdl.appendChild(label); - tr.appendChild(tdl); + 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); - // td Right - const tdr = document.createElement('td'); - let element = null; - - switch(settings[setting].type){ - case 'select': - element = document.createElement('div'); - element.classList.add('select-container'); - let select = document.createElement('select'); - select.setAttribute('id', setting); - select.setAttribute('data-key', setting); - if (romOpts) { select.setAttribute('data-romOpt', '1'); } - settings[setting].options.forEach((opt) => { - const option = document.createElement('option'); - option.setAttribute('value', opt.value); - option.innerText = opt.name; - if ((isNaN(currentSettings[gameName][setting]) && - (parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) || - (opt.value === currentSettings[gameName][setting])) - { - option.selected = true; - } - select.appendChild(option); - }); - select.addEventListener('change', (event) => updateGameSetting(event)); - element.appendChild(select); - break; - - case 'range': - element = document.createElement('div'); - element.classList.add('range-container'); - - let range = document.createElement('input'); - range.setAttribute('type', 'range'); - range.setAttribute('data-key', setting); - range.setAttribute('min', settings[setting].min); - range.setAttribute('max', settings[setting].max); - range.value = currentSettings[gameName][setting]; - range.addEventListener('change', (event) => { - document.getElementById(`${setting}-value`).innerText = event.target.value; - updateGameSetting(event); - }); - element.appendChild(range); - - let rangeVal = document.createElement('span'); - rangeVal.classList.add('range-value'); - rangeVal.setAttribute('id', `${setting}-value`); - rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue; - element.appendChild(rangeVal); - break; - - default: - console.error(`Unknown setting type: ${settings[setting].type}`); - console.error(setting); - return; - } - - tdr.appendChild(element); - tr.appendChild(tdr); + 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); - return table; + 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'); + + switch(setting.type){ + case 'select': + 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); + + 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); + optionsWrapper.appendChild(settingWrapper); + break; + + case 'range': + // TODO: Include range settings + break; + + default: + console.error(`Unknown setting type for ${game} setting ${setting}: ${settings[setting].type}`); + return; + } + }); + + return optionsWrapper; +}; + +const updateVisibleGames = () => { + const settings = JSON.parse(localStorage.getItem('weighted-settings')); + Object.keys(settings.game).forEach((game) => { + const gameDiv = document.getElementById(`${game}-div`); + (parseInt(settings.game[game], 10) > 0) ? + gameDiv.classList.remove('invisible') : + gameDiv.classList.add('invisible') + }); }; const updateBaseSetting = (event) => { - const options = JSON.parse(localStorage.getItem(gameName)); - options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ? - event.target.value : parseInt(event.target.value); - localStorage.setItem(gameName, JSON.stringify(options)); + 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(gameName)); - options[gameName][event.target.getAttribute('data-key')] = isNaN(event.target.value) ? + 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'); + switch (type){ + case 'select': + console.log(`${game}-${setting}-${option}`); + document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value; + break; + case 'range': + break; + } + options[game][setting][option] = isNaN(event.target.value) ? event.target.value : parseInt(event.target.value, 10); - localStorage.setItem(gameName, JSON.stringify(options)); + localStorage.setItem('weighted-settings', JSON.stringify(options)); }; const exportSettings = () => { - const settings = JSON.parse(localStorage.getItem(gameName)); - if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; } + 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; + } const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`); download(`${document.getElementById('player-name').value}.yaml`, yamlText); }; @@ -209,8 +321,8 @@ const download = (filename, text) => { const generateGame = (raceMode = false) => { axios.post('/api/generate', { - weights: { player: localStorage.getItem(gameName) }, - presetData: { player: localStorage.getItem(gameName) }, + weights: { player: localStorage.getItem('weighted-settings') }, + presetData: { player: localStorage.getItem('weighted-settings') }, playerCount: 1, race: raceMode ? '1' : '0', }).then((response) => { diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 3a56604b..70a7b34b 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -14,6 +14,35 @@ html{ color: #eeffeb; } +#weighted-settings #games-wrapper{ + width: 100%; +} + +#weighted-settings .setting-wrapper{ + width: 100%; + margin-bottom: 2rem; +} + +#weighted-settings .setting-description{ + font-weight: bold; + margin: 0 0 1rem; +} + +#weighted-settings table{ + width: 100%; +} + +#weighted-settings table .td-left{ + padding-right: 1rem; +} + +#weighted-settings table .td-middle{ + display: flex; + flex-direction: column; + justify-content: space-evenly; + padding-right: 1rem; +} + #weighted-settings #weighted-settings-button-row{ display: flex; flex-direction: row; @@ -94,42 +123,11 @@ html{ #weighted-settings .game-options, #weighted-settings .rom-options{ display: flex; - flex-direction: row; + flex-direction: column; } -#weighted-settings .left, #weighted-settings .right{ - flex-grow: 1; -} - -#weighted-settings table .select-container{ - display: flex; - flex-direction: row; -} - -#weighted-settings table .select-container select{ - min-width: 200px; - flex-grow: 1; -} - -#weighted-settings table .range-container{ - display: flex; - flex-direction: row; -} - -#weighted-settings table .range-container input[type=range]{ - flex-grow: 1; -} - -#weighted-settings table .range-value{ - min-width: 20px; - margin-left: 0.25rem; -} - -#weighted-settings table label{ - display: block; - min-width: 200px; - margin-right: 4px; - cursor: default; +#weighted-settings .invisible{ + display: none; } @media all and (max-width: 1000px), all and (orientation: portrait){ @@ -138,10 +136,6 @@ html{ flex-wrap: wrap; } - #weighted-settings .left, #weighted-settings .right{ - flex-grow: unset; - } - #game-options table label{ display: block; min-width: 200px; diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html index 09cd4771..353d82fb 100644 --- a/WebHostLib/templates/weighted-settings.html +++ b/WebHostLib/templates/weighted-settings.html @@ -24,15 +24,13 @@

-
+
-

(Game Name) Options

-
-
-
+
+
From d98d69336965bc43c3230e40c97ed11551cf45ee Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sat, 1 Jan 2022 17:05:08 -0500 Subject: [PATCH 08/24] Remove debug logging --- WebHostLib/static/assets/weighted-settings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 26744c76..833069fa 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -284,7 +284,6 @@ const updateGameSetting = (event) => { const type = event.target.getAttribute('data-type'); switch (type){ case 'select': - console.log(`${game}-${setting}-${option}`); document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value; break; case 'range': From 7622f7f28fce67cf82b86a3c9c647daaf604e8d8 Mon Sep 17 00:00:00 2001 From: Ross Bemrose Date: Sun, 2 Jan 2022 10:33:29 -0500 Subject: [PATCH 09/24] Timespinner: Fix missing double-jump checks for LoreChecks locations (#181) --- worlds/timespinner/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index a1f474de..83463841 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -230,7 +230,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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), + 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), From 51fa00399df84daabe6caedcbb19b65d721ea977 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 2 Jan 2022 16:41:26 +0100 Subject: [PATCH 10/24] [Timespinner] Fixed logic for original wayyy up there location --- worlds/timespinner/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 83463841..955b06d3 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -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), From 08a08711686f16346dfd461fd0e81de06244b997 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sun, 2 Jan 2022 16:31:49 -0500 Subject: [PATCH 11/24] Add game-jumping and hint text css to weighted-settings --- WebHostLib/static/assets/weighted-settings.js | 64 ++++++++++++------- .../static/styles/weighted-settings.css | 14 +++- 2 files changed, 55 insertions(+), 23 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index 833069fa..f00d6230 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -130,9 +130,15 @@ const buildGameChoice = (games) => { const gameSelectDescription = document.createElement('p'); gameSelectDescription.classList.add('setting-description'); - gameSelectDescription.innerText = 'Choose which games you might be required to play' + 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'); @@ -141,7 +147,10 @@ const buildGameChoice = (games) => { const tr = document.createElement('tr'); const tdLeft = document.createElement('td'); tdLeft.classList.add('td-left'); - tdLeft.innerText = game; + 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'); @@ -183,17 +192,17 @@ const buildOptionsDiv = (game, settings) => { 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 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); - const optionTable = document.createElement('table'); const tbody = document.createElement('tbody'); @@ -235,7 +244,10 @@ const buildOptionsDiv = (game, settings) => { break; case 'range': - // TODO: Include range settings + const settingDescription = document.createElement('p'); + settingDescription.classList.add('setting-description'); + settingDescription.innerText = setting.description.replace(/(\n)/g, ' '); + settingWrapper.appendChild(settingDescription); break; default: @@ -251,9 +263,23 @@ const updateVisibleGames = () => { const settings = JSON.parse(localStorage.getItem('weighted-settings')); Object.keys(settings.game).forEach((game) => { const gameDiv = document.getElementById(`${game}-div`); - (parseInt(settings.game[game], 10) > 0) ? - gameDiv.classList.remove('invisible') : - gameDiv.classList.add('invisible') + 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'); + + } }); }; @@ -282,13 +308,7 @@ const updateGameSetting = (event) => { const setting = event.target.getAttribute('data-setting'); const option = event.target.getAttribute('data-option'); const type = event.target.getAttribute('data-type'); - switch (type){ - case 'select': - document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value; - break; - case 'range': - break; - } + 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)); diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 70a7b34b..5eb71ecd 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -2,6 +2,7 @@ html{ background-image: url('../static/backgrounds/grass/grass-0007-large.png'); background-repeat: repeat; background-size: 650px 650px; + scroll-padding-top: 90px; } #weighted-settings{ @@ -23,11 +24,22 @@ html{ margin-bottom: 2rem; } -#weighted-settings .setting-description{ +#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%; } From b2980178d183bf7878c93c90af4622c538967ee0 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Sun, 2 Jan 2022 18:25:33 +0100 Subject: [PATCH 12/24] [Timespinner] Fixed logic of journal --- worlds/timespinner/Locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/timespinner/Locations.py b/worlds/timespinner/Locations.py index 955b06d3..e2a877aa 100644 --- a/worlds/timespinner/Locations.py +++ b/worlds/timespinner/Locations.py @@ -240,7 +240,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L 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), + 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) ) From 74bb057314ca0b4a98b0d9ae26dadc9737a9a2ed Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sun, 2 Jan 2022 18:31:15 -0500 Subject: [PATCH 13/24] Implemented range settings --- WebHostLib/static/assets/weighted-settings.js | 131 +++++++++++++++++- .../static/styles/weighted-settings.css | 35 +++++ 2 files changed, 161 insertions(+), 5 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index f00d6230..e7d80af1 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -240,20 +240,141 @@ const buildOptionsDiv = (game, settings) => { optionTable.appendChild(tbody); settingWrapper.appendChild(optionTable); - optionsWrapper.appendChild(settingWrapper); break; case 'range': - const settingDescription = document.createElement('p'); - settingDescription.classList.add('setting-description'); - settingDescription.innerText = setting.description.replace(/(\n)/g, ' '); - settingWrapper.appendChild(settingDescription); + 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.
Minimum value: ${setting.min}
` + + `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'); + + 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); + } + }); + + 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; diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 5eb71ecd..4c45e231 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -24,6 +24,26 @@ html{ 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; @@ -46,6 +66,7 @@ html{ #weighted-settings table .td-left{ padding-right: 1rem; + width: 200px; } #weighted-settings table .td-middle{ @@ -55,6 +76,20 @@ html{ 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; From d4e0347d1d61852b6d4d06097e96d42ccba51d98 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sun, 2 Jan 2022 18:45:45 -0500 Subject: [PATCH 14/24] [WebHost] weighted-settings: Fix footer style and clean up yaml download --- WebHostLib/static/assets/weighted-settings.js | 20 +++++++++++++++++++ WebHostLib/templates/weighted-settings.html | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index e7d80af1..caeef3dc 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -444,6 +444,26 @@ const exportSettings = () => { 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); }; diff --git a/WebHostLib/templates/weighted-settings.html b/WebHostLib/templates/weighted-settings.html index 353d82fb..7d60e724 100644 --- a/WebHostLib/templates/weighted-settings.html +++ b/WebHostLib/templates/weighted-settings.html @@ -33,7 +33,7 @@
-
+
From 9623c1fffd137e11c2185e558ca078e5cd4a05a1 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sun, 2 Jan 2022 18:55:38 -0500 Subject: [PATCH 15/24] [WebHost] weighted-settings: Add collapse/expand buttons to game divs --- WebHostLib/static/assets/weighted-settings.js | 24 ++++++++++++++++++- .../static/styles/weighted-settings.css | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index caeef3dc..ddf3284b 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -116,8 +116,30 @@ const buildUI = (settingData) => { gameHeader.innerText = game; gameDiv.appendChild(gameHeader); - gameDiv.appendChild(buildOptionsDiv(game, settingData.games[game].gameSettings)); + 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'); + }); }); }; diff --git a/WebHostLib/static/styles/weighted-settings.css b/WebHostLib/static/styles/weighted-settings.css index 4c45e231..ae488aff 100644 --- a/WebHostLib/static/styles/weighted-settings.css +++ b/WebHostLib/static/styles/weighted-settings.css @@ -147,6 +147,7 @@ html{ #weighted-settings a{ color: #ffef00; + cursor: pointer; } #weighted-settings input:not([type]){ From 27c528a6b33010b95d6524bfb53a94f14baff4ed Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Sun, 2 Jan 2022 19:57:26 -0500 Subject: [PATCH 16/24] [WebHost] weighted-settings: Add random, random-low, and random-high to range options --- WebHostLib/static/assets/weighted-settings.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index ddf3284b..dc69f85e 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -81,6 +81,9 @@ const createDefaultSettings = (settingData) => { 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}`); @@ -335,6 +338,36 @@ const buildOptionsDiv = (game, settings) => { } }); + ['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); From 41fdafa3fb18e932166a0e1c4ca5134d19299475 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 2 Jan 2022 21:07:43 -0500 Subject: [PATCH 17/24] LTTP Shop updates (#177) * Shop price modifier and non-lttp item price changes * Item price modifier setting --- worlds/alttp/Options.py | 6 ++++++ worlds/alttp/Shops.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index fb832d5b..962f1297 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -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, diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index fce50d72..a5d35b1b 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -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) From f06e565441e8b9bbbf1f353076182d59a6f57bca Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 3 Jan 2022 18:12:32 +0000 Subject: [PATCH 18/24] Add Rogue Legacy to Archipelago (#180) --- .gitignore | 5 +- README.md | 1 + .../static/assets/gameInfo/en_Rogue Legacy.md | 22 +++ .../assets/tutorial/legacy/legacy_en.md | 47 +++++++ .../static/assets/tutorial/tutorials.json | 19 +++ worlds/legacy/Items.py | 129 +++++++++++++++++ worlds/legacy/Locations.py | 85 ++++++++++++ worlds/legacy/Names/ItemName.py | 95 +++++++++++++ worlds/legacy/Names/LocationName.py | 52 +++++++ worlds/legacy/Options.py | 128 +++++++++++++++++ worlds/legacy/Regions.py | 60 ++++++++ worlds/legacy/Rules.py | 131 ++++++++++++++++++ worlds/legacy/__init__.py | 105 ++++++++++++++ 13 files changed, 878 insertions(+), 1 deletion(-) create mode 100644 WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md create mode 100644 WebHostLib/static/assets/tutorial/legacy/legacy_en.md create mode 100644 worlds/legacy/Items.py create mode 100644 worlds/legacy/Locations.py create mode 100644 worlds/legacy/Names/ItemName.py create mode 100644 worlds/legacy/Names/LocationName.py create mode 100644 worlds/legacy/Options.py create mode 100644 worlds/legacy/Regions.py create mode 100644 worlds/legacy/Rules.py create mode 100644 worlds/legacy/__init__.py diff --git a/.gitignore b/.gitignore index 26885103..d859d4f8 100644 --- a/.gitignore +++ b/.gitignore @@ -155,4 +155,7 @@ Archipelago.zip #minecraft server stuff jdk*/ -minecraft*/ \ No newline at end of file +minecraft*/ + +#pyenv +.python-version diff --git a/README.md b/README.md index 739a8caf..35224128 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md b/WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md new file mode 100644 index 00000000..f7c1068d --- /dev/null +++ b/WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md @@ -0,0 +1,22 @@ +# Rogue Legacy (PC) + +## Where is the settings page? +The player settings page for this game is located here. 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! diff --git a/WebHostLib/static/assets/tutorial/legacy/legacy_en.md b/WebHostLib/static/assets/tutorial/legacy/legacy_en.md new file mode 100644 index 00000000..65f9d704 --- /dev/null +++ b/WebHostLib/static/assets/tutorial/legacy/legacy_en.md @@ -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 rogue legacy settings page here. + +### 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!** diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index 67f33e16..eb27113d 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -342,5 +342,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": "legacy/legacy_en.md", + "link": "legacy/legacy/en", + "authors": [ + "Phar" + ] + } + ] + } + ] } ] diff --git a/worlds/legacy/Items.py b/worlds/legacy/Items.py new file mode 100644 index 00000000..e134444b --- /dev/null +++ b/worlds/legacy/Items.py @@ -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} diff --git a/worlds/legacy/Locations.py b/worlds/legacy/Locations.py new file mode 100644 index 00000000..8e256c0f --- /dev/null +++ b/worlds/legacy/Locations.py @@ -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()} diff --git a/worlds/legacy/Names/ItemName.py b/worlds/legacy/Names/ItemName.py new file mode 100644 index 00000000..474f8ef7 --- /dev/null +++ b/worlds/legacy/Names/ItemName.py @@ -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" diff --git a/worlds/legacy/Names/LocationName.py b/worlds/legacy/Names/LocationName.py new file mode 100644 index 00000000..f6699907 --- /dev/null +++ b/worlds/legacy/Names/LocationName.py @@ -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" diff --git a/worlds/legacy/Options.py b/worlds/legacy/Options.py new file mode 100644 index 00000000..c46aa207 --- /dev/null +++ b/worlds/legacy/Options.py @@ -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, +} diff --git a/worlds/legacy/Regions.py b/worlds/legacy/Regions.py new file mode 100644 index 00000000..7990da27 --- /dev/null +++ b/worlds/legacy/Regions.py @@ -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 diff --git a/worlds/legacy/Rules.py b/worlds/legacy/Rules.py new file mode 100644 index 00000000..3dd233ca --- /dev/null +++ b/worlds/legacy/Rules.py @@ -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) diff --git a/worlds/legacy/__init__.py b/worlds/legacy/__init__.py new file mode 100644 index 00000000..41e7a20b --- /dev/null +++ b/worlds/legacy/__init__.py @@ -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) From 5a064b0979ade7cce248a51a2a558617dd7b7b48 Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 3 Jan 2022 19:56:54 -0500 Subject: [PATCH 19/24] [WebHost] weighted-settings: Ranges with a total distance <= 10 are always printed in full --- WebHostLib/static/assets/weighted-settings.js | 113 +++++++++++------- 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index dc69f85e..91789c2f 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -292,51 +292,84 @@ const buildOptionsDiv = (game, settings) => { const rangeTable = document.createElement('table'); const rangeTbody = document.createElement('tbody'); - Object.keys(currentSettings[game][settingName]).forEach((option) => { - if (currentSettings[game][settingName][option] > 0) { + 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 = option; - tr.appendChild(tdLeft); + 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}-${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 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}-${option}`) - tdRight.classList.add('td-right'); - tdRight.innerText = range.value; - tr.appendChild(tdRight); + const tdRight = document.createElement('td'); + tdRight.setAttribute('id', `${game}-${settingName}-${i}`) + 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); + 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'); From b3c1c0bbe84064b53113ae93f01b25346c518784 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 3 Jan 2022 20:17:33 -0600 Subject: [PATCH 20/24] RogueLegacy: Moved world definition from "legacy" to "rogue-legacy" to avoid confusion with deprecation terms --- .../{legacy/legacy_en.md => rogue-legacy/rogue-legacy_en.md} | 0 WebHostLib/static/assets/tutorial/tutorials.json | 4 ++-- worlds/{legacy => rogue-legacy}/Items.py | 0 worlds/{legacy => rogue-legacy}/Locations.py | 0 worlds/{legacy => rogue-legacy}/Names/ItemName.py | 0 worlds/{legacy => rogue-legacy}/Names/LocationName.py | 0 worlds/{legacy => rogue-legacy}/Options.py | 0 worlds/{legacy => rogue-legacy}/Regions.py | 0 worlds/{legacy => rogue-legacy}/Rules.py | 0 worlds/{legacy => rogue-legacy}/__init__.py | 0 10 files changed, 2 insertions(+), 2 deletions(-) rename WebHostLib/static/assets/tutorial/{legacy/legacy_en.md => rogue-legacy/rogue-legacy_en.md} (100%) rename worlds/{legacy => rogue-legacy}/Items.py (100%) rename worlds/{legacy => rogue-legacy}/Locations.py (100%) rename worlds/{legacy => rogue-legacy}/Names/ItemName.py (100%) rename worlds/{legacy => rogue-legacy}/Names/LocationName.py (100%) rename worlds/{legacy => rogue-legacy}/Options.py (100%) rename worlds/{legacy => rogue-legacy}/Regions.py (100%) rename worlds/{legacy => rogue-legacy}/Rules.py (100%) rename worlds/{legacy => rogue-legacy}/__init__.py (100%) diff --git a/WebHostLib/static/assets/tutorial/legacy/legacy_en.md b/WebHostLib/static/assets/tutorial/rogue-legacy/rogue-legacy_en.md similarity index 100% rename from WebHostLib/static/assets/tutorial/legacy/legacy_en.md rename to WebHostLib/static/assets/tutorial/rogue-legacy/rogue-legacy_en.md diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index eb27113d..5cc02658 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -352,8 +352,8 @@ "files": [ { "language": "English", - "filename": "legacy/legacy_en.md", - "link": "legacy/legacy/en", + "filename": "rogue-legacy/rogue-legacy_en.md", + "link": "rogue-legacy/rogue-legacy/en", "authors": [ "Phar" ] diff --git a/worlds/legacy/Items.py b/worlds/rogue-legacy/Items.py similarity index 100% rename from worlds/legacy/Items.py rename to worlds/rogue-legacy/Items.py diff --git a/worlds/legacy/Locations.py b/worlds/rogue-legacy/Locations.py similarity index 100% rename from worlds/legacy/Locations.py rename to worlds/rogue-legacy/Locations.py diff --git a/worlds/legacy/Names/ItemName.py b/worlds/rogue-legacy/Names/ItemName.py similarity index 100% rename from worlds/legacy/Names/ItemName.py rename to worlds/rogue-legacy/Names/ItemName.py diff --git a/worlds/legacy/Names/LocationName.py b/worlds/rogue-legacy/Names/LocationName.py similarity index 100% rename from worlds/legacy/Names/LocationName.py rename to worlds/rogue-legacy/Names/LocationName.py diff --git a/worlds/legacy/Options.py b/worlds/rogue-legacy/Options.py similarity index 100% rename from worlds/legacy/Options.py rename to worlds/rogue-legacy/Options.py diff --git a/worlds/legacy/Regions.py b/worlds/rogue-legacy/Regions.py similarity index 100% rename from worlds/legacy/Regions.py rename to worlds/rogue-legacy/Regions.py diff --git a/worlds/legacy/Rules.py b/worlds/rogue-legacy/Rules.py similarity index 100% rename from worlds/legacy/Rules.py rename to worlds/rogue-legacy/Rules.py diff --git a/worlds/legacy/__init__.py b/worlds/rogue-legacy/__init__.py similarity index 100% rename from worlds/legacy/__init__.py rename to worlds/rogue-legacy/__init__.py From 03a892aded788181e1c33ee8e9c3a4d2c8ee6ce4 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Tue, 4 Jan 2022 10:16:09 -0600 Subject: [PATCH 21/24] OoT updates (#160) * OoT: disable mixed entrance pools and decoupled entrances for now * OoT: fix error message crash in get_hint_area * Oot Adjuster: kill zootdec if it's not the vanilla rom anymore * OoT Adjuster: fix dmaTable issue Adjuster should now work on compiled versions of the software * OoT: don't skip dungeon items shuffled as any_dungeon for barren hints * OoT: wrap zootdec remove in try-finally --- OoTAdjuster.py | 27 ++++++++++++++++++--------- worlds/oot/Hints.py | 2 +- worlds/oot/Options.py | 4 ++-- worlds/oot/Rom.py | 2 +- worlds/oot/__init__.py | 9 ++++++++- worlds/oot/data/Compress/dmaTable.dat | 1 + 6 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 worlds/oot/data/Compress/dmaTable.dat diff --git a/OoTAdjuster.py b/OoTAdjuster.py index a9746e9a..36c37b04 100644 --- a/OoTAdjuster.py +++ b/OoTAdjuster.py @@ -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__': diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index f282aeee..0f28ca9c 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -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 diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index b0e7fa1e..e46927a3 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -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, diff --git a/worlds/oot/Rom.py b/worlds/oot/Rom.py index d4396254..8c4129e2 100644 --- a/worlds/oot/Rom.py +++ b/worlds/oot/Rom.py @@ -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" diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index 66d08895..ee936a44 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -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 diff --git a/worlds/oot/data/Compress/dmaTable.dat b/worlds/oot/data/Compress/dmaTable.dat new file mode 100644 index 00000000..7db35a81 --- /dev/null +++ b/worlds/oot/data/Compress/dmaTable.dat @@ -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 \ No newline at end of file From 4dd7c63cab9bc425d47938d01cfe9e9cdc533134 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 4 Jan 2022 20:04:02 +0100 Subject: [PATCH 22/24] Generate: fix accessibility and progression_balancing override --- Generate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Generate.py b/Generate.py index fc82ad43..4704a59d 100644 --- a/Generate.py +++ b/Generate.py @@ -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": From 963e9d4bb5a493c50ded2035228a5172d9250984 Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Tue, 4 Jan 2022 22:56:53 +0100 Subject: [PATCH 23/24] [Timespinner] Updated timespinner setup docs (#184) * [Timespinner] Updated setup docs --- .../assets/tutorial/timespinner/setup_en.md | 40 +++---------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md index 9dec349b..ce514322 100644 --- a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md +++ b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md @@ -2,7 +2,7 @@ ## Required Software -- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner) +- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/), [Timespinner (humble)](https://www.humblebundle.com/store/timespinner) or [Timespinner (GOG)](https://www.gog.com/game/timespinner) (other versions are not supported) - [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) ## General Concept @@ -11,7 +11,7 @@ The timespinner Randomizer loads Timespinner.exe from the same folder, and alter ## Installation Procedures -Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) 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 [readme](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](https://github.com/JarnoWesthof/TsRandomizer) ## Joining a MultiWorld Game @@ -23,38 +23,8 @@ Download latest version of [Timespinner Randomizer](https://github.com/JarnoWest 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 -## YAML Settings -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 +## Where do I get a config file? +The [Player Settings](https://archipelago.gg/tutorial/timespinner/setup/en) page on the website allows you to configure your personal settings and export a config file from them. + * 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 \ No newline at end of file From 0c3b5439e9adfc96f9c28b7d2d265317e81dc0bc Mon Sep 17 00:00:00 2001 From: Jarno Westhof Date: Tue, 4 Jan 2022 23:01:15 +0100 Subject: [PATCH 24/24] [Timespinner] Actually use the correct url in setup doc --- WebHostLib/static/assets/tutorial/timespinner/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md index ce514322..a65ff52a 100644 --- a/WebHostLib/static/assets/tutorial/timespinner/setup_en.md +++ b/WebHostLib/static/assets/tutorial/timespinner/setup_en.md @@ -24,7 +24,7 @@ Download latest release on [Timespinner Randomizer Releases](https://github.com/ 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 ## Where do I get a config file? -The [Player Settings](https://archipelago.gg/tutorial/timespinner/setup/en) page on the website allows you to configure your personal settings and export a config file from them. +The [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) page on the website allows you to configure your personal settings and export a config file from them. * 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 \ No newline at end of file