Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebHost, Core: Developer-defined game option presets. #2143

Merged
merged 15 commits into from
Nov 16, 2023
Merged
11 changes: 5 additions & 6 deletions WebHostLib/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@
import os
import typing

import yaml
from jinja2 import Template

import Options
from Utils import __version__, local_path
from Utils import local_path
from worlds.AutoWorld import AutoWorldRegister

handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
Expand All @@ -28,7 +25,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str:
weighted_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"name": "Player",
"name": "",
"game": {},
},
"games": {},
Expand All @@ -46,7 +43,7 @@ def get_html_doc(option_type: type(Options.Option)) -> str:
"baseOptions": {
"description": f"Generated by https://archipelago.gg/ for {game_name}",
"game": game_name,
"name": "Player",
"name": "",
},
}

Expand Down Expand Up @@ -124,6 +121,8 @@ def get_html_doc(option_type: type(Options.Option)) -> str:

player_settings["gameOptions"] = game_options

player_settings["presetOptions"] = world.web.options_presets

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:
Expand Down
112 changes: 109 additions & 3 deletions WebHostLib/static/assets/player-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@ window.addEventListener('load', () => {
const nameInput = document.getElementById('player-name');
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
nameInput.value = playerSettings.name;

// Presets
const presetSelect = document.getElementById("game-options-preset");
presetSelect.addEventListener("change", (event) => setPresets(results, event.target.value));
for (const preset in results["presetOptions"]) {
const presetOption = document.createElement("option");
presetOption.innerText = preset;
presetSelect.appendChild(presetOption);
}
presetSelect.value = localStorage.getItem(`${gameName}-preset`);
results["presetOptions"]["__default"] = {};
}).catch((e) => {
console.error(e);
const url = new URL(window.location.href);
Expand All @@ -45,7 +56,8 @@ window.addEventListener('load', () => {

const resetSettings = () => {
localStorage.removeItem(gameName);
localStorage.removeItem(`${gameName}-hash`)
localStorage.removeItem(`${gameName}-hash`);
localStorage.removeItem(`${gameName}-preset`);
window.location.reload();
};

Expand Down Expand Up @@ -77,6 +89,10 @@ const createDefaultSettings = (settingData) => {
}
localStorage.setItem(gameName, JSON.stringify(newSettings));
}

if (!localStorage.getItem(`${gameName}-preset`)) {
localStorage.setItem(`${gameName}-preset`, "__default");
}
};

const buildUI = (settingData) => {
Expand Down Expand Up @@ -162,6 +178,7 @@ const buildOptionsTable = (settings, romOpts = false) => {
element.classList.add('range-container');

let range = document.createElement('input');
range.setAttribute('id', setting);
range.setAttribute('type', 'range');
range.setAttribute('data-key', setting);
range.setAttribute('min', settings[setting].min);
Expand Down Expand Up @@ -294,6 +311,75 @@ const buildOptionsTable = (settings, romOpts = false) => {
return table;
};

const setPresets = (settingsData, presetName) => {
const defaults = settingsData["gameOptions"];
const preset = settingsData["presetOptions"][presetName];

localStorage.setItem(`${gameName}-preset`, presetName);

if (!preset) {
console.error(`No presets defined for preset name: "${presetName}"`);
return;
}

for (const setting in defaults) {
let presetValue = preset[setting];
if (presetValue === undefined) {
// Using the default value if not set in presets.
presetValue = defaults[setting]["defaultValue"];
}

switch (defaults[setting].type) {
case "range":
case "select": {
const optionElement = document.querySelector(`#${setting}[data-key="${setting}"]`);
const randomElement = document.querySelector(`.randomize-button[data-key="${setting}"]`);

if (presetValue === "random") {
randomElement.classList.add("active");
optionElement.disabled = true;
updateGameSetting(randomElement, false);
} else {
optionElement.value = presetValue;
randomElement.classList.remove("active");
optionElement.disabled = undefined;
updateGameSetting(optionElement, false);
}

break;
}

case "special_range": {
const selectElement = document.querySelector(`select[data-key="${setting}"]`);
const rangeElement = document.querySelector(`input[data-key="${setting}"]`);
const randomElement = document.querySelector(`.randomize-button[data-key="${setting}"]`);

if (presetValue === "random") {
randomElement.classList.add("active");
selectElement.disabled = true;
rangeElement.disabled = true;
updateGameSetting(randomElement, false);
} else {
rangeElement.value = presetValue;
selectElement.value = Object.values(defaults[setting]["value_names"]).includes(parseInt(presetValue)) ?
parseInt(presetValue) : "custom";
document.getElementById(`${setting}-value`).innerText = presetValue;

randomElement.classList.remove("active");
selectElement.disabled = undefined;
rangeElement.disabled = undefined;
updateGameSetting(rangeElement, false);
}
break;
}

default:
console.warn(`Ignoring preset value for unknown setting type: ${defaults[setting].type} with name ${setting}`);
break;
}
}
};

const toggleRandomize = (event, inputElement, optionalSelectElement = null) => {
const active = event.target.classList.contains('active');
const randomButton = event.target;
Expand Down Expand Up @@ -322,9 +408,15 @@ const updateBaseSetting = (event) => {
localStorage.setItem(gameName, JSON.stringify(options));
};

const updateGameSetting = (settingElement) => {
const updateGameSetting = (settingElement, toggleCustomPreset = true) => {
const options = JSON.parse(localStorage.getItem(gameName));

if (toggleCustomPreset) {
localStorage.setItem(`${gameName}-preset`, "__custom");
const presetElement = document.getElementById("game-options-preset");
presetElement.value = "__custom";
}

if (settingElement.classList.contains('randomize-button')) {
// If the event passed in is the randomize button, then we know what we must do.
options[gameName][settingElement.getAttribute('data-key')] = 'random';
Expand All @@ -338,7 +430,21 @@ const updateGameSetting = (settingElement) => {

const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
const preset = localStorage.getItem(`${gameName}-preset`);
switch (preset) {
case "__default":
settings["description"] = `Generated by https://archipelago.gg with the default preset.`;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This case does exactly the same thing as the default case, it's therefore not necessary

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Negative, because it would output Generated by https://archipelago.gg with the __default preset. because it uses the value (not the display name).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I don't want that :P

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, if __default and __custom include the leading underscores, would other presets also include it? Should it maybe get trimmed away here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other preset names shouldn't have the leading underscores.

break;

case "__custom":
settings["description"] = `Generated by https://archipelago.gg.`;
break;

default:
settings["description"] = `Generated by https://archipelago.gg with the ${preset} preset.`;
}

if (!settings.name || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
Expand Down
17 changes: 17 additions & 0 deletions WebHostLib/static/styles/player-settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ html{
flex-direction: row;
}

#player-settings #meta-options {
display: flex;
flex-direction: column;
gap: 6px;
}

#player-settings #meta-options label {
display: inline-block;
min-width: 180px;
}

#player-settings #meta-options input,
#player-settings #meta-options select {
box-sizing: border-box;
min-width: 200px;
}

#player-settings .left, #player-settings .right{
flex-grow: 1;
}
Expand Down
23 changes: 19 additions & 4 deletions WebHostLib/templates/player-settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,25 @@ <h1><span id="game-name">Player</span> Settings</h1>
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
</p>

<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
items if you are playing in a MultiWorld.</label><br />
<input id="player-name" placeholder="Player Name" data-key="name" maxlength="16" />
</p>
<h2>Meta Options</h2>
<div id="meta-options">
<div>
<label for="player-name">
Player Name: <span class="interactive" data-tooltip="This is the name you use to connect with your game. This is also known as your 'slot name'.">(?)</span>
</label>
<input id="player-name" placeholder="Player" data-key="name" maxlength="16" />
</div>
<div>
<label for="game-options-preset">
Options Preset: <span class="interactive" data-tooltip="Select a developer-curated preset of game options!">(?)</span>
</label>
<select id="game-options-preset">
<option value="__default">Defaults</option>
<option value="__custom" hidden>Custom</option>
</select>
</div>

</div>

<h2>Game Options</h2>
<div id="game-options">
Expand Down
3 changes: 3 additions & 0 deletions worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,9 @@ class WebWorld:
bug_report_page: Optional[str]
"""display a link to a bug report page, most likely a link to a GitHub issue page."""

options_presets: Dict[str, Dict[str, Any]] = {}
"""A dictionary containing a collection of developer-defined game option presets."""


class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
Expand Down
56 changes: 56 additions & 0 deletions worlds/rogue_legacy/Presets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Any, Dict

rl_option_presets: Dict[str, Dict[str, Any]] = {
"All Random": {
"progression_balancing": "random",
"accessibility": "random",
"starting_gender": "random",
"starting_class": "random",
"new_game_plus": "random",
"fairy_chests_per_zone": "random",
"chests_per_zone": "random",
"universal_fairy_chests": "random",
"universal_chests": "random",
"vendors": "random",
"architect": "random",
"architect_fee": "random",
"disable_charon": "random",
"require_purchasing": "random",
"progressive_blueprints": "random",
"gold_gain_multiplier": "random",
"number_of_children": "random",
"free_diary_on_generation": "random",
"khidr": "random",
"alexander": "random",
"leon": "random",
"herodotus": "random",
"health_pool": "random",
"mana_pool": "random",
"attack_pool": "random",
"magic_damage_pool": "random",
"armor_pool": "random",
"equip_pool": "random",
"crit_chance_pool": "random",
"crit_damage_pool": "random",
"allow_default_names": True,
"death_link": "random",
},
"Limited Resources": {
"progression_balancing": 0,
"fairy_chests_per_zone": 2,
"chests_per_zone": 30,
"vendors": "normal",
"architect": "disabled",
"gold_gain_multiplier": "half",
"number_of_children": 2,
"free_diary_on_generation": False,
"health_pool": 10,
"mana_pool": 10,
"attack_pool": 10,
"magic_damage_pool": 10,
"armor_pool": 5,
"equip_pool": 10,
"crit_chance_pool": 0,
"crit_damage_pool": 0,
}
}
2 changes: 2 additions & 0 deletions worlds/rogue_legacy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .Items import RLItem, RLItemData, event_item_table, get_items_by_category, item_table
from .Locations import RLLocation, location_table
from .Options import rl_options
from .Presets import rl_option_presets
from .Regions import create_regions
from .Rules import set_rules

Expand All @@ -22,6 +23,7 @@ class RLWeb(WebWorld):
)]
bug_report_page = "https://github.com/ThePhar/RogueLegacyRandomizer/issues/new?assignees=&labels=bug&template=" \
"report-an-issue---.md&title=%5BIssue%5D"
options_presets = rl_option_presets
ThePhar marked this conversation as resolved.
Show resolved Hide resolved


class RLWorld(World):
Expand Down