feat: Add dynamic layer system for Grid (closes #147)

Implements ColorLayer and TileLayer classes with z_index ordering:
- ColorLayer: stores RGBA color per cell for overlays, fog of war, etc.
- TileLayer: stores sprite index per cell with optional texture
- z_index < 0: renders below entities
- z_index >= 0: renders above entities

Python API:
- grid.add_layer(type, z_index, texture) - create layer
- grid.remove_layer(layer) - remove layer
- grid.layers - list of layers sorted by z_index
- grid.layer(z_index) - get layer by z_index
- layer.at(x,y) / layer.set(x,y,value) - cell access
- layer.fill(value) - fill entire layer

Layers are allocated separately from UIGridPoint, reducing memory
for grids that don't need all features. Base grid retains walkable/
transparent arrays for TCOD pathfinding.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-11-28 21:35:38 -05:00
parent f769c6c5f5
commit 4b05a95efe
6 changed files with 1342 additions and 19 deletions

621
src/GridLayers.cpp Normal file
View File

@ -0,0 +1,621 @@
#include "GridLayers.h"
#include "UIGrid.h"
#include "PyColor.h"
#include "PyTexture.h"
#include <sstream>
// =============================================================================
// 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<sf::Color> 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<PyTexture> 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<int> 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<char**>(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<ColorLayer>(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 << "<ColorLayer (invalid)>";
} else {
ss << "<ColorLayer z_index=" << self->data->z_index
<< " size=(" << self->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<char**>(kwlist),
&z_index, &texture_obj, &grid_size_obj)) {
return -1;
}
// Parse texture
std::shared_ptr<PyTexture> 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<TileLayer>(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 << "<TileLayer (invalid)>";
} else {
ss << "<TileLayer z_index=" << self->data->z_index
<< " size=(" << self->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());
}

219
src/GridLayers.h Normal file
View File

@ -0,0 +1,219 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include <SFML/Graphics.hpp>
#include <memory>
#include <vector>
// 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<sf::Color> 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<int> tiles; // Sprite indices (-1 = no tile)
std::shared_ptr<PyTexture> texture;
TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
std::shared_ptr<PyTexture> 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<GridLayer> data;
std::shared_ptr<UIGrid> grid; // Parent grid reference
} PyGridLayerObject;
typedef struct {
PyObject_HEAD
std::shared_ptr<ColorLayer> data;
std::shared_ptr<UIGrid> grid;
} PyColorLayerObject;
typedef struct {
PyObject_HEAD
std::shared_ptr<TileLayer> data;
std::shared_ptr<UIGrid> 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;
}
};
}

View File

@ -15,6 +15,7 @@
#include "UILine.h"
#include "UICircle.h"
#include "UIArc.h"
#include "GridLayers.h"
#include "Resources.h"
#include "PyScene.h"
#include <filesystem>
@ -303,6 +304,9 @@ PyObject* PyInit_mcrfpy()
/*game map & perspective data*/
&PyUIGridPointType, &PyUIGridPointStateType,
/*grid layers (#147)*/
&PyColorLayerType, &PyTileLayerType,
/*collections & iterators*/
&PyUICollectionType, &PyUICollectionIterType,
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,

View File

@ -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<ColorLayer> UIGrid::addColorLayer(int z_index) {
auto layer = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, this);
layers.push_back(layer);
layers_need_sort = true;
return layer;
}
std::shared_ptr<TileLayer> UIGrid::addTileLayer(int z_index, std::shared_ptr<PyTexture> texture) {
auto layer = std::make_shared<TileLayer>(z_index, grid_x, grid_y, this, texture);
layers.push_back(layer);
layers_need_sort = true;
return layer;
}
void UIGrid::removeLayer(std::shared_ptr<GridLayer> 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<char**>(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<PyTexture> 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<ColorLayer>(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<TileLayer>(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<ColorLayer>(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<TileLayer>(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(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}
};
@ -1390,20 +1643,15 @@ 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(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)},

View File

@ -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<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true; // Dirty flag for z_index sorting
// Dynamic layer system (#147)
std::vector<std::shared_ptr<GridLayer>> layers;
bool layers_need_sort = true; // Dirty flag for z_index sorting
// Layer management
std::shared_ptr<ColorLayer> addColorLayer(int z_index);
std::shared_ptr<TileLayer> addTileLayer(int z_index, std::shared_ptr<PyTexture> texture = nullptr);
void removeLayer(std::shared_ptr<GridLayer> layer);
void sortLayers();
// Background rendering
sf::Color fill_color;
@ -148,6 +159,11 @@ public:
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 {

View File

@ -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)