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 9ade10e8e2..3b742068b5 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 f650993862..6215ffade1 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 3b742068b5..a2041339f3 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 f1fbce853b..f92077ad74 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 cfc5de8164..a91ee51eb3 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 228bf37548..d530f9e05e 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 7095d7d06d..cdd7e31534 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 355bfdfdd2..7919a34e3f 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 74cf4c42ea..a1f474de05 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 da3cff0895..9bdc689ede 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 cba17c9510..0e9d7a3678 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 9ade10e8e2..a1c7b0df5c 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 21902eca1d..26744c7639 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 3a56604b5e..70a7b34bb5 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 09cd477141..353d82fb31 100644
--- a/WebHostLib/templates/weighted-settings.html
+++ b/WebHostLib/templates/weighted-settings.html
@@ -24,15 +24,13 @@