commit 727f4a09
Improve theme support (#40)
* Initial theme editor
Changed files
diff --git a/source/code/BlockGameSdl.hpp b/source/code/BlockGameSdl.hpp
index f93c3da..67e105e 100644
--- a/source/code/BlockGameSdl.hpp
+++ b/source/code/BlockGameSdl.hpp
@@ -519,7 +519,7 @@ public:
std::string strHolder;
strHolder = std::to_string(this->GetScore()+this->GetHandicap());
player_score.SetText(strHolder);
- player_score.Draw(globalData.screen, this->GetTopX()+border.score_label_offset.first,this->GetTopY()+border.score_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
+ player_score.Draw(globalData.screen, this->GetTopX()+border.score_field_offset.first,this->GetTopY()+border.score_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
if (this->GetAIenabled()) {
player_name.SetText(_("AI"));
}
@@ -570,14 +570,14 @@ public:
}
player_time.SetText(strHolder);
}
- player_time.Draw(globalData.screen, this->GetTopX()+border.time_label_offset.first,this->GetTopY()+border.time_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
+ player_time.Draw(globalData.screen, this->GetTopX()+border.time_field_offset.first,this->GetTopY()+border.time_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
strHolder = std::to_string(this->GetChains());
player_chain.SetText(strHolder);
- player_chain.Draw(globalData.screen, this->GetTopX()+border.chain_label_offset.first,this->GetTopY()+border.chain_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
+ player_chain.Draw(globalData.screen, this->GetTopX()+border.chain_field_offset.first,this->GetTopY()+border.chain_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
//drawspeedLevel:
strHolder = std::to_string(this->GetSpeedLevel());
player_speed.SetText(strHolder);
- player_speed.Draw(globalData.screen, this->GetTopX()+border.speed_label_offset.first,this->GetTopY()+border.speed_label_offset.second+22, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
+ player_speed.Draw(globalData.screen, this->GetTopX()+border.speed_field_offset.first,this->GetTopY()+border.speed_field_offset.second, sago::SagoTextField::Alignment::left, sago::SagoTextField::VerticalAlignment::top, &globalData.logicalResize);
if ((this->isStageClear()) &&(this->GetTopY()+700+50*(this->GetStageClearLimit()-this->GetLinesCleared())-this->GetPixels()-1<600+this->GetTopY())) {
oldBubleX = this->GetTopX()+280;
oldBubleY = this->GetTopY()+650+50*(this->GetStageClearLimit()-this->GetLinesCleared())-this->GetPixels()-1;
diff --git a/source/code/main.cpp b/source/code/main.cpp
index c9bea2d..52e78f0 100644
--- a/source/code/main.cpp
+++ b/source/code/main.cpp
@@ -70,6 +70,7 @@ https://blockattack.net
#include "editor/SagoTextureSelector.hpp"
#include "puzzle_editor/PuzzleEditorState.hpp"
+#include "theme_editor/ThemeEditorState.hpp"
#include "SagoImGui.hpp"
/*******************************************************************************
@@ -930,6 +931,7 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) {
("always-sixteen-nine", "Use 16:9 format even in Window mode")
("editor", "Start the sprite editor/browser")
("puzzle-editor", "Start the build in puzzle editor")
+ ("theme-editor", "Start the theme editor")
("puzzle-level-file", boost::program_options::value<std::string>(), "Sets the default puzzle file to load")
("puzzle-single-level", boost::program_options::value<int>(), "Start the specific puzzle level directly")
#ifdef REPLAY_IMPLEMENTED
@@ -1031,6 +1033,9 @@ static void ParseArguments(int argc, char* argv[], globalConfig& conf) {
if (vm.count("puzzle-editor")) {
puzzleEditor = true;
}
+ if (vm.count("theme-editor")) {
+ themeEditor = true;
+ }
if (vm.count("puzzle-level-file")) {
conf.puzzleName = vm["puzzle-level-file"].as<std::string>();
}
@@ -1335,6 +1340,17 @@ int main(int argc, char* argv[]) {
RunImGuiGameState(s);
ImGui::SaveIniSettingsToDisk(imgui_inifile.c_str());
}
+ else if (themeEditor) {
+ InitImGui(sdlWindow, renderer, globalData.xsize, globalData.ysize);
+ ImGuiIO& io = ImGui::GetIO();
+ io.IniFilename = nullptr;
+ std::string imgui_inifile = getPathToSaveFiles() + "/imgui_theme_editor.ini";
+ ImGui::LoadIniSettingsFromDisk(imgui_inifile.c_str());
+ ThemeEditorState s;
+ s.Init();
+ RunImGuiGameState(s);
+ ImGui::SaveIniSettingsToDisk(imgui_inifile.c_str());
+ }
else if (globalData.replayArgument.length()) {
ReplayPlayer rp;
RunGameState(rp);
diff --git a/source/code/mainVars.inc b/source/code/mainVars.inc
index f7c48a2..34f768e 100644
--- a/source/code/mainVars.inc
+++ b/source/code/mainVars.inc
@@ -92,6 +92,7 @@ bool twoPlayers; //True if two playerImGui_ImplSDL2_ProcessEvent(&event);s ar
static bool singlePuzzle = false;
static bool editor = false;
static bool puzzleEditor = false;
+static bool themeEditor = false;
static int singlePuzzleNr = 0;
static std::string singlePuzzleFile;
diff --git a/source/code/theme_editor/ThemeEditorState.cpp b/source/code/theme_editor/ThemeEditorState.cpp
new file mode 100644
index 0000000..e4541ce
--- /dev/null
+++ b/source/code/theme_editor/ThemeEditorState.cpp
@@ -0,0 +1,619 @@
+/*
+===========================================================================
+blockattack - Block Attack - Rise of the Blocks
+Copyright (C) 2005-2026 Poul Sander
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see http://www.gnu.org/licenses/
+
+Source information and contacts persons can be found at
+https://blockattack.net
+===========================================================================
+*/
+
+#include "ThemeEditorState.hpp"
+
+#include <fmt/core.h>
+#include <algorithm>
+
+#include "imgui.h"
+#include "imgui_internal.h"
+#include "backends/imgui_impl_sdl2.h"
+#include "backends/imgui_impl_sdlrenderer2.h"
+#include "../sago/SagoMisc.hpp"
+#include "../BlockGameSdl.hpp"
+
+ThemeEditorState::ThemeEditorState() {
+ current_border.name = "standard";
+ current_border.border_sprite = "board_back_back";
+ current_border.border_sprite_offset = {-60, -68};
+ current_border.score_label_offset = {310, 80};
+ current_border.time_label_offset = {310, 129};
+ current_border.chain_label_offset = {310, 178};
+ current_border.speed_label_offset = {310, 227};
+ current_border.score_field_offset = {310, 102};
+ current_border.time_field_offset = {310, 151};
+ current_border.chain_field_offset = {310, 200};
+ current_border.speed_field_offset = {310, 249};
+
+ current_background.name = "standard";
+ current_background.background_sprite = "background";
+ current_background.background_sprite_16x9 = "background_sixteen_nine";
+ current_background.background_scale = ImgScale::Stretch;
+}
+
+ThemeEditorState::~ThemeEditorState() {
+ // Restore the original theme when editor exits
+ globalData.theme = saved_theme;
+}
+
+bool ThemeEditorState::IsActive() {
+ return isActive;
+}
+
+void ThemeEditorState::ProcessInput(const SDL_Event& event, bool& processed) {
+ (void)event;
+ (void)processed;
+ // Handle any custom input processing here if needed
+}
+
+void ThemeEditorState::Init() {
+ // Initialize the theme system
+ ThemesInit();
+
+ // Populate lists
+ border_names = ThemesGetBorderNames();
+ background_names = ThemesGetBackgroundNames();
+ decoration_names = ThemesGetDecorationNames();
+
+ // Save current theme and set up preview
+ saved_theme = globalData.theme;
+ preview_theme = ThemesGet(0);
+
+ // Create preview game instance positioned in the right 40% of screen
+ // Editor takes 60% of width, so preview starts at 60% + some padding
+ int preview_x = static_cast<int>(globalData.xsize * 0.62f);
+ preview_game = std::make_shared<BlockGameSdl>(preview_x, 100, &globalData.spriteHolder->GetDataHolder());
+ preview_game->name = "Preview";
+ preview_game->putSampleBlocks();
+
+ // Initialize preview with current border/background
+ preview_theme.border = current_border;
+ preview_theme.background = current_background;
+ globalData.theme = preview_theme;
+}
+
+void ThemeEditorState::Update() {
+ // Update preview if needed
+ if (preview_needs_update) {
+ preview_needs_update = false;
+ UpdatePreview();
+ }
+}
+
+void ThemeEditorState::UpdatePreview() {
+ // Update preview theme based on current tab
+ switch (current_tab) {
+ case 0: // Border Editor
+ preview_theme.border = current_border;
+ break;
+ case 1: // Background Editor
+ preview_theme.background = current_background;
+ break;
+ case 2: // Theme Composer
+ // Combine selected components
+ if (static_cast<size_t>(selected_border_index) < border_names.size()) {
+ preview_theme.border = ThemesGetBorder(border_names[selected_border_index]);
+ }
+ if (static_cast<size_t>(selected_background_index) < background_names.size()) {
+ preview_theme.background = ThemesGetBackground(background_names[selected_background_index]);
+ }
+ preview_theme.back_board = board_backgrounds[selected_board_background];
+ break;
+ }
+
+ // Apply theme to global for preview rendering
+ globalData.theme = preview_theme;
+}
+
+void ThemeEditorState::Draw(SDL_Renderer* target) {
+ // Get actual physical renderer size and keep logicalResize in sync every frame
+ int display_w = 1;
+ int display_h = 1;
+ SDL_GetRendererOutputSize(target, &display_w, &display_h);
+ globalData.logicalResize.SetPhysicalSize(display_w, display_h);
+
+ // Main window - take up left 60% of screen to leave room for preview
+ const float editor_width = display_w * 0.5f;
+ ImGui::SetNextWindowPos(ImVec2(0, 0), ImGuiCond_Always);
+ ImGui::SetNextWindowSize(ImVec2(editor_width, (float)display_h), ImGuiCond_Always);
+
+ ImGui::Begin("Theme Editor", nullptr, ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse);
+
+ // Tab bar
+ if (ImGui::BeginTabBar("EditorTabs")) {
+ if (ImGui::BeginTabItem("Border Editor")) {
+ current_tab = 0;
+ DrawBorderEditor();
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginTabItem("Background Editor")) {
+ current_tab = 1;
+ DrawBackgroundEditor();
+ ImGui::EndTabItem();
+ }
+ if (ImGui::BeginTabItem("Theme Composer")) {
+ current_tab = 2;
+ DrawThemeComposer();
+ ImGui::EndTabItem();
+ }
+ ImGui::EndTabBar();
+ }
+
+ ImGui::Separator();
+ if (ImGui::Button("Exit Editor")) {
+ isActive = false;
+ }
+
+ ImGui::End();
+
+ // Draw the preview: convert physical position (ImGui space) to logical space for DoPaintJob
+ if (preview_game) {
+ const int preview_physical_x = static_cast<int>(editor_width) + 250;
+ const int preview_physical_y = 150;
+ int preview_logical_x = 0;
+ int preview_logical_y = 0;
+ globalData.logicalResize.PhysicalToLogical(preview_physical_x, preview_physical_y, preview_logical_x, preview_logical_y);
+ preview_game->SetTopXY(preview_logical_x, preview_logical_y);
+ preview_game->DoPaintJob();
+ }
+}
+
+void ThemeEditorState::DrawBorderEditor() {
+ ImGui::Text("Create and edit custom borders");
+ ImGui::Separator();
+
+ // Load existing border
+ ImGui::Text("Load existing border:");
+ if (ImGui::BeginCombo("##BorderLoad", border_names.empty() ? "No borders" : current_border.name.c_str())) {
+ for (const auto& name : border_names) {
+ bool is_selected = (current_border.name == name);
+ if (ImGui::Selectable(name.c_str(), is_selected)) {
+ LoadBorder(name);
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ImGui::Separator();
+ ImGui::Text("Border Settings:");
+
+ // Border name
+ ImGui::InputText("Border Name", border_name_buffer, sizeof(border_name_buffer));
+
+ // Border sprite
+ if (ImGui::InputText("Border Sprite", border_sprite_buffer, sizeof(border_sprite_buffer))) {
+ current_border.border_sprite = border_sprite_buffer;
+ preview_needs_update = true;
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("Examples: board_back_back, board_back_back_mirror");
+ }
+
+ // Validate sprite button
+ ImGui::SameLine();
+ if (ImGui::Button("Validate##border")) {
+ if (ValidateSprite(border_sprite_buffer)) {
+ ImGui::OpenPopup("Valid Sprite");
+ } else {
+ ImGui::OpenPopup("Invalid Sprite");
+ }
+ }
+
+ if (ImGui::BeginPopup("Valid Sprite")) {
+ ImGui::Text("Sprite exists!");
+ ImGui::EndPopup();
+ }
+ if (ImGui::BeginPopup("Invalid Sprite")) {
+ ImGui::Text("Warning: Sprite does not exist!");
+ ImGui::EndPopup();
+ }
+
+ // Border sprite offset
+ if (ImGui::DragInt2("Border Sprite Offset", &border_offset_x)) {
+ current_border.border_sprite_offset = {border_offset_x, border_offset_y};
+ preview_needs_update = true;
+ }
+
+ // Label offsets
+ ImGui::Separator();
+ ImGui::Text("Label Positions:");
+ if (ImGui::DragInt2("Score Label Offset", &score_offset_x)) {
+ current_border.score_label_offset = {score_offset_x, score_offset_y};
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt2("Time Label Offset", &time_offset_x)) {
+ current_border.time_label_offset = {time_offset_x, time_offset_y};
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt2("Chain Label Offset", &chain_offset_x)) {
+ current_border.chain_label_offset = {chain_offset_x, chain_offset_y};
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt2("Speed Label Offset", &speed_offset_x)) {
+ current_border.speed_label_offset = {speed_offset_x, speed_offset_y};
+ preview_needs_update = true;
+ }
+
+ // Field offsets
+ ImGui::Separator();
+ ImGui::Text("Field Positions:");
+ if (ImGui::DragInt2("Score Field Offset", &score_field_x)) {
+ current_border.score_field_offset = {score_field_x, score_field_y};
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt2("Time Field Offset", &time_field_x)) {
+ current_border.time_field_offset = {time_field_x, time_field_y};
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt2("Chain Field Offset", &chain_field_x)) {
+ current_border.chain_field_offset = {chain_field_x, chain_field_y};
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt2("Speed Field Offset", &speed_field_x)) {
+ current_border.speed_field_offset = {speed_field_x, speed_field_y};
+ preview_needs_update = true;
+ }
+
+ ImGui::Separator();
+ if (ImGui::Button("Save Border")) {
+ SaveCurrentBorder();
+ }
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: borders/<name>.json");
+}
+
+void ThemeEditorState::DrawBackgroundEditor() {
+ ImGui::Text("Create and edit custom backgrounds");
+ ImGui::Separator();
+
+ // Load existing background
+ ImGui::Text("Load existing background:");
+ if (ImGui::BeginCombo("##BackgroundLoad", background_names.empty() ? "No backgrounds" : current_background.name.c_str())) {
+ for (const auto& name : background_names) {
+ bool is_selected = (current_background.name == name);
+ if (ImGui::Selectable(name.c_str(), is_selected)) {
+ LoadBackground(name);
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ImGui::Separator();
+ ImGui::Text("Background Settings:");
+
+ // Background name
+ ImGui::InputText("Background Name", background_name_buffer, sizeof(background_name_buffer));
+
+ // Background sprite
+ if (ImGui::InputText("Background Sprite", background_sprite_buffer, sizeof(background_sprite_buffer))) {
+ current_background.background_sprite = background_sprite_buffer;
+ preview_needs_update = true;
+ }
+ if (ImGui::IsItemHovered()) {
+ ImGui::SetTooltip("Examples: background, background_autumn, custom_background_<filename>");
+ }
+
+ // Validate sprite button
+ ImGui::SameLine();
+ if (ImGui::Button("Validate##background")) {
+ if (ValidateSprite(background_sprite_buffer)) {
+ ImGui::OpenPopup("Valid BG Sprite");
+ } else {
+ ImGui::OpenPopup("Invalid BG Sprite");
+ }
+ }
+
+ if (ImGui::BeginPopup("Valid BG Sprite")) {
+ ImGui::Text("Sprite exists!");
+ ImGui::EndPopup();
+ }
+ if (ImGui::BeginPopup("Invalid BG Sprite")) {
+ ImGui::Text("Warning: Sprite does not exist!");
+ ImGui::EndPopup();
+ }
+
+ // 16:9 background sprite (optional)
+ if (ImGui::InputText("Background Sprite 16:9 (optional)", background_sprite_16x9_buffer, sizeof(background_sprite_16x9_buffer))) {
+ current_background.background_sprite_16x9 = background_sprite_16x9_buffer;
+ preview_needs_update = true;
+ }
+
+ // Scaling mode
+ ImGui::Separator();
+ ImGui::Text("Scaling Mode:");
+ const char* scale_modes[] = { "Stretch", "Tile", "Resize", "Cut" };
+ if (ImGui::Combo("##ScaleMode", &background_scale_mode, scale_modes, 4)) {
+ switch (background_scale_mode) {
+ case 0: current_background.background_scale = ImgScale::Stretch; break;
+ case 1: current_background.background_scale = ImgScale::Tile; break;
+ case 2: current_background.background_scale = ImgScale::Resize; break;
+ case 3: current_background.background_scale = ImgScale::Cut; break;
+ }
+ preview_needs_update = true;
+ }
+
+ // Tile animation speed
+ ImGui::Separator();
+ ImGui::Text("Tile Animation (0 = disabled):");
+ if (ImGui::DragInt("Move Speed X (ms)", &tile_move_speed_x, 1.0f, 0, 10000)) {
+ current_background.tileMoveSpeedX = tile_move_speed_x;
+ preview_needs_update = true;
+ }
+ if (ImGui::DragInt("Move Speed Y (ms)", &tile_move_speed_y, 1.0f, 0, 10000)) {
+ current_background.tileMoveSpeedY = tile_move_speed_y;
+ preview_needs_update = true;
+ }
+
+ ImGui::Separator();
+ if (ImGui::Button("Save Background")) {
+ SaveCurrentBackground();
+ }
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: backgrounds/<name>.json");
+}
+
+void ThemeEditorState::DrawThemeComposer() {
+ ImGui::Text("Combine borders and backgrounds into a complete theme");
+ ImGui::Separator();
+
+ // Theme name
+ ImGui::InputText("Theme Name", theme_name_buffer, sizeof(theme_name_buffer));
+
+ // Border selection
+ ImGui::Separator();
+ ImGui::Text("Select Border:");
+ if (ImGui::BeginCombo("##BorderSelect", static_cast<size_t>(selected_border_index) < border_names.size() ? border_names[selected_border_index].c_str() : "")) {
+ for (size_t i = 0; i < border_names.size(); i++) {
+ bool is_selected = (static_cast<size_t>(selected_border_index) == i);
+ if (ImGui::Selectable(border_names[i].c_str(), is_selected)) {
+ selected_border_index = i;
+ preview_needs_update = true;
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ // Background selection
+ ImGui::Text("Select Background:");
+ if (ImGui::BeginCombo("##BackgroundSelect", static_cast<size_t>(selected_background_index) < background_names.size() ? background_names[selected_background_index].c_str() : "")) {
+ for (size_t i = 0; i < background_names.size(); i++) {
+ bool is_selected = (static_cast<size_t>(selected_background_index) == i);
+ if (ImGui::Selectable(background_names[i].c_str(), is_selected)) {
+ selected_background_index = i;
+ preview_needs_update = true;
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ // Decoration selection
+ ImGui::Text("Select Decoration:");
+ if (ImGui::BeginCombo("##DecorationSelect", static_cast<size_t>(selected_decoration_index) < decoration_names.size() ? decoration_names[selected_decoration_index].c_str() : "")) {
+ for (size_t i = 0; i < decoration_names.size(); i++) {
+ bool is_selected = (static_cast<size_t>(selected_decoration_index) == i);
+ if (ImGui::Selectable(decoration_names[i].c_str(), is_selected)) {
+ selected_decoration_index = i;
+ preview_needs_update = true;
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ // Board background selection
+ ImGui::Text("Select Board Background:");
+ if (ImGui::BeginCombo("##BoardBGSelect", board_backgrounds[selected_board_background].c_str())) {
+ for (size_t i = 0; i < board_backgrounds.size(); i++) {
+ bool is_selected = (static_cast<size_t>(selected_board_background) == i);
+ if (ImGui::Selectable(board_backgrounds[i].c_str(), is_selected)) {
+ selected_board_background = i;
+ preview_needs_update = true;
+ }
+ if (is_selected) {
+ ImGui::SetItemDefaultFocus();
+ }
+ }
+ ImGui::EndCombo();
+ }
+
+ ImGui::Separator();
+ if (ImGui::Button("Save Theme")) {
+ SaveCurrentTheme();
+ }
+ ImGui::SameLine();
+ ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Saved to: custom_themes.json");
+}
+
+void ThemeEditorState::DrawPreview(SDL_Renderer* target) {
+ (void)target;
+ // TODO: Implement preview rendering using BlockGameSdl
+}
+
+void ThemeEditorState::SaveCurrentBorder() {
+ // Update current_border from UI fields
+ std::string name = border_name_buffer;
+ if (name.empty()) {
+ std::cerr << "Error: Border name cannot be empty\n";
+ return;
+ }
+
+ current_border.name = name;
+ current_border.border_sprite = border_sprite_buffer;
+ current_border.border_sprite_offset = {border_offset_x, border_offset_y};
+ current_border.score_label_offset = {score_offset_x, score_offset_y};
+ current_border.time_label_offset = {time_offset_x, time_offset_y};
+ current_border.chain_label_offset = {chain_offset_x, chain_offset_y};
+ current_border.speed_label_offset = {speed_offset_x, speed_offset_y};
+ current_border.score_field_offset = {score_field_x, score_field_y};
+ current_border.time_field_offset = {time_field_x, time_field_y};
+ current_border.chain_field_offset = {chain_field_x, chain_field_y};
+ current_border.speed_field_offset = {speed_field_x, speed_field_y};
+
+ // Validate sprite
+ if (!ValidateSprite(current_border.border_sprite)) {
+ std::cerr << "Warning: Saving border with non-existent sprite: " << current_border.border_sprite << "\n";
+ }
+
+ // Save to file
+ ThemesSaveBorderData(name, current_border);
+ std::cout << "Saved border: " << name << "\n";
+
+ // Refresh border list
+ border_names = ThemesGetBorderNames();
+}
+
+void ThemeEditorState::SaveCurrentBackground() {
+ // Update current_background from UI fields
+ std::string name = background_name_buffer;
+ if (name.empty()) {
+ std::cerr << "Error: Background name cannot be empty\n";
+ return;
+ }
+
+ current_background.name = name;
+ current_background.background_sprite = background_sprite_buffer;
+ current_background.background_sprite_16x9 = background_sprite_16x9_buffer;
+
+ // Convert scale mode
+ switch (background_scale_mode) {
+ case 0: current_background.background_scale = ImgScale::Stretch; break;
+ case 1: current_background.background_scale = ImgScale::Tile; break;
+ case 2: current_background.background_scale = ImgScale::Resize; break;
+ case 3: current_background.background_scale = ImgScale::Cut; break;
+ }
+
+ current_background.tileMoveSpeedX = tile_move_speed_x;
+ current_background.tileMoveSpeedY = tile_move_speed_y;
+
+ // Validate sprite
+ if (!ValidateSprite(current_background.background_sprite)) {
+ std::cerr << "Warning: Saving background with non-existent sprite: " << current_background.background_sprite << "\n";
+ }
+
+ // Save to file
+ ThemesSaveBackgroundData(name, current_background);
+ std::cout << "Saved background: " << name << "\n";
+
+ // Refresh background list
+ background_names = ThemesGetBackgroundNames();
+}
+
+void ThemeEditorState::SaveCurrentTheme() {
+ std::string name = theme_name_buffer;
+ if (name.empty()) {
+ std::cerr << "Error: Theme name cannot be empty\n";
+ return;
+ }
+
+ Theme theme;
+ theme.theme_name = name;
+
+ // Set components
+ if (static_cast<size_t>(selected_border_index) < border_names.size()) {
+ theme.border = ThemesGetBorder(border_names[selected_border_index]);
+ }
+ if (static_cast<size_t>(selected_background_index) < background_names.size()) {
+ theme.background = ThemesGetBackground(background_names[selected_background_index]);
+ }
+ theme.back_board = board_backgrounds[selected_board_background];
+
+ // Add or replace the theme
+ ThemesAddOrReplace(theme);
+
+ // Save to custom themes file
+ ThemesSaveCustomSlots();
+
+ std::cout << "Saved theme: " << name << "\n";
+}
+
+void ThemeEditorState::LoadBorder(const std::string& name) {
+ current_border = ThemesGetBorder(name);
+
+ // Update UI fields
+ strncpy(border_name_buffer, current_border.name.c_str(), sizeof(border_name_buffer) - 1);
+ strncpy(border_sprite_buffer, current_border.border_sprite.c_str(), sizeof(border_sprite_buffer) - 1);
+ border_offset_x = current_border.border_sprite_offset.first;
+ border_offset_y = current_border.border_sprite_offset.second;
+ score_offset_x = current_border.score_label_offset.first;
+ score_offset_y = current_border.score_label_offset.second;
+ time_offset_x = current_border.time_label_offset.first;
+ time_offset_y = current_border.time_label_offset.second;
+ chain_offset_x = current_border.chain_label_offset.first;
+ chain_offset_y = current_border.chain_label_offset.second;
+ speed_offset_x = current_border.speed_label_offset.first;
+ speed_offset_y = current_border.speed_label_offset.second;
+ score_field_x = current_border.score_field_offset.first;
+ score_field_y = current_border.score_field_offset.second;
+ time_field_x = current_border.time_field_offset.first;
+ time_field_y = current_border.time_field_offset.second;
+ chain_field_x = current_border.chain_field_offset.first;
+ chain_field_y = current_border.chain_field_offset.second;
+ speed_field_x = current_border.speed_field_offset.first;
+ speed_field_y = current_border.speed_field_offset.second;
+ preview_needs_update = true;
+}
+
+void ThemeEditorState::LoadBackground(const std::string& name) {
+ current_background = ThemesGetBackground(name);
+
+ // Update UI fields
+ strncpy(background_name_buffer, current_background.name.c_str(), sizeof(background_name_buffer) - 1);
+ strncpy(background_sprite_buffer, current_background.background_sprite.c_str(), sizeof(background_sprite_buffer) - 1);
+ strncpy(background_sprite_16x9_buffer, current_background.background_sprite_16x9.c_str(), sizeof(background_sprite_16x9_buffer) - 1);
+
+ // Convert scale mode
+ switch (current_background.background_scale) {
+ case ImgScale::Stretch: background_scale_mode = 0; break;
+ case ImgScale::Tile: background_scale_mode = 1; break;
+ case ImgScale::Resize: background_scale_mode = 2; break;
+ case ImgScale::Cut: background_scale_mode = 3; break;
+ }
+
+ tile_move_speed_x = current_background.tileMoveSpeedX;
+ tile_move_speed_y = current_background.tileMoveSpeedY;
+ preview_needs_update = true;
+}
+
+bool ThemeEditorState::ValidateSprite(const std::string& sprite_name) {
+ return ThemesValidateSpriteName(sprite_name);
+}
+
+std::vector<std::string> ThemeEditorState::GetAvailableSprites() {
+ // TODO: Implement getting available sprite names from globalData.spriteHolder
+ return std::vector<std::string>();
+}
diff --git a/source/code/theme_editor/ThemeEditorState.hpp b/source/code/theme_editor/ThemeEditorState.hpp
new file mode 100644
index 0000000..9152dda
--- /dev/null
+++ b/source/code/theme_editor/ThemeEditorState.hpp
@@ -0,0 +1,116 @@
+/*
+===========================================================================
+blockattack - Block Attack - Rise of the Blocks
+Copyright (C) 2005-2026 Poul Sander
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see http://www.gnu.org/licenses/
+
+Source information and contacts persons can be found at
+https://blockattack.net
+===========================================================================
+*/
+
+#pragma once
+
+#include "../global.hpp"
+#include "../sago/GameStateInterface.hpp"
+#include "../themes.hpp"
+#include <string>
+#include <vector>
+#include <memory>
+
+class BlockGameSdl;
+
+class ThemeEditorState : public sago::GameStateInterface {
+public:
+ ThemeEditorState();
+ ThemeEditorState(const ThemeEditorState& orig) = delete;
+ virtual ~ThemeEditorState();
+
+ void Init();
+
+ bool IsActive() override;
+ void ProcessInput(const SDL_Event& event, bool& processed) override;
+ void Draw(SDL_Renderer* target) override;
+ void Update() override;
+
+private:
+ void DrawBorderEditor();
+ void DrawBackgroundEditor();
+ void DrawThemeComposer();
+ void DrawPreview(SDL_Renderer* target);
+
+ void SaveCurrentBorder();
+ void SaveCurrentBackground();
+ void SaveCurrentTheme();
+
+ void LoadBorder(const std::string& name);
+ void LoadBackground(const std::string& name);
+
+ bool ValidateSprite(const std::string& sprite_name);
+ std::vector<std::string> GetAvailableSprites();
+
+ bool isActive = true;
+ int current_tab = 0; // 0=Border, 1=Background, 2=Theme
+
+ // Border editor state
+ ThemeBorderData current_border;
+ char border_name_buffer[256] = "";
+ char border_sprite_buffer[256] = "board_back_back";
+ int border_offset_x = -60;
+ int border_offset_y = -68;
+ int score_offset_x = 310;
+ int score_offset_y = 80;
+ int time_offset_x = 310;
+ int time_offset_y = 129;
+ int chain_offset_x = 310;
+ int chain_offset_y = 178;
+ int speed_offset_x = 310;
+ int speed_offset_y = 227;
+ int score_field_x = 310;
+ int score_field_y = 102;
+ int time_field_x = 310;
+ int time_field_y = 151;
+ int chain_field_x = 310;
+ int chain_field_y = 200;
+ int speed_field_x = 310;
+ int speed_field_y = 249;
+
+ // Background editor state
+ BackGroundData current_background;
+ char background_name_buffer[256] = "";
+ char background_sprite_buffer[256] = "background";
+ char background_sprite_16x9_buffer[256] = "background_sixteen_nine";
+ int background_scale_mode = 0; // 0=Stretch, 1=Tile, 2=Resize, 3=Cut
+ int tile_move_speed_x = 0;
+ int tile_move_speed_y = 0;
+
+ // Theme composer state
+ char theme_name_buffer[256] = "custom_theme";
+ int selected_border_index = 0;
+ int selected_background_index = 0;
+ int selected_decoration_index = 0;
+ int selected_board_background = 0; // 0=back_board, 1=back_board_sample_snow, 2=trans_cover
+ std::vector<std::string> border_names;
+ std::vector<std::string> background_names;
+ std::vector<std::string> decoration_names;
+ const std::vector<std::string> board_backgrounds = {"back_board", "back_board_sample_snow", "trans_cover"};
+
+ // Preview
+ std::shared_ptr<BlockGameSdl> preview_game;
+ Theme preview_theme;
+ Theme saved_theme; // Backup of the global theme
+ bool preview_needs_update = true;
+ void UpdatePreview();
+};
diff --git a/source/code/themes.cpp b/source/code/themes.cpp
index 5a3645b..cd36719 100644
--- a/source/code/themes.cpp
+++ b/source/code/themes.cpp
@@ -24,6 +24,7 @@ https://www.blockattack.net
#include "themes.hpp"
#include "sago/SagoMisc.hpp"
+#include "os.hpp"
#include <vector>
#include <sstream>
#include <map>
@@ -82,6 +83,22 @@ void to_json(json& j, const BackGroundData& p) {
j = json{ {"background_name", p.name}, {"background_sprite", p.background_sprite}, {"background_sprite_16x9", p.background_sprite_16x9}, {"background_scale", p.background_scale}, {"tileMoveSpeedX", p.tileMoveSpeedX}, {"tileMoveSpeedY", p.tileMoveSpeedY} };
}
+void to_json(json& j, const ThemeBorderData& p) {
+ j = json{
+ {"border_name", p.name},
+ {"border_sprite", p.border_sprite},
+ {"border_sprite_offset", {p.border_sprite_offset.first, p.border_sprite_offset.second}},
+ {"score_label_offset", {p.score_label_offset.first, p.score_label_offset.second}},
+ {"time_label_offset", {p.time_label_offset.first, p.time_label_offset.second}},
+ {"chain_label_offset", {p.chain_label_offset.first, p.chain_label_offset.second}},
+ {"speed_label_offset", {p.speed_label_offset.first, p.speed_label_offset.second}},
+ {"score_field_offset", {p.score_field_offset.first, p.score_field_offset.second}},
+ {"time_field_offset", {p.time_field_offset.first, p.time_field_offset.second}},
+ {"chain_field_offset", {p.chain_field_offset.first, p.chain_field_offset.second}},
+ {"speed_field_offset", {p.speed_field_offset.first, p.speed_field_offset.second}}
+ };
+}
+
void to_json(json& j, const Theme& p) {
j = json{ {"theme_name", p.theme_name}, {"back_board", p.back_board}, {"background_name", p.background.name}, {"decoration_name", p.decoration.name} };
}
@@ -91,7 +108,7 @@ void to_json(json& j, const DecorationData& p) {
}
void to_json(json& j, const ThemeFileData& p) {
- j = json{ {"background_data", p.background_data}, {"decoration_data", p.decoration_data}, {"themes", p.themes} };
+ j = json{ {"background_data", p.background_data}, {"decoration_data", p.decoration_data}, {"border_data", p.border_data}, {"themes", p.themes} };
}
void from_json(const json& j, BackGroundData& p) {
@@ -103,6 +120,47 @@ void from_json(const json& j, BackGroundData& p) {
j.at("tileMoveSpeedY").get_to(p.tileMoveSpeedY);
}
+void from_json(const json& j, ThemeBorderData& p) {
+ j.at("border_name").get_to(p.name);
+ j.at("border_sprite").get_to(p.border_sprite);
+ if (j.contains("border_sprite_offset")) {
+ auto offset = j.at("border_sprite_offset");
+ p.border_sprite_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("score_label_offset")) {
+ auto offset = j.at("score_label_offset");
+ p.score_label_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("time_label_offset")) {
+ auto offset = j.at("time_label_offset");
+ p.time_label_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("chain_label_offset")) {
+ auto offset = j.at("chain_label_offset");
+ p.chain_label_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("speed_label_offset")) {
+ auto offset = j.at("speed_label_offset");
+ p.speed_label_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("score_field_offset")) {
+ auto offset = j.at("score_field_offset");
+ p.score_field_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("time_field_offset")) {
+ auto offset = j.at("time_field_offset");
+ p.time_field_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("chain_field_offset")) {
+ auto offset = j.at("chain_field_offset");
+ p.chain_field_offset = {offset[0], offset[1]};
+ }
+ if (j.contains("speed_field_offset")) {
+ auto offset = j.at("speed_field_offset");
+ p.speed_field_offset = {offset[0], offset[1]};
+ }
+}
+
void from_json(const json& j, Theme& p) {
j.at("theme_name").get_to(p.theme_name);
j.at("back_board").get_to(p.back_board);
@@ -122,6 +180,9 @@ void from_json(const json& j, ThemeFileData& p) {
if (j.contains("decoration_data")) {
j.at("decoration_data").get_to(p.decoration_data);
}
+ if (j.contains("border_data")) {
+ j.at("border_data").get_to(p.border_data);
+ }
if (j.contains("themes")) {
j.at("themes").get_to(p.themes);
}
@@ -162,6 +223,9 @@ static void ThemesReadDataFromFile(const std::string& filename) {
for (const auto& dec : tfd.decoration_data) {
decoration_data[dec.name] = dec;
}
+ for (const auto& border : tfd.border_data) {
+ border_data[border.name] = border;
+ }
for (const Theme& theme : tfd.themes) {
if (globalData.verboseLevel) {
std::cout << "Adding theme " << theme.theme_name << "\n";
@@ -197,6 +261,10 @@ static void ThemesInitBorderData() {
mirror.time_label_offset = {-100,129};
mirror.chain_label_offset = {-100, 178};
mirror.speed_label_offset = {-100, 227};
+ mirror.score_field_offset = {-100, 102};
+ mirror.time_field_offset = {-100, 151};
+ mirror.chain_field_offset = {-100, 200};
+ mirror.speed_field_offset = {-100, 249};
border_data[mirror.name] = mirror;
}
@@ -213,6 +281,9 @@ static void ThemesDumpData() {
for (auto& pair : decoration_data) {
tfd.decoration_data.push_back(pair.second);
}
+ for (auto& pair : border_data) {
+ tfd.border_data.push_back(pair.second);
+ }
json j = tfd;
std::string s = j.dump(4);
sago::WriteFileContent("themes_dump.json", s);
@@ -252,6 +323,8 @@ void ThemesInit() {
ThemesInitBackGroundData();
ThemesInitCustomBackgrounds();
ThemesInitBorderData();
+ ThemesLoadCustomBorders();
+ ThemesLoadCustomBackgrounds();
themes.resize(1); //Add the default theme
ThemesFillMissingFields(themes[0]);
const std::vector<std::string>& theme_files = sago::GetFileList("themes");
@@ -395,4 +468,127 @@ void ThemesInitCustomBackgrounds() {
}
sprite_stream << "}\n";
sago::WriteFileContent("sprites/custom_backgrounds.sprite", sprite_stream.str());
+}
+
+void ThemesSaveBorderData(const std::string& name, const ThemeBorderData& data) {
+ ThemesInit();
+ // Create borders directory if it doesn't exist
+ OsCreateFolder("borders");
+
+ json j = data;
+ std::string s = j.dump(4);
+ std::string filename = fmt::format("borders/{}.json", name);
+ sago::WriteFileContent(filename.c_str(), s);
+
+ // Add to the border_data map
+ border_data[name] = data;
+}
+
+void ThemesSaveBackgroundData(const std::string& name, const BackGroundData& data) {
+ ThemesInit();
+ // Create backgrounds directory if it doesn't exist
+ OsCreateFolder("backgrounds");
+
+ json j = data;
+ std::string s = j.dump(4);
+ std::string filename = fmt::format("backgrounds/{}.json", name);
+ sago::WriteFileContent(filename.c_str(), s);
+
+ // Add to the background_data map
+ background_data[name] = data;
+}
+
+void ThemesLoadCustomBorders() {
+ const std::vector<std::string>& border_files = sago::GetFileList("borders");
+ for (const std::string& filename : border_files) {
+ if (boost::algorithm::ends_with(filename, ".json")) {
+ std::string filepath = fmt::format("borders/{}", filename);
+ std::string s = sago::GetFileContent(filepath);
+ if (s.empty()) {
+ continue;
+ }
+ try {
+ json j = json::parse(s);
+ ThemeBorderData border = j;
+ border_data[border.name] = border;
+ if (globalData.verboseLevel) {
+ std::cout << "Loaded custom border: " << border.name << "\n";
+ }
+ }
+ catch (const std::exception& e) {
+ std::cerr << "Error loading border file " << filepath << ": " << e.what() << "\n";
+ }
+ }
+ }
+}
+
+void ThemesLoadCustomBackgrounds() {
+ const std::vector<std::string>& background_files = sago::GetFileList("backgrounds");
+ for (const std::string& filename : background_files) {
+ if (boost::algorithm::ends_with(filename, ".json")) {
+ std::string filepath = fmt::format("backgrounds/{}", filename);
+ std::string s = sago::GetFileContent(filepath);
+ if (s.empty()) {
+ continue;
+ }
+ try {
+ json j = json::parse(s);
+ BackGroundData background = j;
+ background_data[background.name] = background;
+ if (globalData.verboseLevel) {
+ std::cout << "Loaded custom background: " << background.name << "\n";
+ }
+ }
+ catch (const std::exception& e) {
+ std::cerr << "Error loading background file " << filepath << ": " << e.what() << "\n";
+ }
+ }
+ }
+}
+
+bool ThemesValidateSpriteName(const std::string& sprite_name) {
+ ThemesInit();
+ if (sprite_name.empty()) {
+ return false;
+ }
+ // Note: GetSprite returns a default sprite if not found, so we can't easily validate
+ // The sprite will be validated when actually used in the game
+ // For now, just check that it's not empty
+ return true;
+}
+
+std::vector<std::string> ThemesGetBorderNames() {
+ ThemesInit();
+ std::vector<std::string> names;
+ for (const auto& pair : border_data) {
+ names.push_back(pair.first);
+ }
+ return names;
+}
+
+std::vector<std::string> ThemesGetBackgroundNames() {
+ ThemesInit();
+ std::vector<std::string> names;
+ for (const auto& pair : background_data) {
+ names.push_back(pair.first);
+ }
+ return names;
+}
+
+std::vector<std::string> ThemesGetDecorationNames() {
+ ThemesInit();
+ std::vector<std::string> names;
+ for (const auto& pair : decoration_data) {
+ names.push_back(pair.first);
+ }
+ return names;
+}
+
+BackGroundData ThemesGetBackground(const std::string& name) {
+ ThemesInit();
+ auto it = background_data.find(name);
+ if (it == background_data.end()) {
+ return background_data["standard"];
+ }
+ return it->second;
}
\ No newline at end of file
diff --git a/source/code/themes.hpp b/source/code/themes.hpp
index 5c9c099..f2601b7 100644
--- a/source/code/themes.hpp
+++ b/source/code/themes.hpp
@@ -51,6 +51,10 @@ struct ThemeBorderData {
std::pair<int, int> time_label_offset = {310,129};
std::pair<int, int> chain_label_offset = {310, 178};
std::pair<int, int> speed_label_offset = {310, 227};
+ std::pair<int, int> score_field_offset = {310, 102};
+ std::pair<int, int> time_field_offset = {310, 151};
+ std::pair<int, int> chain_field_offset = {310, 200};
+ std::pair<int, int> speed_field_offset = {310, 249};
};
@@ -70,6 +74,7 @@ struct Theme {
struct ThemeFileData {
std::vector<BackGroundData> background_data;
std::vector<DecorationData> decoration_data;
+ std::vector<ThemeBorderData> border_data;
std::vector<Theme> themes;
};
@@ -77,6 +82,11 @@ void ThemesFillMissingFields(Theme& theme);
void ThemesAddOrReplace(const Theme& theme);
+/**
+ * @brief Initializes the theme system
+ */
+void ThemesInit();
+
/**
* @brief returns a theme from a list
@@ -113,4 +123,60 @@ ThemeBorderData ThemesGetBorder(const std::string& name);
ThemeBorderData ThemesGetNextBorder(const std::string& current);
-void ThemesInitCustomBackgrounds();
\ No newline at end of file
+void ThemesInitCustomBackgrounds();
+
+/**
+ * @brief Saves a border to the borders/ directory
+ * @param name The name of the border
+ * @param data The border data to save
+ */
+void ThemesSaveBorderData(const std::string& name, const ThemeBorderData& data);
+
+/**
+ * @brief Saves a background to the backgrounds/ directory
+ * @param name The name of the background
+ * @param data The background data to save
+ */
+void ThemesSaveBackgroundData(const std::string& name, const BackGroundData& data);
+
+/**
+ * @brief Loads custom borders from the borders/ directory
+ */
+void ThemesLoadCustomBorders();
+
+/**
+ * @brief Loads custom backgrounds from the backgrounds/ directory
+ */
+void ThemesLoadCustomBackgrounds();
+
+/**
+ * @brief Validates that a sprite name exists in the sprite holder
+ * @param sprite_name The sprite name to validate
+ * @return true if the sprite exists, false otherwise
+ */
+bool ThemesValidateSpriteName(const std::string& sprite_name);
+
+/**
+ * @brief Gets all available border names
+ * @return Vector of border names
+ */
+std::vector<std::string> ThemesGetBorderNames();
+
+/**
+ * @brief Gets all available background names
+ * @return Vector of background names
+ */
+std::vector<std::string> ThemesGetBackgroundNames();
+
+/**
+ * @brief Gets all available decoration names
+ * @return Vector of decoration names
+ */
+std::vector<std::string> ThemesGetDecorationNames();
+
+/**
+ * @brief Gets a background by name
+ * @param name The background name
+ * @return The background data
+ */
+BackGroundData ThemesGetBackground(const std::string& name);
\ No newline at end of file