Add Gameplay settings & make Panes scrollable

This commit is contained in:
Luke Street
2026-04-29 21:52:33 -06:00
parent 6425b452e7
commit b86d6e90e2
7 changed files with 315 additions and 43 deletions
+59 -13
View File
@@ -19,6 +19,9 @@ button {
}
.window {
display: flex;
flex-flow: column;
height: 100%;
max-width: 1088dp;
max-height: 768dp;
margin: auto;
@@ -30,8 +33,18 @@ button {
background-color: rgba(21, 22, 16, 90%);
}
@media (max-height: 640dp) {
body {
padding: 16dp;
}
.window {
box-shadow: none;
}
}
.window .tab-bar {
display: flex;
flex: 0 0 64dp;
height: 64dp;
background-color: rgba(217, 217, 217, 10%);
font-family: "Fira Sans Condensed";
@@ -68,7 +81,10 @@ button {
.window .content {
display: flex;
height: 100%;
flex: 1 1 0;
min-width: 0;
min-height: 0;
overflow: hidden;
}
.window .content .pane {
@@ -76,30 +92,56 @@ button {
flex-flow: column;
flex: 1 1 0;
height: 100%;
min-width: 0;
min-height: 0;
padding: 24dp;
padding-bottom: 0dp;
gap: 8dp;
overflow: auto;
overflow: hidden auto;
}
.window .content .pane:not(:last-of-type) {
border-right: 1dp #92875B;
}
.window .content .pane.detail-pane {
gap: 16dp;
.window .content .pane > * {
flex: 0 0 auto;
}
.window .content .detail-value {
padding: 12dp 16dp;
border-radius: 12dp;
background-color: rgba(17, 16, 10, 20%);
box-shadow: rgba(146, 135, 91, 25%) 0 0 0 1dp;
font-size: 20dp;
.window .content .pane > .spacer {
display: block;
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
flex: 0 0 16dp;
height: 16dp;
pointer-events: none;
}
.window .content .detail-controls {
display: flex;
gap: 12dp;
scrollbarvertical {
width: 8dp;
}
scrollbarvertical sliderarrowdec,
scrollbarvertical sliderarrowinc {
width: 0;
height: 0;
}
scrollbarvertical slidertrack {
width: 8dp;
background-color: rgba(224, 219, 200, 5%);
border-radius: 2dp;
}
scrollbarvertical sliderbar {
width: 8dp;
min-height: 24dp;
background-color: rgba(224, 219, 200, 45%);
border-radius: 2dp;
}
scrollbarvertical sliderbar:hover,
scrollbarvertical sliderbar:active {
background-color: rgba(194, 164, 45, 80%);
}
.section-heading {
@@ -109,6 +151,10 @@ button {
opacity: 0.25;
}
.section-heading:not(:first-of-type) {
padding-top: 12dp;
}
.button {
text-align: center;
background-color: rgba(17, 16, 10, 20%);
+6
View File
@@ -15,6 +15,12 @@ void Component::update() {
bool Component::focus() {
// Can we focus self?
if (mRoot->Focus(true)) {
mRoot->ScrollIntoView(Rml::ScrollIntoViewOptions{
Rml::ScrollAlignment::Center,
Rml::ScrollAlignment::Nearest,
Rml::ScrollBehavior::Smooth,
Rml::ScrollParentage::Closest,
});
return true;
}
// Otherwise, try to focus a child
+31 -11
View File
@@ -38,16 +38,10 @@ Pane::Pane(Rml::Element* parent, Direction direction, const Rml::String& classNa
break;
}
}
int i = focusedChild + direction;
if (focusedChild == -1) {
// If the container itself is focused and next is pressed, focus the first element
if (direction == 1) {
i = 0;
} else {
// Otherwise, allow event to bubble
return;
}
return;
}
int i = focusedChild + direction;
while (i >= 0 && i < static_cast<int>(mChildren.size())) {
if (mChildren[i]->focus()) {
event.StopPropagation();
@@ -58,6 +52,11 @@ Pane::Pane(Rml::Element* parent, Direction direction, const Rml::String& classNa
});
}
void Pane::update() {
finalize();
Component::update();
}
bool Pane::focus() {
for (const auto& child : mChildren) {
if (child->focus()) {
@@ -80,8 +79,29 @@ Rml::Element* Pane::add_text(const Rml::String& text) {
return elem;
}
void Pane::clear() {
clear_children();
Rml::Element* Pane::add_rml(const Rml::String& rml) {
auto* elem = append(mRoot, "div");
elem->SetInnerRML(rml);
return elem;
}
} // namespace dusk::ui
void Pane::finalize() {
if (finalized) {
return;
}
finalized = true;
// Append spacer element to the bottom. RmlUi does not properly handle
// padding-bottom or margin-bottom on a scrollable flex container, so
// we need to create a fake spacer with an actual layout height to get
// padding at the bottom of a scrollable container.
auto* elem = append(mRoot, "div");
elem->SetClass("spacer", true);
}
void Pane::clear() {
clear_children();
finalized = false;
}
} // namespace dusk::ui
+5 -1
View File
@@ -16,6 +16,7 @@ public:
explicit Pane(Rml::Element* parent, Direction direction, const Rml::String& className = "pane");
bool focus() override;
void update() override;
Rml::Element* add_section(const Rml::String& text);
Button& add_button(Button::Props props) { return add_child<Button>(mRoot, std::move(props)); }
@@ -23,10 +24,13 @@ public:
return add_child<SelectButton>(mRoot, std::move(props));
}
Rml::Element* add_text(const Rml::String& text);
Rml::Element* add_rml(const Rml::String& rml);
void finalize();
void clear();
private:
Direction mDirection;
bool finalized = false;
};
} // namespace dusk::ui
} // namespace dusk::ui
+212 -11
View File
@@ -5,12 +5,44 @@
#include "aurora/gfx.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/config.hpp"
#include "dusk/livesplit.h"
#include "m_Do/m_Do_main.h"
#include "pane.hpp"
#include "ui.hpp"
#include <algorithm>
#include <climits>
namespace dusk::ui {
namespace {
void reset_for_speedrun_mode() {
mDoMain::developmentMode = -1;
getSettings().game.damageMultiplier.setValue(1);
getSettings().game.instantDeath.setValue(false);
getSettings().game.noHeartDrops.setValue(false);
getSettings().game.infiniteHearts.setValue(false);
getSettings().game.infiniteArrows.setValue(false);
getSettings().game.infiniteBombs.setValue(false);
getSettings().game.infiniteOil.setValue(false);
getSettings().game.infiniteOxygen.setValue(false);
getSettings().game.infiniteRupees.setValue(false);
getSettings().game.enableIndefiniteItemDrops.setValue(false);
getSettings().game.moonJump.setValue(false);
getSettings().game.superClawshot.setValue(false);
getSettings().game.alwaysGreatspin.setValue(false);
getSettings().game.enableFastIronBoots.setValue(false);
getSettings().game.canTransformAnywhere.setValue(false);
getSettings().game.fastSpinner.setValue(false);
getSettings().game.freeMagicArmor.setValue(false);
getSettings().game.enableTurboKeybind.setValue(false);
}
} // namespace
template <typename T>
struct ConfigProps {
@@ -34,11 +66,11 @@ public:
if (!mHelpText.empty() && mRightPane != nullptr) {
listen(Rml::EventId::Focus, [this](Rml::Event&) {
mRightPane->clear();
mRightPane->add_text(mHelpText);
mRightPane->add_rml(mHelpText);
});
listen(Rml::EventId::Mouseover, [this](Rml::Event&) {
mRightPane->clear();
mRightPane->add_text(mHelpText);
mRightPane->add_rml(mHelpText);
});
}
}
@@ -87,23 +119,57 @@ protected:
}
};
class ConfigIntPercentSelect : public ConfigSelect<int> {
class ConfigIntSelect : public ConfigSelect<int> {
public:
ConfigIntPercentSelect(Rml::Element* parent, Props props)
: ConfigSelect(parent, std::move(props)) {}
struct Props {
Rml::String key;
ConfigVar<int>* value;
std::function<void(int)> onChange;
std::function<bool()> isDisabled;
Rml::String helpText;
Pane* rightPane = nullptr;
int min = 0;
int max = INT_MAX;
int step = 1;
Rml::String prefix;
Rml::String suffix;
};
ConfigIntSelect(Rml::Element* parent, Props props)
: ConfigSelect(parent,
{
.key = std::move(props.key),
.value = props.value,
.onChange = std::move(props.onChange),
.isDisabled = std::move(props.isDisabled),
.helpText = std::move(props.helpText),
.rightPane = props.rightPane,
}),
mMin(props.min), mMax(props.max), mStep(props.step), mPrefix(std::move(props.prefix)),
mSuffix(std::move(props.suffix)) {}
protected:
Rml::String get_value() override { return fmt::format("{}%", mVar->getValue()); }
Rml::String get_value() override {
return fmt::format("{}{}{}", mPrefix, mVar->getValue(), mSuffix);
}
bool handle_nav_command(NavCommand cmd) override {
if (cmd == NavCommand::Left) {
set_value(std::max(mVar->getValue() - 1, 0));
set_value(std::clamp(mVar->getValue() - mStep, mMin, mMax));
return true;
} else if (cmd == NavCommand::Right) {
set_value(std::min(mVar->getValue() + 1, 100));
set_value(std::clamp(mVar->getValue() + mStep, mMin, mMax));
return true;
}
return false;
}
private:
int mMin;
int mMax;
int mStep;
Rml::String mPrefix;
Rml::String mSuffix;
};
SettingsWindow::SettingsWindow() {
@@ -112,13 +178,15 @@ SettingsWindow::SettingsWindow() {
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
leftPane.add_section("Volume");
leftPane.add_child<ConfigIntPercentSelect>(leftPane.root(),
ConfigIntPercentSelect::Props{
leftPane.add_child<ConfigIntSelect>(leftPane.root(),
ConfigIntSelect::Props{
.key = "Master Volume",
.value = &getSettings().audio.masterVolume,
.onChange = [](int value) { audio::SetMasterVolume(value / 100.f); },
.helpText = "Adjusts the volume of all sounds in the game.",
.rightPane = &rightPane,
.max = 100,
.suffix = "%",
});
leftPane.add_section("Effects");
@@ -158,7 +226,7 @@ SettingsWindow::SettingsWindow() {
leftPane.root(), ConfigBoolSelect::Props{
.key = key,
.value = &value,
.isDisabled = [] { return !getSettings().game.speedrunMode; },
.isDisabled = [] { return getSettings().game.speedrunMode; },
.helpText = helpText,
.rightPane = &rightPane,
});
@@ -195,7 +263,114 @@ SettingsWindow::SettingsWindow() {
});
add_tab("Gameplay", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Direction::Vertical);
auto& rightPane = add_child<Pane>(content, Pane::Direction::Vertical);
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
leftPane.add_child<ConfigBoolSelect>(leftPane.root(), ConfigBoolSelect::Props{
.key = key,
.value = &value,
.helpText = helpText,
.rightPane = &rightPane,
});
};
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
leftPane.add_child<ConfigBoolSelect>(
leftPane.root(), ConfigBoolSelect::Props{
.key = key,
.value = &value,
.isDisabled = [] { return getSettings().game.speedrunMode; },
.helpText = helpText,
.rightPane = &rightPane,
});
};
leftPane.add_section("General");
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
"Mirrors the world horizontally, matching the Wii version of the game.");
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
"experience.");
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
"Restores patched glitches from Wii USA 1.0, the first released version.");
addOption("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation,
"Enables rotating Link in the collection menu with the C-Stick.");
leftPane.add_section("Difficulty");
leftPane.add_child<ConfigIntSelect>(
leftPane.root(), ConfigIntSelect::Props{
.key = "Damage Multiplier",
.value = &getSettings().game.damageMultiplier,
.isDisabled = [] { return getSettings().game.speedrunMode; },
.helpText = "Multiplies incoming damage.",
.rightPane = &rightPane,
.min = 1,
.max = 8,
.prefix = "x",
});
addSpeedrunDisabledOption(
"Instant Death", getSettings().game.instantDeath, "Any hit will instantly kill you.");
addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops,
"Hearts will never drop from enemies, pots, and various other places.");
leftPane.add_section("Quality of Life");
addOption("Bigger Wallets", getSettings().game.biggerWallets,
"Wallet sizes are like in the HD version. (500, 1000, 2000)");
addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes,
"Rupees will not play cutscenes after you have collected them the first time.");
addOption("Faster Climbing", getSettings().game.fastClimbing,
"Quicker climbing on ladders and vines like the HD version.");
addOption("Faster Tears of Light", getSettings().game.fastTears,
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
addOption("Instant Saves", getSettings().game.instantSaves,
"Skips the delay when writing to the Memory Card.");
addOption("Hold B for Instant Text", getSettings().game.instantText,
"Makes text scroll immediately by holding B.");
addOption("No Climbing Miss Animation", getSettings().game.noMissClimbing,
"Prevents Link from playing a struggle animation when grabbing ledges or climbing on "
"vines.");
addOption("No Rupee Returns", getSettings().game.noReturnRupees,
"Always collect Rupees even if your Wallet is too full.");
addOption("No Sword Recoil", getSettings().game.noSwordRecoil,
"Link will not recoil when his sword hits walls.");
addOption("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen,
"Skips the TV calibration screen shown when loading a save.");
addOption("Skip Warning Screen", getSettings().game.skipWarningScreen,
"Skips the warning screen shown when starting the game.");
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
"Allows Wolf Link to howl and change the time of day.");
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
"Transform instantly by pressing R and Y simultaneously.");
leftPane.add_section("Speedrunning");
leftPane.add_child<ConfigBoolSelect>(leftPane.root(),
ConfigBoolSelect::Props{
.key = "Speedrun Mode",
.value = &getSettings().game.speedrunMode,
.onChange = [](bool) { reset_for_speedrun_mode(); },
.helpText = "Enables speedrunning options while restricting certain "
"gameplay modifiers.",
.rightPane = &rightPane,
});
leftPane.add_child<ConfigBoolSelect>(
leftPane.root(), ConfigBoolSelect::Props{
.key = "LiveSplit Connection",
.value = &getSettings().game.liveSplitEnabled,
.onChange =
[](bool enabled) {
if (enabled) {
speedrun::connectLiveSplit();
} else {
speedrun::disconnectLiveSplit();
}
},
.isDisabled = [] { return !getSettings().game.speedrunMode; },
.helpText = "Connect to LiveSplit server on localhost:16834.",
.rightPane = &rightPane,
});
});
add_tab("Graphics", [this](Rml::Element* content) {
@@ -246,6 +421,32 @@ SettingsWindow::SettingsWindow() {
});
leftPane.add_section("Resolution");
// TODO: Internal Resolution
leftPane.add_section("Post-Processing");
// TODO: Bloom
// TODO: Bloom Brightness
leftPane.add_section("Rendering");
leftPane.add_child<ConfigBoolSelect>(leftPane.root(),
ConfigBoolSelect::Props{
.key = "Unlock Framerate",
.value = &getSettings().game.enableFrameInterpolation,
.helpText =
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>Visual "
"artifacts, animation glitches, or instability may occur.",
.rightPane = &rightPane,
});
leftPane.add_child<ConfigBoolSelect>(
leftPane.root(), ConfigBoolSelect::Props{
.key = "Enable Depth of Field",
.value = &getSettings().game.enableDepthOfField,
});
leftPane.add_child<ConfigBoolSelect>(
leftPane.root(), ConfigBoolSelect::Props{
.key = "Enable Mini-Map Shadows",
.value = &getSettings().game.enableMapBackground,
});
});
}
-5
View File
@@ -2,7 +2,6 @@
#include <RmlUi/Core.h>
#include "aurora/lib/logging.hpp"
#include "aurora/rmlui.hpp"
#include "button.hpp"
#include "magic_enum.hpp"
@@ -184,10 +183,6 @@ bool Window::handle_tab_bar_nav(Rml::Event& event, NavCommand cmd) noexcept {
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
if (cmd == NavCommand::Up || cmd == NavCommand::Cancel) {
return focus_active_tab();
} else if (cmd == NavCommand::Down) {
if (!mContentComponents.empty()) {
return mContentComponents.front()->focus();
}
} else if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
int currentComponent = -1;
for (int i = 0; i < mContentComponents.size(); ++i) {
+2 -2
View File
@@ -590,9 +590,9 @@ int game_main(int argc, char* argv[]) {
// TODO: just for testing
auto& editorWindow = dusk::ui::add_window(std::make_unique<dusk::ui::EditorWindow>());
editorWindow.show();
// editorWindow.show();
auto& settingsWindow = dusk::ui::add_window(std::make_unique<dusk::ui::SettingsWindow>());
// settingsWindow.show();
settingsWindow.show();
std::string dvd_path;
bool dvd_opened = false;