diff --git a/CMakeLists.txt b/CMakeLists.txt index ef63cb1..4ef0e13 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,8 @@ if (PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) message(FATAL_ERROR "In-source builds are not allowed!") endif() +set(CMAKE_BUILD_PARALLEL_LEVEL 8) + set(CMAKE_CXX_STANDARD 20) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") diff --git a/include/EditorApp.hpp b/include/EditorApp.hpp index 1052829..fdc4999 100644 --- a/include/EditorApp.hpp +++ b/include/EditorApp.hpp @@ -61,6 +61,20 @@ class EditHistoryLogWindow : public JUI::Window { public: }; +class LayerView : public JUI::Window +{ +public: + LayerView() : JUI::Window() + { + this->SetTitle("Layers"); + Name("LayerView"); + } + explicit LayerView(Widget* parent) : LayerView() + { + Parent(parent); + } +}; + class EditorApp : public OpenGLWindow { public: EditorCamera camera; @@ -70,6 +84,7 @@ public: JUI::Window* tileset_viewer; JUI::Rect* cell_indicator; JGL::Texture* test_tilesheet; + LayerView* layer_view; NewMapDialog* nmd = nullptr; @@ -140,6 +155,7 @@ public: /// This provides a mapping from a 1D flat array to a 2D grid. /// @return The {x,y} coordinate of the grid-cell that maps to the given index. Vector2i IndexToCell(int index, int width); + void ReplaceFill(int x, int y, int tileid); void FloodFill(int x, int y, int tileid); /// This provides a mapping from a 2D grid to a 1D flat array. diff --git a/include/Entity.hpp b/include/Entity.hpp index f1dc142..6ef5dd4 100644 --- a/include/Entity.hpp +++ b/include/Entity.hpp @@ -1,8 +1,22 @@ -// -// Created by dawsh on 5/12/25. -// +#pragma once -#ifndef ENTITY_HPP -#define ENTITY_HPP -#endif //ENTITY_HPP +/// @class Entity +/// @brief Represents an object or interactive element placed on the map, not necessarily tied to the tile grid. +/// This can include plater spawn points, enemies, pickups, triggers, boxes (my favorite), or any other +/// game specific object that needs to be placed and configured in the level. +class Entity +{ + public: +#pragma region json properties + + std::string name; + std::string type; + float x; + float y; +#pragma endregion + Entity(); + Entity(const json::value& json); + + +}; \ No newline at end of file diff --git a/include/Layer.hpp b/include/Layer.hpp index 1d2016a..c40326b 100644 --- a/include/Layer.hpp +++ b/include/Layer.hpp @@ -8,6 +8,11 @@ using namespace JJX; +enum class DataFormat +{ + SignedInt32, +}; + /// @class Layer /// @brief Represents a single plane of tiles within a level. /// @@ -22,117 +27,82 @@ using namespace JJX; class Layer { public: +#pragma region json properties + /// The file path to the binary file containing this layer's tile data. + /// @note This path is typically relative to the main level's JSON file. std::string binary_path; + /// The unique name of the layer (e.g., "Ground", "Decor", "Collisions"). std::string name; + /// The width of the layer in tiles. int rows; + /// The height of the layer in tiles. int cols; + /// The width of the individual cells of this layer, measured in pixels. int cell_width; + /// The height of the individual cells of this layer, measured in pixels. int cell_height; - int** cells; - float parallax_x; float parallax_y; + bool visible; + float opacity; + + +#pragma endregion + +#pragma region binary data storage + int** cells; +#pragma endregion + + /// The default constructor for Layer initializes member variables to default, zero values. - Layer() {} + /// @note The tile-ID grid is initialized to a null-pointer, as well. + Layer(); /// This constructor creates an empty layer of a given size, and cell-size. - Layer(int rows, int cols, int cell_width, int cell_height) - { - this->rows = rows; - this->cols = cols; - this->cell_width = cell_width; - this->cell_height = cell_height; - - InitGrid(); - } + /// @note The tile-ID grid is initialized to a null-pointer. + Layer(int rows, int cols, int cell_width, int cell_height); /// Constructs a Layer object by deserializing metadata from a JSON object. /// The binary tile data will need to be loaded separately using `ReadFromFile()` - explicit Layer(const json::value& json) + explicit Layer(const json::value& json); + + + /// Destructor for Layer cleans up all dynamically allocated memory for the tile grid. + ~Layer() { - Deserialize(json); + DeleteGrid(); } - /// Destructor for Layer cleans up - ~Layer() = default; + /// Constructs a layer object from the JSON contained in the file at the given path. + explicit Layer(const std::filesystem::path& path); - explicit Layer(const std::filesystem::path& path) - { - auto [json, err] = json::parse(read_file_contents(path)); - Deserialize(json); - } + /// Serializes the current layer object's metadata into a JSON object. + /// This does *NOT* include the actual tile grid data, which is stored in a separate binary file. + json::value Serialize() const; - json::value Serialize() const - { - json::object data; - data["name"] = name; - data["binary-path"] = binary_path; - data["rows"] = (float)rows; - data["cols"] = (float)cols; - data["cell-width"] = (float)cell_width; - data["cell-height"] = (float)cell_height; + /// Deserializes a JSON object into the current Layer object's metadata. + /// This also allocates the internal tile grid based on the deserialized width and height, + /// but does NOT load the actual tile grid data; use ReadBinaryData() for that. + void Deserialize(const json::value& json); + + void InitGrid(); + + void DeleteGrid(); - return data; - } - - void Deserialize(const json::value& json) - { - name = json["name"].as_string(); - binary_path = json["binary-path"].as_string(); - rows = json["rows"].number.value(); - cols = json["cols"].number.value(); - cell_width = json["cell-width"].number.value(); - cell_height = json["cell-height"].number.value(); - - } - - void InitGrid() - { - cells = new int*[rows]; - - for (int i = 0; i < rows; i++) - cells[i] = new int[cols]; - - for (int x = 0; x < rows; x++) { - for (int y = 0; y < cols; y++) { - cells[x][y] = -1; - } - } - } - - void DeleteGrid() - { - for (int i = 0; i < rows; i++) - delete[] cells[i]; - - delete[] cells; - } - - - /// Resizes the layer's dimensions and reinitializes the tile data. - /// Existing tile data will be lost or truncated. + /// Resizes the layer's dimensions and reinitialize the tile data. /// - void Resize(int newWidth, int newHeight) - { - - } + /// This method creates a new internal tile grid with the specified dimensions. + /// Existing tile data is copied to the new grid, preserving data within the + /// overlapping region. Any data outside the new bounds is truncated, and + /// new cells are initialized to -1 (empty tile). + /// + void Resize(int newWidth, int newHeight); - - void LoadFromDataBuffer(const int* buffer) - { - for (int x = 0; x < rows; x++) - { - for (int y = 0; y < cols; y++) - { - int index = (x * cols + y); - cells[x][y] = buffer[index]; - } - } - } + void LoadFromDataBuffer(const int* buffer); void WriteToDataBuffer(int* buffer) { @@ -150,6 +120,7 @@ public: /// Writes the current tile grid data to a specified raw binary file. /// The file path should typically be stored in the layer's JSON metadata. /// @param path + /// @return True if the binary data was loaded successfully, false otherwise. bool WriteBinaryData(const std::filesystem::path& path) { int* buffer = new int[rows*cols]; @@ -187,6 +158,4 @@ public: return true; } - - -}; \ No newline at end of file +}; diff --git a/include/Level.hpp b/include/Level.hpp index c5a7a2b..e687b0f 100644 --- a/include/Level.hpp +++ b/include/Level.hpp @@ -20,6 +20,7 @@ public: int cols; std::vector layers; std::string tileset_path; + json::value metadata; Level() {} diff --git a/libtest.cpp b/libtest.cpp index 4225a22..0e022f0 100644 --- a/libtest.cpp +++ b/libtest.cpp @@ -49,7 +49,11 @@ int main() Layer layer(128, 128, 16, 16); layer.binary_path = "test.lvl"; - layer.ReadBinaryData(); + layer.Load(); + + layer.Resize(64, 64); + + layer.Save(); level.layers.push_back(layer); diff --git a/src/EditorApp.cpp b/src/EditorApp.cpp index 9766955..ee75691 100644 --- a/src/EditorApp.cpp +++ b/src/EditorApp.cpp @@ -20,6 +20,48 @@ Vector2i EditorApp::IndexToCell(int index, int width) return {x, y}; } +void EditorApp::ReplaceFill(int x, int y, int tileid) +{ + + std::vector matching_adjacent_cells; + + int replace = GetTile(x, y); + SetTile(x, y, tileid); + + int range = 1; + + bool exhausted = false; + + while (!exhausted) + { + int stopped = 0; + if (GetTile(x-range, y) == replace) + matching_adjacent_cells.push_back({x-range, y}); + else + stopped++; + if (GetTile(x+range, y) == replace) + matching_adjacent_cells.push_back({x+range, y}); + else + stopped++; + if (GetTile(x, y-range) == replace) + matching_adjacent_cells.push_back({x, y-range}); + else + stopped++; + if (GetTile(x, y+range) == replace) + matching_adjacent_cells.push_back({x, y+range}); + else + stopped++; + + if (stopped == 4) + exhausted = true; + + range++; + } + + + for (auto& coords : matching_adjacent_cells) + SetTile(coords, tileid); +} void EditorApp::FloodFill(int x, int y, int tileid) { @@ -32,10 +74,6 @@ void EditorApp::FloodFill(int x, int y, int tileid) if (GetTile(x, y-1) == -1) FloodFill(x, y-1, tileid); if (GetTile(x, y+1) == -1) FloodFill(x, y+1, tileid); } - - - - } int EditorApp::CellToIndex(Vector2i cell, int width) @@ -94,7 +132,13 @@ void EditorApp::LoadTestFile() //if (!std::filesystem::exists("test_level/metadata.json")) + Level test_level = Level(std::filesystem::path("level.json")); + grid_rows = test_level.layers[0].rows; + grid_cols = test_level.layers[0].cols; + PopulateQuads(); + + InitGrid(); //std::string level_metadata_text = read_file_contents("test_level/metadata.json"); std::ifstream input; @@ -163,14 +207,14 @@ void EditorApp::LoadMisc() tileset_width = tileset.rows;//texture_size.x / grid_pixel_width; tileset_height = tileset.cols;//texture_size.y / grid_pixel_height; - PopulateQuads(); - InitGrid(); if (std::filesystem::exists("test.lvl")) LoadTestFile(); + + } JUI::Window* EditorApp::CreateTilesetViewerWindow(JUI::Widget* parent) @@ -279,6 +323,9 @@ void EditorApp::CreateWidgets() nmd = new NewMapDialog(scene); nmd->Close(); + layer_view = new LayerView(scene); + layer_view->Open(); + auto* topbar = new JUI::UtilityBar(scene); auto* file = topbar->AddSubmenu("File"); @@ -424,10 +471,10 @@ int EditorApp::GetTile(const Vector2i& cell) if (cell.y < 0) return -2; // out of bounds horizontally - if (cell.x > grid_rows) return -2; + if (cell.x >= grid_rows) return -2; // out of bounds vertically - if (cell.y > grid_cols) return -2; + if (cell.y >= grid_cols) return -2; return grid[cell.x][cell.y]; } @@ -443,10 +490,10 @@ void EditorApp::SetTile(const Vector2i& cell, int tileID) if (cell.y < 0) return; // out of bounds horizontally - if (cell.x > grid_rows) return; + if (cell.x >= grid_rows) return; // out of bounds vertically - if (cell.y > grid_cols) return; + if (cell.y >= grid_cols) return; grid[cell.x][cell.y] = tileID; } @@ -467,6 +514,8 @@ void EditorApp::PlaceTileBrush(const Vector2i& cell) if (IsKeyDown(Keys::B)) FloodFill(cell.x, cell.y, selected_quad); + else if (IsKeyDown(Keys::R)) + ReplaceFill(cell.x, cell.y, selected_quad); else SetTile(cell, selected_quad); } @@ -548,7 +597,7 @@ void EditorApp::DrawGrid(const Color4& color) float sw = GetWidth(); float sh = GetHeight(); - DrawGrid({{0,0}, {sw, sh}}, color); + DrawGrid({{0,0}, {static_cast(grid_rows*grid_pixel_width), static_cast(grid_cols*grid_pixel_height)}}, color); } void EditorApp::DrawCellPointerOutline() @@ -568,11 +617,16 @@ void EditorApp::DrawTiles() auto quad_idx = grid[gx][gy]; + Vector2 pos(gx*grid_pixel_width, gy*grid_pixel_height); + + //if ((gx+gy) % 2 == 0) + //J2D::FillRect(Colors::Purples::Fuchsia, pos, {16, 16}); + if (quad_idx < 0) continue; auto quad = quads[quad_idx]; - Vector2 pos(gx*grid_pixel_width, gy*grid_pixel_height); + J2D::DrawPartialSprite(test_tilesheet, pos, Vector2(quad.x, quad.y), Vector2(quad.w, quad.h)); } diff --git a/src/Layer.cpp b/src/Layer.cpp index 5d6292c..b2fca03 100644 --- a/src/Layer.cpp +++ b/src/Layer.cpp @@ -1 +1,117 @@ #include + +Layer::Layer(): rows(0), cols(0), cell_width(0), cell_height(0), parallax_x(0), parallax_y(0), cells(nullptr) +{ } + +Layer::Layer(int rows, int cols, int cell_width, int cell_height): cells(nullptr), parallax_x(0), parallax_y(0) +{ + this->rows = rows; + this->cols = cols; + this->cell_width = cell_width; + this->cell_height = cell_height; + +} + +Layer::Layer(const json::value& json) +{ + Deserialize(json); +} + +Layer::Layer(const std::filesystem::path& path) +{ + auto [json, err] = json::parse(read_file_contents(path)); + Deserialize(json); +} + +json::value Layer::Serialize() const +{ + json::object data; + data["name"] = name; + data["binary-path"] = binary_path; + data["rows"] = (float)rows; + data["cols"] = (float)cols; + data["cell-width"] = (float)cell_width; + data["cell-height"] = (float)cell_height; + + + return data; +} + +void Layer::Deserialize(const json::value& json) +{ + name = json["name"].as_string(); + binary_path = json["binary-path"].as_string(); + rows = json["rows"].number.value(); + cols = json["cols"].number.value(); + cell_width = json["cell-width"].number.value(); + cell_height = json["cell-height"].number.value(); + +} + +void Layer::InitGrid() +{ + cells = new int*[rows]; + + for (int i = 0; i < rows; i++) + cells[i] = new int[cols]; + + for (int x = 0; x < rows; x++) { + for (int y = 0; y < cols; y++) { + cells[x][y] = -1; + } + } +} + +void Layer::DeleteGrid() +{ + for (int i = 0; i < rows; i++) + delete[] cells[i]; + + delete[] cells; +} + +void Layer::Resize(int newWidth, int newHeight) { + + // If dimensions are unchanged, do nothing. + if (newWidth == rows && newHeight == cols) + return; + + int best_fit_rows = J3ML::Math::Min(rows, newWidth); + int best_fit_cols = J3ML::Math::Min(cols, newHeight); + + int old_data_length = rows*cols; + int* old_data_contiguous = new int[old_data_length]; + + for (int x = 0; x < best_fit_rows; x++) { + for (int y = 0; y < best_fit_cols; y++) { + int index = (x * best_fit_cols + y); + old_data_contiguous[index] = cells[x][y]; + } + } + + DeleteGrid(); + this->rows = newWidth; + this->cols = newHeight; + InitGrid(); + + for (int x = 0; x < best_fit_rows; x++) { + for (int y = 0; y < best_fit_cols; y++) { + int index = (x * best_fit_cols + y); + cells[x][y] = old_data_contiguous[index]; + } + } + + delete[] old_data_contiguous; +} + +void Layer::LoadFromDataBuffer(const int* buffer) +{ + for (int x = 0; x < rows; x++) + { + for (int y = 0; y < cols; y++) + { + int index = (x * cols + y); + cells[x][y] = buffer[index]; + } + } +}