mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-26 00:14:33 -04:00
Attempted to start making the save editor functional
This commit is contained in:
@@ -77,6 +77,23 @@ body {
|
||||
border-right: 1dp #92875B;
|
||||
}
|
||||
|
||||
.window .content .pane.detail-pane {
|
||||
gap: 16dp;
|
||||
}
|
||||
|
||||
.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 .detail-controls {
|
||||
display: flex;
|
||||
gap: 12dp;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
|
||||
+172
-76
@@ -2,72 +2,11 @@
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
#include "fmt/format.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kPlayerStatusContent = R"RML(
|
||||
<div class="pane">
|
||||
<div class="section-heading">Player</div>
|
||||
<button class="select-button">
|
||||
<div class="key">Player Name</div>
|
||||
<div class="value">Link</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Horse Name</div>
|
||||
<div class="value">Epona</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Max Health</div>
|
||||
<div class="value">15</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Health</div>
|
||||
<div class="value">12</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Max Oil</div>
|
||||
<div class="value">0</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Oil</div>
|
||||
<div class="value">0</div>
|
||||
</button>
|
||||
<div class="section-heading">Equipment</div>
|
||||
<button class="select-button selected">
|
||||
<div class="key">Equip X</div>
|
||||
<div class="value">Spinner</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Equip Y</div>
|
||||
<div class="value">None</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Combo Equip X</div>
|
||||
<div class="value">None</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Combo Equip Y</div>
|
||||
<div class="value">None</div>
|
||||
</button>
|
||||
<button class="select-button">
|
||||
<div class="key">Clothes</div>
|
||||
<div class="value">Hero's Clothes</div>
|
||||
</button>
|
||||
</div>
|
||||
<!-- TODO: right pane is going to be dynamic based on the highlighted left pane value -->
|
||||
<div class="pane">
|
||||
<button class="button">Slot 0 (Gale Boomerang)</button>
|
||||
<button class="button">Slot 1 (Lantern)</button>
|
||||
<button class="button">Slot 2 (Spinner)</button>
|
||||
<button class="button">Slot 3 (Iron Boots)</button>
|
||||
<button class="button">Slot 4 (Hero's Bow)</button>
|
||||
<button class="button">Slot 5 (Hawkeye)</button>
|
||||
<button class="button">Slot 6 (Ball and Chain)</button>
|
||||
<button class="button">Slot 7 (None)</button>
|
||||
<button class="button">Slot 8 (Dominion Rod)</button>
|
||||
</div>
|
||||
)RML";
|
||||
|
||||
const Rml::String kLocationContent = R"RML(
|
||||
<div class="pane">
|
||||
<div class="section-heading">Save Location</div>
|
||||
@@ -92,21 +31,178 @@ const Rml::String kLocationContent = R"RML(
|
||||
<div class="pane"></div>
|
||||
)RML";
|
||||
|
||||
bool has_save_data() {
|
||||
return dComIfGs_getSaveData() != nullptr;
|
||||
}
|
||||
|
||||
dSv_player_status_a_c* get_player_status() {
|
||||
if (!has_save_data()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &dComIfGs_getSaveData()->getPlayer().getPlayerStatusA();
|
||||
}
|
||||
|
||||
Rml::String get_player_name() {
|
||||
if (!has_save_data()) {
|
||||
return nullptr;
|
||||
}
|
||||
return dComIfGs_getPlayerName();
|
||||
}
|
||||
|
||||
Rml::String get_horse_name() {
|
||||
if (!has_save_data()) {
|
||||
return nullptr;
|
||||
}
|
||||
return dComIfGs_getHorseName();
|
||||
}
|
||||
|
||||
Rml::String value_for_player_selection(const Rml::String& selection) {
|
||||
dSv_player_status_a_c* status = get_player_status();
|
||||
if (selection == "get_horse_name") {
|
||||
return get_horse_name();
|
||||
}
|
||||
if (status == nullptr) {
|
||||
return "?";
|
||||
}
|
||||
if (selection == "max_health") {
|
||||
return fmt::format("{}", static_cast<u16>(status->mMaxLife));
|
||||
}
|
||||
if (selection == "health") {
|
||||
return fmt::format("{}", static_cast<u16>(status->mLife));
|
||||
}
|
||||
if (selection == "max_oil") {
|
||||
return fmt::format("{}", static_cast<u16>(status->mMaxOil));
|
||||
}
|
||||
if (selection == "oil") {
|
||||
return fmt::format("{}", static_cast<u16>(status->mOil));
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
Rml::String make_select_row(std::string_view key, std::string_view label, const Rml::String& value, const Rml::String& activeSelection) {
|
||||
const char* selectedClass = key == activeSelection ? " selected" : "";
|
||||
return fmt::format(
|
||||
"<button class=\"select-button{0}\" data-event-click=\"set_active_selection('{1}')\">"
|
||||
"<div class=\"key\">{2}</div><div class=\"value\">{3}</div></button>",
|
||||
selectedClass,
|
||||
key,
|
||||
label,
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
Rml::String make_numeric_detail(std::string_view label, std::string_view decAction, std::string_view incAction) {
|
||||
return fmt::format(
|
||||
"<div class=\"pane detail-pane\">"
|
||||
"<div class=\"section-heading\">{0}</div>"
|
||||
"<div class=\"detail-controls\">"
|
||||
"<button class=\"button\" data-event-click=\"window_action('{1}')\">-1</button>"
|
||||
"<button class=\"button\" data-event-click=\"window_action('{2}')\">+1</button>"
|
||||
"</div>"
|
||||
"</div>",
|
||||
label,
|
||||
decAction,
|
||||
incAction
|
||||
);
|
||||
}
|
||||
|
||||
template <typename TValue>
|
||||
void adjust_u16(TValue& value, int delta, u16 minValue, u16 maxValue) {
|
||||
const int nextValue = std::clamp(static_cast<int>(value) + delta, static_cast<int>(minValue), static_cast<int>(maxValue));
|
||||
value = static_cast<u16>(nextValue);
|
||||
}
|
||||
|
||||
void render_player_status_tab(Rml::Element* content, const Rml::String& activeSelection) {
|
||||
Rml::String leftPane = R"RML(<div class="pane"><div class="section-heading">Player</div>)RML";
|
||||
leftPane += make_select_row("player_name", "Player Name", get_player_name(), activeSelection);
|
||||
leftPane += make_select_row("horse_name", "Horse Name", get_horse_name(), activeSelection);
|
||||
leftPane += make_select_row("max_health", "Max Health", value_for_player_selection("max_health"), activeSelection);
|
||||
leftPane += make_select_row("health", "Health", value_for_player_selection("health"), activeSelection);
|
||||
leftPane += make_select_row("max_oil", "Max Oil", value_for_player_selection("max_oil"), activeSelection);
|
||||
leftPane += make_select_row("oil", "Oil", value_for_player_selection("oil"), activeSelection);
|
||||
leftPane += "</div>";
|
||||
|
||||
Rml::String rightPane;
|
||||
if (activeSelection == "max_health") {
|
||||
rightPane = make_numeric_detail("Max Health", "max_health.dec", "max_health.inc");
|
||||
} else if (activeSelection == "health") {
|
||||
rightPane = make_numeric_detail("Health", "health.dec", "health.inc");
|
||||
} else if (activeSelection == "max_oil") {
|
||||
rightPane = make_numeric_detail("Max Oil", "max_oil.dec", "max_oil.inc");
|
||||
} else if (activeSelection == "oil") {
|
||||
rightPane = make_numeric_detail("Oil", "oil.dec", "oil.inc");
|
||||
}
|
||||
|
||||
Rml::Factory::InstanceElementText(content, leftPane + rightPane);
|
||||
}
|
||||
|
||||
bool handle_editor_action(const Rml::VariantList& arguments) {
|
||||
if (arguments.empty() || !has_save_data()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Rml::String action = arguments[0].Get<Rml::String>();
|
||||
dSv_player_status_a_c* status = get_player_status();
|
||||
if (status == nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == "max_health.inc") {
|
||||
adjust_u16(status->mMaxLife, 1, 0, 0xFFFF);
|
||||
return true;
|
||||
} else if (action == "max_health.dec") {
|
||||
adjust_u16(status->mMaxLife, -1, 0, 0xFFFF);
|
||||
if (status->mLife > status->mMaxLife) {
|
||||
status->mLife = status->mMaxLife;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == "health.inc") {
|
||||
adjust_u16(status->mLife, 1, 0, status->mMaxLife);
|
||||
return true;
|
||||
} else if (action == "health.dec") {
|
||||
adjust_u16(status->mLife, -1, 0, status->mMaxLife);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == "max_oil.inc") {
|
||||
adjust_u16(status->mMaxOil, 1, 0, 0xFFFF);
|
||||
return true;
|
||||
} else if (action == "max_oil.dec") {
|
||||
adjust_u16(status->mMaxOil, -1, 0, 0xFFFF);
|
||||
if (status->mOil > status->mMaxOil) {
|
||||
status->mOil = status->mMaxOil;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (action == "oil.inc") {
|
||||
adjust_u16(status->mOil, 1, 0, status->mMaxOil);
|
||||
return true;
|
||||
} else if (action == "oil.dec") {
|
||||
adjust_u16(status->mOil, -1, 0, status->mMaxOil);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EditorWindow::EditorWindow()
|
||||
: Window({.tabs = {
|
||||
{"Player Status",
|
||||
[](Rml::Element* content) {
|
||||
// TODO: actually bind values and events. wonder if we should have
|
||||
// a SettingsPane element or something for sharing?
|
||||
Rml::Factory::InstanceElementText(content, kPlayerStatusContent);
|
||||
}},
|
||||
{"Location",
|
||||
[](Rml::Element* content) {
|
||||
Rml::Factory::InstanceElementText(content, kLocationContent);
|
||||
}},
|
||||
{"Inventory"},
|
||||
}}) {}
|
||||
{"Player Status",
|
||||
"player_name",
|
||||
[](Rml::Element* content, const Rml::String& activeSelection) { render_player_status_tab(content, activeSelection);
|
||||
}},
|
||||
{"Location",
|
||||
"",
|
||||
[](Rml::Element* content, const Rml::String&) { Rml::Factory::InstanceElementText(content, kLocationContent);
|
||||
}},
|
||||
{"Inventory"},
|
||||
},
|
||||
.actionHandler = handle_editor_action
|
||||
}){}
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
+102
-14
@@ -25,12 +25,76 @@ bool setup_window_model(Rml::Context* context, WindowModel& model, Rml::DataMode
|
||||
|
||||
constructor.Bind("active_tab", &model.activeTab);
|
||||
constructor.Bind("tabs", &model.tabs);
|
||||
constructor.Bind("active_selection", &model.activeSelection);
|
||||
constructor.BindEventCallback("set_active_tab", &WindowModel::set_active_tab, &model);
|
||||
constructor.BindEventCallback("set_active_selection", &WindowModel::set_active_selection, &model);
|
||||
constructor.BindEventCallback("window_action", &WindowModel::handle_action, &model);
|
||||
|
||||
handle = constructor.GetModelHandle();
|
||||
return true;
|
||||
}
|
||||
|
||||
Rml::ElementDocument* get_document_from_event(Rml::Event& event) {
|
||||
auto* currentElem = event.GetCurrentElement();
|
||||
if (currentElem == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return currentElem->GetOwnerDocument();
|
||||
}
|
||||
|
||||
Rml::Element* get_content_element(Rml::ElementDocument* document) {
|
||||
if (document == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return document->GetElementById("content");
|
||||
}
|
||||
|
||||
void clear_children(Rml::Element* element) {
|
||||
if (element == nullptr) {
|
||||
return;
|
||||
}
|
||||
while (element->GetNumChildren() > 0) {
|
||||
element->RemoveChild(element->GetFirstChild());
|
||||
}
|
||||
}
|
||||
|
||||
void ensure_tab_selection_state(WindowModel& model) {
|
||||
if (model.tabSelections.size() < model.tabs.size()) {
|
||||
model.tabSelections.resize(model.tabs.size());
|
||||
}
|
||||
if (model.activeTab < 0 || model.activeTab >= static_cast<int>(model.tabs.size())) {
|
||||
model.activeTab = 0;
|
||||
}
|
||||
if (model.tabs.empty()) {
|
||||
model.activeSelection.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
Rml::String& tabSelection = model.tabSelections[model.activeTab];
|
||||
if (tabSelection.empty()) {
|
||||
tabSelection = model.tabs[model.activeTab].defaultSelection;
|
||||
}
|
||||
model.activeSelection = tabSelection;
|
||||
}
|
||||
|
||||
void render_active_tab_content(WindowModel& model, Rml::ElementDocument* document) {
|
||||
auto* content = get_content_element(document);
|
||||
if (content == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
clear_children(content);
|
||||
if (model.tabs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensure_tab_selection_state(model);
|
||||
const WindowTab& tab = model.tabs[model.activeTab];
|
||||
if (tab.setContent) {
|
||||
tab.setContent(content, model.activeSelection);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void WindowModel::set_active_tab(
|
||||
@@ -45,27 +109,43 @@ void WindowModel::set_active_tab(
|
||||
}
|
||||
|
||||
activeTab = tabIndex;
|
||||
ensure_tab_selection_state(*this);
|
||||
model.DirtyVariable("active_tab");
|
||||
model.DirtyVariable("active_selection");
|
||||
render_active_tab_content(*this, get_document_from_event(event));
|
||||
}
|
||||
|
||||
// Replace window content with new tab content
|
||||
auto* currentElem = event.GetCurrentElement();
|
||||
if (currentElem == nullptr) {
|
||||
void WindowModel::set_active_selection(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) {
|
||||
if (arguments.empty() || tabs.empty()) {
|
||||
return;
|
||||
}
|
||||
auto* doc = currentElem->GetOwnerDocument();
|
||||
if (doc == nullptr) {
|
||||
|
||||
const Rml::String selection = arguments[0].Get<Rml::String>();
|
||||
ensure_tab_selection_state(*this);
|
||||
if (activeSelection == selection) {
|
||||
return;
|
||||
}
|
||||
auto* content = doc->GetElementById("content");
|
||||
if (content == nullptr) {
|
||||
|
||||
activeSelection = selection;
|
||||
tabSelections[activeTab] = selection;
|
||||
model.DirtyVariable("active_selection");
|
||||
render_active_tab_content(*this, get_document_from_event(event));
|
||||
}
|
||||
|
||||
void WindowModel::handle_action(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments) {
|
||||
bool shouldRerender = true;
|
||||
if (actionHandler) {
|
||||
shouldRerender = actionHandler(arguments);
|
||||
}
|
||||
if (!shouldRerender) {
|
||||
return;
|
||||
}
|
||||
while (content->GetNumChildren() > 0) {
|
||||
content->RemoveChild(content->GetFirstChild());
|
||||
}
|
||||
if (tabs[tabIndex].setContent) {
|
||||
tabs[tabIndex].setContent(content);
|
||||
}
|
||||
|
||||
model.DirtyVariable("active_tab");
|
||||
model.DirtyVariable("active_selection");
|
||||
render_active_tab_content(*this, get_document_from_event(event));
|
||||
}
|
||||
|
||||
Window::Window(WindowModel model) : mModel(std::move(model)) {
|
||||
@@ -73,12 +153,20 @@ Window::Window(WindowModel model) : mModel(std::move(model)) {
|
||||
if (context == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
setup_window_model(context, mModel, mModelHandle);
|
||||
|
||||
mDocument = context->LoadDocument("res/rml/window.rml");
|
||||
if (mDocument == nullptr) {
|
||||
return;
|
||||
}
|
||||
mModel.tabs[0].setContent(mDocument->GetElementById("content"));
|
||||
|
||||
ensure_tab_selection_state(mModel);
|
||||
render_active_tab();
|
||||
}
|
||||
|
||||
void Window::render_active_tab() noexcept {
|
||||
render_active_tab_content(mModel, mDocument);
|
||||
}
|
||||
|
||||
Window::~Window() {
|
||||
|
||||
+13
-3
@@ -7,15 +7,23 @@ namespace dusk::ui {
|
||||
|
||||
struct WindowTab {
|
||||
Rml::String label;
|
||||
std::function<void(Rml::Element*)> setContent;
|
||||
Rml::String defaultSelection;
|
||||
std::function<void(Rml::Element*, const Rml::String&)> setContent;
|
||||
};
|
||||
|
||||
struct WindowModel {
|
||||
int activeTab = 0;
|
||||
Rml::String activeSelection;
|
||||
std::vector<WindowTab> tabs;
|
||||
std::vector<Rml::String> tabSelections;
|
||||
std::function<bool(const Rml::VariantList&)> actionHandler;
|
||||
|
||||
void set_active_tab(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
|
||||
void set_active_selection(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
|
||||
void handle_action(
|
||||
Rml::DataModelHandle model, Rml::Event& event, const Rml::VariantList& arguments);
|
||||
};
|
||||
|
||||
class Window {
|
||||
@@ -27,9 +35,11 @@ public:
|
||||
void hide();
|
||||
|
||||
private:
|
||||
void render_active_tab() noexcept;
|
||||
|
||||
WindowModel mModel;
|
||||
Rml::DataModelHandle mModelHandle;
|
||||
Rml::ElementDocument* mDocument;
|
||||
Rml::ElementDocument* mDocument = nullptr;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
} // namespace dusk::ui
|
||||
|
||||
Reference in New Issue
Block a user