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 @@ class BlockGameSdl : public BlockGame { 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 @@ class BlockGameSdl : public BlockGame { } 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 @@ Source information and contacts persons can be found at #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(), "Sets the default puzzle file to load") ("puzzle-single-level", boost::program_options::value(), "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(); } @@ -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 +#include + +#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(globalData.xsize * 0.62f); + preview_game = std::make_shared(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(selected_border_index) < border_names.size()) { + preview_theme.border = ThemesGetBorder(border_names[selected_border_index]); + } + if (static_cast(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(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/.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_"); + } + + // 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/.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(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(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(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(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(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(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(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(selected_border_index) < border_names.size()) { + theme.border = ThemesGetBorder(border_names[selected_border_index]); + } + if (static_cast(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 ThemeEditorState::GetAvailableSprites() { + // TODO: Implement getting available sprite names from globalData.spriteHolder + return std::vector(); +} 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 +#include +#include + +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 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 border_names; + std::vector background_names; + std::vector decoration_names; + const std::vector board_backgrounds = {"back_board", "back_board_sample_snow", "trans_cover"}; + + // Preview + std::shared_ptr 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 @@ Source information and contacts persons can be found at #include "themes.hpp" #include "sago/SagoMisc.hpp" +#include "os.hpp" #include #include #include @@ -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& 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& 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& 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 ThemesGetBorderNames() { + ThemesInit(); + std::vector names; + for (const auto& pair : border_data) { + names.push_back(pair.first); + } + return names; +} + +std::vector ThemesGetBackgroundNames() { + ThemesInit(); + std::vector names; + for (const auto& pair : background_data) { + names.push_back(pair.first); + } + return names; +} + +std::vector ThemesGetDecorationNames() { + ThemesInit(); + std::vector 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 time_label_offset = {310,129}; std::pair chain_label_offset = {310, 178}; std::pair speed_label_offset = {310, 227}; + std::pair score_field_offset = {310, 102}; + std::pair time_field_offset = {310, 151}; + std::pair chain_field_offset = {310, 200}; + std::pair speed_field_offset = {310, 249}; }; @@ -70,6 +74,7 @@ struct Theme { struct ThemeFileData { std::vector background_data; std::vector decoration_data; + std::vector border_data; std::vector 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 ThemesGetBorderNames(); + +/** + * @brief Gets all available background names + * @return Vector of background names + */ +std::vector ThemesGetBackgroundNames(); + +/** + * @brief Gets all available decoration names + * @return Vector of decoration names + */ +std::vector 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