diff --git a/src/GridLayers.cpp b/src/GridLayers.cpp new file mode 100644 index 0000000..be67cad --- /dev/null +++ b/src/GridLayers.cpp @@ -0,0 +1,621 @@ +#include "GridLayers.h" +#include "UIGrid.h" +#include "PyColor.h" +#include "PyTexture.h" +#include + +// ============================================================================= +// GridLayer base class +// ============================================================================= + +GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent) + : type(type), z_index(z_index), grid_x(grid_x), grid_y(grid_y), + parent_grid(parent), visible(true) +{} + +// ============================================================================= +// ColorLayer implementation +// ============================================================================= + +ColorLayer::ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent) + : GridLayer(GridLayerType::Color, z_index, grid_x, grid_y, parent), + colors(grid_x * grid_y, sf::Color::Transparent) +{} + +sf::Color& ColorLayer::at(int x, int y) { + return colors[y * grid_x + x]; +} + +const sf::Color& ColorLayer::at(int x, int y) const { + return colors[y * grid_x + x]; +} + +void ColorLayer::fill(const sf::Color& color) { + std::fill(colors.begin(), colors.end(), color); +} + +void ColorLayer::resize(int new_grid_x, int new_grid_y) { + std::vector new_colors(new_grid_x * new_grid_y, sf::Color::Transparent); + + // Copy existing data + int copy_x = std::min(grid_x, new_grid_x); + int copy_y = std::min(grid_y, new_grid_y); + for (int y = 0; y < copy_y; ++y) { + for (int x = 0; x < copy_x; ++x) { + new_colors[y * new_grid_x + x] = colors[y * grid_x + x]; + } + } + + colors = std::move(new_colors); + grid_x = new_grid_x; + grid_y = new_grid_y; +} + +void ColorLayer::render(sf::RenderTarget& target, + float left_spritepixels, float top_spritepixels, + int left_edge, int top_edge, int x_limit, int y_limit, + float zoom, int cell_width, int cell_height) { + if (!visible) return; + + sf::RectangleShape rect; + rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom)); + rect.setOutlineThickness(0); + + for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) { + for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) { + if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue; + + const sf::Color& color = at(x, y); + if (color.a == 0) continue; // Skip fully transparent + + auto pixel_pos = sf::Vector2f( + (x * cell_width - left_spritepixels) * zoom, + (y * cell_height - top_spritepixels) * zoom + ); + + rect.setPosition(pixel_pos); + rect.setFillColor(color); + target.draw(rect); + } + } +} + +// ============================================================================= +// TileLayer implementation +// ============================================================================= + +TileLayer::TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent, + std::shared_ptr texture) + : GridLayer(GridLayerType::Tile, z_index, grid_x, grid_y, parent), + tiles(grid_x * grid_y, -1), // -1 = no tile + texture(texture) +{} + +int& TileLayer::at(int x, int y) { + return tiles[y * grid_x + x]; +} + +int TileLayer::at(int x, int y) const { + return tiles[y * grid_x + x]; +} + +void TileLayer::fill(int tile_index) { + std::fill(tiles.begin(), tiles.end(), tile_index); +} + +void TileLayer::resize(int new_grid_x, int new_grid_y) { + std::vector new_tiles(new_grid_x * new_grid_y, -1); + + // Copy existing data + int copy_x = std::min(grid_x, new_grid_x); + int copy_y = std::min(grid_y, new_grid_y); + for (int y = 0; y < copy_y; ++y) { + for (int x = 0; x < copy_x; ++x) { + new_tiles[y * new_grid_x + x] = tiles[y * grid_x + x]; + } + } + + tiles = std::move(new_tiles); + grid_x = new_grid_x; + grid_y = new_grid_y; +} + +void TileLayer::render(sf::RenderTarget& target, + float left_spritepixels, float top_spritepixels, + int left_edge, int top_edge, int x_limit, int y_limit, + float zoom, int cell_width, int cell_height) { + if (!visible || !texture) return; + + for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) { + for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) { + if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue; + + int tile_index = at(x, y); + if (tile_index < 0) continue; // No tile + + auto pixel_pos = sf::Vector2f( + (x * cell_width - left_spritepixels) * zoom, + (y * cell_height - top_spritepixels) * zoom + ); + + sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(zoom, zoom)); + target.draw(sprite); + } + } +} + +// ============================================================================= +// Python API - ColorLayer +// ============================================================================= + +PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = { + {"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS, + "at(x, y) -> Color\n\nGet the color at cell position (x, y)."}, + {"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS, + "set(x, y, color)\n\nSet the color at cell position (x, y)."}, + {"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS, + "fill(color)\n\nFill the entire layer with the specified color."}, + {NULL} +}; + +PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = { + {"z_index", (getter)PyGridLayerAPI::ColorLayer_get_z_index, + (setter)PyGridLayerAPI::ColorLayer_set_z_index, + "Layer z-order. Negative values render below entities.", NULL}, + {"visible", (getter)PyGridLayerAPI::ColorLayer_get_visible, + (setter)PyGridLayerAPI::ColorLayer_set_visible, + "Whether the layer is rendered.", NULL}, + {"grid_size", (getter)PyGridLayerAPI::ColorLayer_get_grid_size, NULL, + "Layer dimensions as (width, height) tuple.", NULL}, + {NULL} +}; + +int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"z_index", "grid_size", NULL}; + int z_index = -1; + PyObject* grid_size_obj = nullptr; + int grid_x = 0, grid_y = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", const_cast(kwlist), + &z_index, &grid_size_obj)) { + return -1; + } + + // Parse grid_size if provided + if (grid_size_obj && grid_size_obj != Py_None) { + if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) { + PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple"); + return -1; + } + grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0)); + grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1)); + if (PyErr_Occurred()) return -1; + } + + // Create the layer (will be attached to grid via add_layer) + self->data = std::make_shared(z_index, grid_x, grid_y, nullptr); + self->grid.reset(); + + return 0; +} + +PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) { + int x, y; + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + return NULL; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { + PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds"); + return NULL; + } + + const sf::Color& color = self->data->at(x, y); + + // Return as mcrfpy.Color + auto* color_type = (PyTypeObject*)PyObject_GetAttrString( + PyImport_ImportModule("mcrfpy"), "Color"); + if (!color_type) return NULL; + + PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0); + Py_DECREF(color_type); + if (!color_obj) return NULL; + + color_obj->data = color; + return (PyObject*)color_obj; +} + +PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) { + int x, y; + PyObject* color_obj; + if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) { + return NULL; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { + PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds"); + return NULL; + } + + // Parse color + sf::Color color; + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return NULL; + + auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color"); + Py_DECREF(mcrfpy_module); + if (!color_type) return NULL; + + if (PyObject_IsInstance(color_obj, color_type)) { + color = ((PyColorObject*)color_obj)->data; + } else if (PyTuple_Check(color_obj)) { + int r, g, b, a = 255; + if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) { + Py_DECREF(color_type); + return NULL; + } + color = sf::Color(r, g, b, a); + } else { + Py_DECREF(color_type); + PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple"); + return NULL; + } + Py_DECREF(color_type); + + self->data->at(x, y) = color; + Py_RETURN_NONE; +} + +PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) { + PyObject* color_obj; + if (!PyArg_ParseTuple(args, "O", &color_obj)) { + return NULL; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + // Parse color + sf::Color color; + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return NULL; + + auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color"); + Py_DECREF(mcrfpy_module); + if (!color_type) return NULL; + + if (PyObject_IsInstance(color_obj, color_type)) { + color = ((PyColorObject*)color_obj)->data; + } else if (PyTuple_Check(color_obj)) { + int r, g, b, a = 255; + if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) { + Py_DECREF(color_type); + return NULL; + } + color = sf::Color(r, g, b, a); + } else { + Py_DECREF(color_type); + PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple"); + return NULL; + } + Py_DECREF(color_type); + + self->data->fill(color); + Py_RETURN_NONE; +} + +PyObject* PyGridLayerAPI::ColorLayer_get_z_index(PyColorLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + return PyLong_FromLong(self->data->z_index); +} + +int PyGridLayerAPI::ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return -1; + } + long z = PyLong_AsLong(value); + if (PyErr_Occurred()) return -1; + self->data->z_index = z; + // TODO: Trigger re-sort in parent grid + return 0; +} + +PyObject* PyGridLayerAPI::ColorLayer_get_visible(PyColorLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + return PyBool_FromLong(self->data->visible); +} + +int PyGridLayerAPI::ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return -1; + } + int v = PyObject_IsTrue(value); + if (v < 0) return -1; + self->data->visible = v; + return 0; +} + +PyObject* PyGridLayerAPI::ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y); +} + +PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) { + std::ostringstream ss; + if (!self->data) { + ss << ""; + } else { + ss << "data->grid_x << "x" << self->data->grid_y << ")" + << " visible=" << (self->data->visible ? "True" : "False") << ">"; + } + return PyUnicode_FromString(ss.str().c_str()); +} + +// ============================================================================= +// Python API - TileLayer +// ============================================================================= + +PyMethodDef PyGridLayerAPI::TileLayer_methods[] = { + {"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS, + "at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."}, + {"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS, + "set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."}, + {"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS, + "fill(index)\n\nFill the entire layer with the specified tile index."}, + {NULL} +}; + +PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = { + {"z_index", (getter)PyGridLayerAPI::TileLayer_get_z_index, + (setter)PyGridLayerAPI::TileLayer_set_z_index, + "Layer z-order. Negative values render below entities.", NULL}, + {"visible", (getter)PyGridLayerAPI::TileLayer_get_visible, + (setter)PyGridLayerAPI::TileLayer_set_visible, + "Whether the layer is rendered.", NULL}, + {"texture", (getter)PyGridLayerAPI::TileLayer_get_texture, + (setter)PyGridLayerAPI::TileLayer_set_texture, + "Texture atlas for tile sprites.", NULL}, + {"grid_size", (getter)PyGridLayerAPI::TileLayer_get_grid_size, NULL, + "Layer dimensions as (width, height) tuple.", NULL}, + {NULL} +}; + +int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"z_index", "texture", "grid_size", NULL}; + int z_index = -1; + PyObject* texture_obj = nullptr; + PyObject* grid_size_obj = nullptr; + int grid_x = 0, grid_y = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iOO", const_cast(kwlist), + &z_index, &texture_obj, &grid_size_obj)) { + return -1; + } + + // Parse texture + std::shared_ptr texture; + if (texture_obj && texture_obj != Py_None) { + // Check if it's a PyTexture + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return -1; + + auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture"); + Py_DECREF(mcrfpy_module); + if (!texture_type) return -1; + + if (PyObject_IsInstance(texture_obj, texture_type)) { + texture = ((PyTextureObject*)texture_obj)->data; + } else { + Py_DECREF(texture_type); + PyErr_SetString(PyExc_TypeError, "texture must be a Texture object"); + return -1; + } + Py_DECREF(texture_type); + } + + // Parse grid_size if provided + if (grid_size_obj && grid_size_obj != Py_None) { + if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) { + PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple"); + return -1; + } + grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0)); + grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1)); + if (PyErr_Occurred()) return -1; + } + + // Create the layer + self->data = std::make_shared(z_index, grid_x, grid_y, nullptr, texture); + self->grid.reset(); + + return 0; +} + +PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) { + int x, y; + if (!PyArg_ParseTuple(args, "ii", &x, &y)) { + return NULL; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { + PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds"); + return NULL; + } + + return PyLong_FromLong(self->data->at(x, y)); +} + +PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) { + int x, y, index; + if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) { + return NULL; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) { + PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds"); + return NULL; + } + + self->data->at(x, y) = index; + Py_RETURN_NONE; +} + +PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) { + int index; + if (!PyArg_ParseTuple(args, "i", &index)) { + return NULL; + } + + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + self->data->fill(index); + Py_RETURN_NONE; +} + +PyObject* PyGridLayerAPI::TileLayer_get_z_index(PyTileLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + return PyLong_FromLong(self->data->z_index); +} + +int PyGridLayerAPI::TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return -1; + } + long z = PyLong_AsLong(value); + if (PyErr_Occurred()) return -1; + self->data->z_index = z; + // TODO: Trigger re-sort in parent grid + return 0; +} + +PyObject* PyGridLayerAPI::TileLayer_get_visible(PyTileLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + return PyBool_FromLong(self->data->visible); +} + +int PyGridLayerAPI::TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return -1; + } + int v = PyObject_IsTrue(value); + if (v < 0) return -1; + self->data->visible = v; + return 0; +} + +PyObject* PyGridLayerAPI::TileLayer_get_texture(PyTileLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + + if (!self->data->texture) { + Py_RETURN_NONE; + } + + auto* texture_type = (PyTypeObject*)PyObject_GetAttrString( + PyImport_ImportModule("mcrfpy"), "Texture"); + if (!texture_type) return NULL; + + PyTextureObject* tex_obj = (PyTextureObject*)texture_type->tp_alloc(texture_type, 0); + Py_DECREF(texture_type); + if (!tex_obj) return NULL; + + tex_obj->data = self->data->texture; + return (PyObject*)tex_obj; +} + +int PyGridLayerAPI::TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return -1; + } + + if (value == Py_None) { + self->data->texture.reset(); + return 0; + } + + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return -1; + + auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture"); + Py_DECREF(mcrfpy_module); + if (!texture_type) return -1; + + if (!PyObject_IsInstance(value, texture_type)) { + Py_DECREF(texture_type); + PyErr_SetString(PyExc_TypeError, "texture must be a Texture object or None"); + return -1; + } + Py_DECREF(texture_type); + + self->data->texture = ((PyTextureObject*)value)->data; + return 0; +} + +PyObject* PyGridLayerAPI::TileLayer_get_grid_size(PyTileLayerObject* self, void* closure) { + if (!self->data) { + PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); + return NULL; + } + return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y); +} + +PyObject* PyGridLayerAPI::TileLayer_repr(PyTileLayerObject* self) { + std::ostringstream ss; + if (!self->data) { + ss << ""; + } else { + ss << "data->grid_x << "x" << self->data->grid_y << ")" + << " visible=" << (self->data->visible ? "True" : "False") + << " texture=" << (self->data->texture ? "set" : "None") << ">"; + } + return PyUnicode_FromString(ss.str().c_str()); +} diff --git a/src/GridLayers.h b/src/GridLayers.h new file mode 100644 index 0000000..7aa5ef3 --- /dev/null +++ b/src/GridLayers.h @@ -0,0 +1,219 @@ +#pragma once +#include "Common.h" +#include "Python.h" +#include "structmember.h" +#include +#include +#include + +// Forward declarations +class UIGrid; +class PyTexture; + +// Include PyTexture.h for PyTextureObject (typedef, not struct) +#include "PyTexture.h" + +// Layer type enumeration +enum class GridLayerType { + Color, + Tile +}; + +// Abstract base class for grid layers +class GridLayer { +public: + GridLayerType type; + int z_index; // Negative = below entities, >= 0 = above entities + int grid_x, grid_y; // Dimensions + UIGrid* parent_grid; // Parent grid reference + bool visible; // Visibility flag + + GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent); + virtual ~GridLayer() = default; + + // Render the layer to a RenderTarget with the given transformation parameters + virtual void render(sf::RenderTarget& target, + float left_spritepixels, float top_spritepixels, + int left_edge, int top_edge, int x_limit, int y_limit, + float zoom, int cell_width, int cell_height) = 0; + + // Resize the layer (reallocates storage) + virtual void resize(int new_grid_x, int new_grid_y) = 0; +}; + +// Color layer - stores RGBA color per cell +class ColorLayer : public GridLayer { +public: + std::vector colors; + + ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent); + + // Access color at position + sf::Color& at(int x, int y); + const sf::Color& at(int x, int y) const; + + // Fill entire layer with a color + void fill(const sf::Color& color); + + void render(sf::RenderTarget& target, + float left_spritepixels, float top_spritepixels, + int left_edge, int top_edge, int x_limit, int y_limit, + float zoom, int cell_width, int cell_height) override; + + void resize(int new_grid_x, int new_grid_y) override; +}; + +// Tile layer - stores sprite index per cell with texture reference +class TileLayer : public GridLayer { +public: + std::vector tiles; // Sprite indices (-1 = no tile) + std::shared_ptr texture; + + TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent, + std::shared_ptr texture = nullptr); + + // Access tile index at position + int& at(int x, int y); + int at(int x, int y) const; + + // Fill entire layer with a tile index + void fill(int tile_index); + + void render(sf::RenderTarget& target, + float left_spritepixels, float top_spritepixels, + int left_edge, int top_edge, int x_limit, int y_limit, + float zoom, int cell_width, int cell_height) override; + + void resize(int new_grid_x, int new_grid_y) override; +}; + +// Python wrapper types +typedef struct { + PyObject_HEAD + std::shared_ptr data; + std::shared_ptr grid; // Parent grid reference +} PyGridLayerObject; + +typedef struct { + PyObject_HEAD + std::shared_ptr data; + std::shared_ptr grid; +} PyColorLayerObject; + +typedef struct { + PyObject_HEAD + std::shared_ptr data; + std::shared_ptr grid; +} PyTileLayerObject; + +// Python API classes +class PyGridLayerAPI { +public: + // ColorLayer methods + static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds); + static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args); + static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args); + static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args); + static PyObject* ColorLayer_get_z_index(PyColorLayerObject* self, void* closure); + static int ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure); + static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure); + static int ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure); + static PyObject* ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure); + static PyObject* ColorLayer_repr(PyColorLayerObject* self); + + // TileLayer methods + static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds); + static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args); + static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args); + static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args); + static PyObject* TileLayer_get_z_index(PyTileLayerObject* self, void* closure); + static int TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure); + static PyObject* TileLayer_get_visible(PyTileLayerObject* self, void* closure); + static int TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure); + static PyObject* TileLayer_get_texture(PyTileLayerObject* self, void* closure); + static int TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure); + static PyObject* TileLayer_get_grid_size(PyTileLayerObject* self, void* closure); + static PyObject* TileLayer_repr(PyTileLayerObject* self); + + // Method and getset arrays + static PyMethodDef ColorLayer_methods[]; + static PyGetSetDef ColorLayer_getsetters[]; + static PyMethodDef TileLayer_methods[]; + static PyGetSetDef TileLayer_getsetters[]; +}; + +namespace mcrfpydef { + // ColorLayer type + static PyTypeObject PyColorLayerType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.ColorLayer", + .tp_basicsize = sizeof(PyColorLayerObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) { + PyColorLayerObject* obj = (PyColorLayerObject*)self; + obj->data.reset(); + obj->grid.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)PyGridLayerAPI::ColorLayer_repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("ColorLayer(z_index=-1, grid_size=None)\n\n" + "A grid layer that stores RGBA colors per cell.\n\n" + "Args:\n" + " z_index (int): Render order. Negative = below entities. Default: -1\n" + " grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n" + "Attributes:\n" + " z_index (int): Layer z-order relative to entities\n" + " visible (bool): Whether layer is rendered\n" + " grid_size (tuple): Layer dimensions (read-only)\n\n" + "Methods:\n" + " at(x, y): Get color at cell position\n" + " set(x, y, color): Set color at cell position\n" + " fill(color): Fill entire layer with color"), + .tp_methods = PyGridLayerAPI::ColorLayer_methods, + .tp_getset = PyGridLayerAPI::ColorLayer_getsetters, + .tp_init = (initproc)PyGridLayerAPI::ColorLayer_init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { + PyColorLayerObject* self = (PyColorLayerObject*)type->tp_alloc(type, 0); + return (PyObject*)self; + } + }; + + // TileLayer type + static PyTypeObject PyTileLayerType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.TileLayer", + .tp_basicsize = sizeof(PyTileLayerObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)[](PyObject* self) { + PyTileLayerObject* obj = (PyTileLayerObject*)self; + obj->data.reset(); + obj->grid.reset(); + Py_TYPE(self)->tp_free(self); + }, + .tp_repr = (reprfunc)PyGridLayerAPI::TileLayer_repr, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("TileLayer(z_index=-1, texture=None, grid_size=None)\n\n" + "A grid layer that stores sprite indices per cell.\n\n" + "Args:\n" + " z_index (int): Render order. Negative = below entities. Default: -1\n" + " texture (Texture): Sprite atlas for tile rendering. Default: None\n" + " grid_size (tuple): Dimensions as (width, height). Default: parent grid size\n\n" + "Attributes:\n" + " z_index (int): Layer z-order relative to entities\n" + " visible (bool): Whether layer is rendered\n" + " texture (Texture): Tile sprite atlas\n" + " grid_size (tuple): Layer dimensions (read-only)\n\n" + "Methods:\n" + " at(x, y): Get tile index at cell position\n" + " set(x, y, index): Set tile index at cell position\n" + " fill(index): Fill entire layer with tile index"), + .tp_methods = PyGridLayerAPI::TileLayer_methods, + .tp_getset = PyGridLayerAPI::TileLayer_getsetters, + .tp_init = (initproc)PyGridLayerAPI::TileLayer_init, + .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { + PyTileLayerObject* self = (PyTileLayerObject*)type->tp_alloc(type, 0); + return (PyObject*)self; + } + }; +} diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index c383688..aaefd44 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -15,6 +15,7 @@ #include "UILine.h" #include "UICircle.h" #include "UIArc.h" +#include "GridLayers.h" #include "Resources.h" #include "PyScene.h" #include @@ -303,6 +304,9 @@ PyObject* PyInit_mcrfpy() /*game map & perspective data*/ &PyUIGridPointType, &PyUIGridPointStateType, + /*grid layers (#147)*/ + &PyColorLayerType, &PyTileLayerType, + /*collections & iterators*/ &PyUICollectionType, &PyUICollectionIterType, &PyUIEntityCollectionType, &PyUIEntityCollectionIterType, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index abdd92e..118df49 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -183,6 +183,14 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) // Record how many cells were rendered Resources::game->metrics.gridCellsRendered += cellsRendered; + // #147 - Render dynamic layers with z_index < 0 (below entities) + sortLayers(); + for (auto& layer : layers) { + if (layer->z_index >= 0) break; // Stop at layers that go above entities + layer->render(renderTexture, left_spritepixels, top_spritepixels, + left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height); + } + // middle layer - entities // disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window) { @@ -217,6 +225,13 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) Resources::game->metrics.totalEntities += totalEntities; } + // #147 - Render dynamic layers with z_index >= 0 (above entities) + for (auto& layer : layers) { + if (layer->z_index < 0) continue; // Skip layers below entities + layer->render(renderTexture, left_spritepixels, top_spritepixels, + left_edge, top_edge, x_limit, y_limit, zoom, cell_width, cell_height); + } + // Children layer - UIDrawables in grid-world pixel coordinates // Positioned between entities and FOV overlay for proper z-ordering if (children && !children->empty()) { @@ -377,6 +392,36 @@ PyObjectsEnum UIGrid::derived_type() return PyObjectsEnum::UIGRID; } +// #147 - Layer management methods +std::shared_ptr UIGrid::addColorLayer(int z_index) { + auto layer = std::make_shared(z_index, grid_x, grid_y, this); + layers.push_back(layer); + layers_need_sort = true; + return layer; +} + +std::shared_ptr UIGrid::addTileLayer(int z_index, std::shared_ptr texture) { + auto layer = std::make_shared(z_index, grid_x, grid_y, this, texture); + layers.push_back(layer); + layers_need_sort = true; + return layer; +} + +void UIGrid::removeLayer(std::shared_ptr layer) { + auto it = std::find(layers.begin(), layers.end(), layer); + if (it != layers.end()) { + layers.erase(it); + } +} + +void UIGrid::sortLayers() { + if (layers_need_sort) { + std::sort(layers.begin(), layers.end(), + [](const auto& a, const auto& b) { return a->z_index < b->z_index; }); + layers_need_sort = false; + } +} + // TCOD integration methods void UIGrid::syncTCODMap() { @@ -1301,23 +1346,225 @@ PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, Py return path_list; } +// #147 - Layer system Python API +PyObject* UIGrid::py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds) { + static const char* kwlist[] = {"type", "z_index", "texture", NULL}; + const char* type_str = nullptr; + int z_index = -1; + PyObject* texture_obj = nullptr; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "s|iO", const_cast(kwlist), + &type_str, &z_index, &texture_obj)) { + return NULL; + } + + std::string type(type_str); + + if (type == "color") { + auto layer = self->data->addColorLayer(z_index); + + // Create Python ColorLayer object + auto* color_layer_type = (PyTypeObject*)PyObject_GetAttrString( + PyImport_ImportModule("mcrfpy"), "ColorLayer"); + if (!color_layer_type) return NULL; + + PyColorLayerObject* py_layer = (PyColorLayerObject*)color_layer_type->tp_alloc(color_layer_type, 0); + Py_DECREF(color_layer_type); + if (!py_layer) return NULL; + + py_layer->data = layer; + py_layer->grid = self->data; + return (PyObject*)py_layer; + + } else if (type == "tile") { + // Parse texture + std::shared_ptr texture; + if (texture_obj && texture_obj != Py_None) { + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return NULL; + + auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture"); + Py_DECREF(mcrfpy_module); + if (!texture_type) return NULL; + + if (!PyObject_IsInstance(texture_obj, texture_type)) { + Py_DECREF(texture_type); + PyErr_SetString(PyExc_TypeError, "texture must be a Texture object"); + return NULL; + } + Py_DECREF(texture_type); + texture = ((PyTextureObject*)texture_obj)->data; + } + + auto layer = self->data->addTileLayer(z_index, texture); + + // Create Python TileLayer object + auto* tile_layer_type = (PyTypeObject*)PyObject_GetAttrString( + PyImport_ImportModule("mcrfpy"), "TileLayer"); + if (!tile_layer_type) return NULL; + + PyTileLayerObject* py_layer = (PyTileLayerObject*)tile_layer_type->tp_alloc(tile_layer_type, 0); + Py_DECREF(tile_layer_type); + if (!py_layer) return NULL; + + py_layer->data = layer; + py_layer->grid = self->data; + return (PyObject*)py_layer; + + } else { + PyErr_SetString(PyExc_ValueError, "type must be 'color' or 'tile'"); + return NULL; + } +} + +PyObject* UIGrid::py_remove_layer(PyUIGridObject* self, PyObject* args) { + PyObject* layer_obj; + if (!PyArg_ParseTuple(args, "O", &layer_obj)) { + return NULL; + } + + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return NULL; + + // Check if ColorLayer + auto* color_layer_type = PyObject_GetAttrString(mcrfpy_module, "ColorLayer"); + if (color_layer_type && PyObject_IsInstance(layer_obj, color_layer_type)) { + Py_DECREF(color_layer_type); + Py_DECREF(mcrfpy_module); + auto* py_layer = (PyColorLayerObject*)layer_obj; + self->data->removeLayer(py_layer->data); + Py_RETURN_NONE; + } + if (color_layer_type) Py_DECREF(color_layer_type); + + // Check if TileLayer + auto* tile_layer_type = PyObject_GetAttrString(mcrfpy_module, "TileLayer"); + if (tile_layer_type && PyObject_IsInstance(layer_obj, tile_layer_type)) { + Py_DECREF(tile_layer_type); + Py_DECREF(mcrfpy_module); + auto* py_layer = (PyTileLayerObject*)layer_obj; + self->data->removeLayer(py_layer->data); + Py_RETURN_NONE; + } + if (tile_layer_type) Py_DECREF(tile_layer_type); + + Py_DECREF(mcrfpy_module); + PyErr_SetString(PyExc_TypeError, "layer must be a ColorLayer or TileLayer"); + return NULL; +} + +PyObject* UIGrid::get_layers(PyUIGridObject* self, void* closure) { + self->data->sortLayers(); + + PyObject* list = PyList_New(self->data->layers.size()); + if (!list) return NULL; + + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) { + Py_DECREF(list); + return NULL; + } + + auto* color_layer_type = (PyTypeObject*)PyObject_GetAttrString(mcrfpy_module, "ColorLayer"); + auto* tile_layer_type = (PyTypeObject*)PyObject_GetAttrString(mcrfpy_module, "TileLayer"); + Py_DECREF(mcrfpy_module); + + if (!color_layer_type || !tile_layer_type) { + if (color_layer_type) Py_DECREF(color_layer_type); + if (tile_layer_type) Py_DECREF(tile_layer_type); + Py_DECREF(list); + return NULL; + } + + for (size_t i = 0; i < self->data->layers.size(); ++i) { + auto& layer = self->data->layers[i]; + PyObject* py_layer = nullptr; + + if (layer->type == GridLayerType::Color) { + PyColorLayerObject* obj = (PyColorLayerObject*)color_layer_type->tp_alloc(color_layer_type, 0); + if (obj) { + obj->data = std::static_pointer_cast(layer); + obj->grid = self->data; + py_layer = (PyObject*)obj; + } + } else { + PyTileLayerObject* obj = (PyTileLayerObject*)tile_layer_type->tp_alloc(tile_layer_type, 0); + if (obj) { + obj->data = std::static_pointer_cast(layer); + obj->grid = self->data; + py_layer = (PyObject*)obj; + } + } + + if (!py_layer) { + Py_DECREF(color_layer_type); + Py_DECREF(tile_layer_type); + Py_DECREF(list); + return NULL; + } + + PyList_SET_ITEM(list, i, py_layer); // Steals reference + } + + Py_DECREF(color_layer_type); + Py_DECREF(tile_layer_type); + return list; +} + +PyObject* UIGrid::py_layer(PyUIGridObject* self, PyObject* args) { + int z_index; + if (!PyArg_ParseTuple(args, "i", &z_index)) { + return NULL; + } + + for (auto& layer : self->data->layers) { + if (layer->z_index == z_index) { + auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); + if (!mcrfpy_module) return NULL; + + if (layer->type == GridLayerType::Color) { + auto* type = (PyTypeObject*)PyObject_GetAttrString(mcrfpy_module, "ColorLayer"); + Py_DECREF(mcrfpy_module); + if (!type) return NULL; + + PyColorLayerObject* obj = (PyColorLayerObject*)type->tp_alloc(type, 0); + Py_DECREF(type); + if (!obj) return NULL; + + obj->data = std::static_pointer_cast(layer); + obj->grid = self->data; + return (PyObject*)obj; + } else { + auto* type = (PyTypeObject*)PyObject_GetAttrString(mcrfpy_module, "TileLayer"); + Py_DECREF(mcrfpy_module); + if (!type) return NULL; + + PyTileLayerObject* obj = (PyTileLayerObject*)type->tp_alloc(type, 0); + Py_DECREF(type); + if (!obj) return NULL; + + obj->data = std::static_pointer_cast(layer); + obj->grid = self->data; + return (PyObject*)obj; + } + } + } + + Py_RETURN_NONE; +} + PyMethodDef UIGrid::methods[] = { {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, - {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, - "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n" - "Compute field of view from a position and return visible cells.\n\n" + {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, + "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" + "Compute field of view from a position.\n\n" "Args:\n" " x: X coordinate of the viewer\n" " y: Y coordinate of the viewer\n" " radius: Maximum view distance (0 = unlimited)\n" " light_walls: Whether walls are lit when visible\n" " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n" - "Returns:\n" - " List of tuples (x, y, visible, discovered) for all visible cells:\n" - " - x, y: Grid coordinates\n" - " - visible: True (all returned cells are visible)\n" - " - discovered: True (FOV implies discovery)\n\n" - "Also updates the internal FOV state for use with is_in_fov()."}, + "Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."}, {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, "is_in_fov(x: int, y: int) -> bool\n\n" "Check if a cell is in the field of view.\n\n" @@ -1379,6 +1626,12 @@ PyMethodDef UIGrid::methods[] = { "Returns:\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n" "Alternative A* implementation. Prefer find_path() for consistency."}, + {"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS, + "add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer"}, + {"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS, + "remove_layer(layer: ColorLayer | TileLayer) -> None"}, + {"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS, + "layer(z_index: int) -> ColorLayer | TileLayer | None"}, {NULL, NULL, 0, NULL} }; @@ -1389,21 +1642,16 @@ typedef PyUIGridObject PyObjectType; PyMethodDef UIGrid_all_methods[] = { UIDRAWABLE_METHODS, {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, - {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, - "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n" - "Compute field of view from a position and return visible cells.\n\n" + {"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS, + "compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n" + "Compute field of view from a position.\n\n" "Args:\n" " x: X coordinate of the viewer\n" " y: Y coordinate of the viewer\n" " radius: Maximum view distance (0 = unlimited)\n" " light_walls: Whether walls are lit when visible\n" " algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n" - "Returns:\n" - " List of tuples (x, y, visible, discovered) for all visible cells:\n" - " - x, y: Grid coordinates\n" - " - visible: True (all returned cells are visible)\n" - " - discovered: True (FOV implies discovery)\n\n" - "Also updates the internal FOV state for use with is_in_fov()."}, + "Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."}, {"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS, "is_in_fov(x: int, y: int) -> bool\n\n" "Check if a cell is in the field of view.\n\n" @@ -1465,6 +1713,27 @@ PyMethodDef UIGrid_all_methods[] = { "Returns:\n" " List of (x, y) tuples representing the path, empty list if no path exists\n\n" "Alternative A* implementation. Prefer find_path() for consistency."}, + {"add_layer", (PyCFunction)UIGrid::py_add_layer, METH_VARARGS | METH_KEYWORDS, + "add_layer(type: str, z_index: int = -1, texture: Texture = None) -> ColorLayer | TileLayer\n\n" + "Add a new layer to the grid.\n\n" + "Args:\n" + " type: Layer type ('color' or 'tile')\n" + " z_index: Render order. Negative = below entities, >= 0 = above entities. Default: -1\n" + " texture: Texture for tile layers. Required for 'tile' type.\n\n" + "Returns:\n" + " The created ColorLayer or TileLayer object."}, + {"remove_layer", (PyCFunction)UIGrid::py_remove_layer, METH_VARARGS, + "remove_layer(layer: ColorLayer | TileLayer) -> None\n\n" + "Remove a layer from the grid.\n\n" + "Args:\n" + " layer: The layer to remove."}, + {"layer", (PyCFunction)UIGrid::py_layer, METH_VARARGS, + "layer(z_index: int) -> ColorLayer | TileLayer | None\n\n" + "Get a layer by its z_index.\n\n" + "Args:\n" + " z_index: The z_index of the layer to find.\n\n" + "Returns:\n" + " The layer with the specified z_index, or None if not found."}, {NULL} // Sentinel }; @@ -1481,6 +1750,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"entities", (getter)UIGrid::get_entities, NULL, "EntityCollection of entities on this grid", NULL}, {"children", (getter)UIGrid::get_children, NULL, "UICollection of UIDrawable children (speech bubbles, effects, overlays)", NULL}, + {"layers", (getter)UIGrid::get_layers, NULL, "List of grid layers (ColorLayer, TileLayer) sorted by z_index", NULL}, {"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner X-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 0)}, {"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner Y-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 1)}, diff --git a/src/UIGrid.h b/src/UIGrid.h index e1c41cf..82b66d6 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -20,6 +20,7 @@ #include "UIEntity.h" #include "UIDrawable.h" #include "UIBase.h" +#include "GridLayers.h" class UIGrid: public UIDrawable { @@ -81,6 +82,16 @@ public: std::shared_ptr>> children; bool children_need_sort = true; // Dirty flag for z_index sorting + // Dynamic layer system (#147) + std::vector> layers; + bool layers_need_sort = true; // Dirty flag for z_index sorting + + // Layer management + std::shared_ptr addColorLayer(int z_index); + std::shared_ptr addTileLayer(int z_index, std::shared_ptr texture = nullptr); + void removeLayer(std::shared_ptr layer); + void sortLayers(); + // Background rendering sf::Color fill_color; @@ -147,7 +158,12 @@ public: static PyObject* get_on_cell_click(PyUIGridObject* self, void* closure); static int set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure); static PyObject* get_hovered_cell(PyUIGridObject* self, void* closure); - + + // #147 - Layer system Python API + static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds); + static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args); + static PyObject* get_layers(PyUIGridObject* self, void* closure); + static PyObject* py_layer(PyUIGridObject* self, PyObject* args); }; typedef struct { diff --git a/tests/regression/issue_147_grid_layers.py b/tests/regression/issue_147_grid_layers.py new file mode 100644 index 0000000..c3e5d6e --- /dev/null +++ b/tests/regression/issue_147_grid_layers.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +Regression test for issue #147: Dynamic Layer System for Grid + +Tests: +1. ColorLayer creation and manipulation +2. TileLayer creation and manipulation +3. Layer z_index ordering relative to entities +4. Layer management (add_layer, remove_layer, layers property) +""" +import mcrfpy +import sys + +def run_test(runtime): + print("=" * 60) + print("Issue #147 Regression Test: Dynamic Layer System for Grid") + print("=" * 60) + + # Create test scene + mcrfpy.createScene("test") + ui = mcrfpy.sceneUI("test") + texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) + + # Create grid + grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15), texture=texture) + ui.append(grid) + + print("\n--- Test 1: Initial state (no layers) ---") + if len(grid.layers) == 0: + print(" PASS: Grid starts with no layers") + else: + print(f" FAIL: Expected 0 layers, got {len(grid.layers)}") + sys.exit(1) + + print("\n--- Test 2: Add ColorLayer ---") + color_layer = grid.add_layer("color", z_index=-1) + print(f" Created: {color_layer}") + if color_layer is not None: + print(" PASS: ColorLayer created") + else: + print(" FAIL: ColorLayer creation returned None") + sys.exit(1) + + # Test ColorLayer properties + if color_layer.z_index == -1: + print(" PASS: ColorLayer z_index is -1") + else: + print(f" FAIL: Expected z_index -1, got {color_layer.z_index}") + sys.exit(1) + + if color_layer.visible: + print(" PASS: ColorLayer is visible by default") + else: + print(" FAIL: ColorLayer should be visible by default") + sys.exit(1) + + grid_size = color_layer.grid_size + if grid_size == (20, 15): + print(f" PASS: ColorLayer grid_size is {grid_size}") + else: + print(f" FAIL: Expected (20, 15), got {grid_size}") + sys.exit(1) + + print("\n--- Test 3: ColorLayer cell access ---") + # Set a color + color_layer.set(5, 5, mcrfpy.Color(255, 0, 0, 128)) + color = color_layer.at(5, 5) + if color.r == 255 and color.g == 0 and color.b == 0 and color.a == 128: + print(f" PASS: Color at (5,5) is {color.r}, {color.g}, {color.b}, {color.a}") + else: + print(f" FAIL: Color mismatch") + sys.exit(1) + + # Fill entire layer + color_layer.fill(mcrfpy.Color(0, 0, 255, 64)) + color = color_layer.at(0, 0) + if color.b == 255 and color.a == 64: + print(" PASS: ColorLayer fill works") + else: + print(" FAIL: ColorLayer fill did not work") + sys.exit(1) + + print("\n--- Test 4: Add TileLayer ---") + tile_layer = grid.add_layer("tile", z_index=-2, texture=texture) + print(f" Created: {tile_layer}") + if tile_layer is not None: + print(" PASS: TileLayer created") + else: + print(" FAIL: TileLayer creation returned None") + sys.exit(1) + + if tile_layer.z_index == -2: + print(" PASS: TileLayer z_index is -2") + else: + print(f" FAIL: Expected z_index -2, got {tile_layer.z_index}") + sys.exit(1) + + print("\n--- Test 5: TileLayer cell access ---") + # Set a tile + tile_layer.set(3, 3, 42) + tile = tile_layer.at(3, 3) + if tile == 42: + print(f" PASS: Tile at (3,3) is {tile}") + else: + print(f" FAIL: Expected 42, got {tile}") + sys.exit(1) + + # Fill entire layer + tile_layer.fill(10) + tile = tile_layer.at(0, 0) + if tile == 10: + print(" PASS: TileLayer fill works") + else: + print(" FAIL: TileLayer fill did not work") + sys.exit(1) + + print("\n--- Test 6: Layer ordering ---") + layers = grid.layers + if len(layers) == 2: + print(f" PASS: Grid has 2 layers") + else: + print(f" FAIL: Expected 2 layers, got {len(layers)}") + sys.exit(1) + + # Layers should be sorted by z_index + if layers[0].z_index <= layers[1].z_index: + print(f" PASS: Layers sorted by z_index ({layers[0].z_index}, {layers[1].z_index})") + else: + print(f" FAIL: Layers not sorted") + sys.exit(1) + + print("\n--- Test 7: Get layer by z_index ---") + layer = grid.layer(-1) + if layer is not None and layer.z_index == -1: + print(" PASS: grid.layer(-1) returns ColorLayer") + else: + print(" FAIL: Could not get layer by z_index") + sys.exit(1) + + layer = grid.layer(-2) + if layer is not None and layer.z_index == -2: + print(" PASS: grid.layer(-2) returns TileLayer") + else: + print(" FAIL: Could not get layer by z_index") + sys.exit(1) + + layer = grid.layer(999) + if layer is None: + print(" PASS: grid.layer(999) returns None for non-existent layer") + else: + print(" FAIL: Should return None for non-existent layer") + sys.exit(1) + + print("\n--- Test 8: Layer above entities (z_index >= 0) ---") + fog_layer = grid.add_layer("color", z_index=1) + if fog_layer.z_index == 1: + print(" PASS: Created layer with z_index=1 (above entities)") + else: + print(" FAIL: Layer z_index incorrect") + sys.exit(1) + + # Set fog + fog_layer.fill(mcrfpy.Color(0, 0, 0, 128)) + print(" PASS: Fog layer filled") + + print("\n--- Test 9: Remove layer ---") + initial_count = len(grid.layers) + grid.remove_layer(fog_layer) + final_count = len(grid.layers) + if final_count == initial_count - 1: + print(f" PASS: Layer removed ({initial_count} -> {final_count})") + else: + print(f" FAIL: Layer count didn't decrease ({initial_count} -> {final_count})") + sys.exit(1) + + print("\n--- Test 10: Layer visibility toggle ---") + color_layer.visible = False + if not color_layer.visible: + print(" PASS: Layer visibility can be toggled") + else: + print(" FAIL: Layer visibility toggle failed") + sys.exit(1) + color_layer.visible = True + + print("\n" + "=" * 60) + print("All tests PASSED") + print("=" * 60) + sys.exit(0) + +# Initialize and run +mcrfpy.createScene("init") +mcrfpy.setScene("init") +mcrfpy.setTimer("test", run_test, 100)