git repos / blockattack-game

source/code/theme_editor/ThemeEditorState.cpp

browsing at commit = 727f4a095b4b10d630e54a3ea673343725bd7186

raw · blame · history

/*
===========================================================================
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>();
}