#include "GridLayers.h" #include "UIGrid.h" #include "UIEntity.h" #include "PyColor.h" #include "PyTexture.h" #include "PyFOV.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), chunks_x(0), chunks_y(0), cached_cell_width(0), cached_cell_height(0) { initChunks(); } void GridLayer::initChunks() { // Calculate chunk dimensions chunks_x = (grid_x + CHUNK_SIZE - 1) / CHUNK_SIZE; chunks_y = (grid_y + CHUNK_SIZE - 1) / CHUNK_SIZE; int total_chunks = chunks_x * chunks_y; // Initialize per-chunk tracking chunk_dirty.assign(total_chunks, true); // All chunks start dirty chunk_texture_initialized.assign(total_chunks, false); chunk_textures.clear(); chunk_textures.reserve(total_chunks); for (int i = 0; i < total_chunks; ++i) { chunk_textures.push_back(std::make_unique()); } } void GridLayer::markDirty() { // Mark ALL chunks as dirty std::fill(chunk_dirty.begin(), chunk_dirty.end(), true); } void GridLayer::markDirty(int cell_x, int cell_y) { // Mark only the specific chunk containing this cell if (cell_x < 0 || cell_x >= grid_x || cell_y < 0 || cell_y >= grid_y) return; int chunk_idx = getChunkIndex(cell_x, cell_y); if (chunk_idx >= 0 && chunk_idx < static_cast(chunk_dirty.size())) { chunk_dirty[chunk_idx] = true; } } int GridLayer::getChunkIndex(int cell_x, int cell_y) const { int cx = cell_x / CHUNK_SIZE; int cy = cell_y / CHUNK_SIZE; return cy * chunks_x + cx; } void GridLayer::getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const { chunk_x = cell_x / CHUNK_SIZE; chunk_y = cell_y / CHUNK_SIZE; } void GridLayer::getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const { start_x = chunk_x * CHUNK_SIZE; start_y = chunk_y * CHUNK_SIZE; end_x = std::min(start_x + CHUNK_SIZE, grid_x); end_y = std::min(start_y + CHUNK_SIZE, grid_y); } void GridLayer::ensureChunkTexture(int chunk_idx, int cell_width, int cell_height) { if (chunk_idx < 0 || chunk_idx >= static_cast(chunk_textures.size())) return; if (!chunk_textures[chunk_idx]) return; // Calculate chunk dimensions in cells int cx = chunk_idx % chunks_x; int cy = chunk_idx / chunks_x; int start_x, start_y, end_x, end_y; getChunkBounds(cx, cy, start_x, start_y, end_x, end_y); int chunk_width_cells = end_x - start_x; int chunk_height_cells = end_y - start_y; unsigned int required_width = chunk_width_cells * cell_width; unsigned int required_height = chunk_height_cells * cell_height; // Check if texture needs (re)creation if (chunk_texture_initialized[chunk_idx] && chunk_textures[chunk_idx]->getSize().x == required_width && chunk_textures[chunk_idx]->getSize().y == required_height && cached_cell_width == cell_width && cached_cell_height == cell_height) { return; // Already properly sized } // Create the texture for this chunk if (!chunk_textures[chunk_idx]->create(required_width, required_height)) { chunk_texture_initialized[chunk_idx] = false; return; } chunk_texture_initialized[chunk_idx] = true; chunk_dirty[chunk_idx] = true; // Force re-render after resize cached_cell_width = cell_width; cached_cell_height = cell_height; } // ============================================================================= // 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), perspective_visible(255, 255, 200, 64), perspective_discovered(100, 100, 100, 128), perspective_unknown(0, 0, 0, 255), has_perspective(false) {} 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); markDirty(); // Mark ALL chunks for re-render } void ColorLayer::fillRect(int x, int y, int width, int height, const sf::Color& color) { // Clamp to valid bounds int x1 = std::max(0, x); int y1 = std::max(0, y); int x2 = std::min(grid_x, x + width); int y2 = std::min(grid_y, y + height); // Fill the rectangle for (int fy = y1; fy < y2; ++fy) { for (int fx = x1; fx < x2; ++fx) { colors[fy * grid_x + fx] = color; } } // Mark affected chunks dirty int chunk_x1 = x1 / CHUNK_SIZE; int chunk_y1 = y1 / CHUNK_SIZE; int chunk_x2 = (x2 - 1) / CHUNK_SIZE; int chunk_y2 = (y2 - 1) / CHUNK_SIZE; for (int cy = chunk_y1; cy <= chunk_y2; ++cy) { for (int cx = chunk_x1; cx <= chunk_x2; ++cx) { int idx = cy * chunks_x + cx; if (idx >= 0 && idx < static_cast(chunk_dirty.size())) { chunk_dirty[idx] = true; } } } } void ColorLayer::drawFOV(int source_x, int source_y, int radius, TCOD_fov_algorithm_t algorithm, const sf::Color& visible_color, const sf::Color& discovered_color, const sf::Color& unknown_color) { // Need parent grid for TCOD map access if (!parent_grid) { return; // Cannot compute FOV without parent grid } // Import UIGrid here to avoid circular dependency in header // parent_grid is already a UIGrid*, we can use its tcod_map directly // But we need to forward declare access to it... // Compute FOV on the parent grid parent_grid->computeFOV(source_x, source_y, radius, true, algorithm); // Paint cells based on visibility for (int cy = 0; cy < grid_y; ++cy) { for (int cx = 0; cx < grid_x; ++cx) { // Check if in FOV (visible right now) if (parent_grid->isInFOV(cx, cy)) { colors[cy * grid_x + cx] = visible_color; } // Check if previously discovered (current color != unknown) else if (colors[cy * grid_x + cx] != unknown_color) { colors[cy * grid_x + cx] = discovered_color; } // Otherwise leave as unknown (or set to unknown if first time) else { colors[cy * grid_x + cx] = unknown_color; } } } // Mark entire layer dirty markDirty(); } void ColorLayer::applyPerspective(std::shared_ptr entity, const sf::Color& visible, const sf::Color& discovered, const sf::Color& unknown) { perspective_entity = entity; perspective_visible = visible; perspective_discovered = discovered; perspective_unknown = unknown; has_perspective = true; // Initial draw based on entity's current position updatePerspective(); } void ColorLayer::updatePerspective() { if (!has_perspective) return; auto entity = perspective_entity.lock(); if (!entity) { // Entity was deleted, clear perspective has_perspective = false; return; } if (!parent_grid) return; // Get entity position and grid's FOV settings int source_x = static_cast(entity->position.x); int source_y = static_cast(entity->position.y); int radius = parent_grid->fov_radius; TCOD_fov_algorithm_t algorithm = parent_grid->fov_algorithm; // Use drawFOV with our stored colors drawFOV(source_x, source_y, radius, algorithm, perspective_visible, perspective_discovered, perspective_unknown); } void ColorLayer::clearPerspective() { perspective_entity.reset(); has_perspective = false; } 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; // Reinitialize chunks for new dimensions initChunks(); } // Render a single chunk to its cached texture void ColorLayer::renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) { int chunk_idx = chunk_y * chunks_x + chunk_x; if (chunk_idx < 0 || chunk_idx >= static_cast(chunk_textures.size())) return; if (!chunk_textures[chunk_idx]) return; ensureChunkTexture(chunk_idx, cell_width, cell_height); if (!chunk_texture_initialized[chunk_idx]) return; // Get chunk bounds int start_x, start_y, end_x, end_y; getChunkBounds(chunk_x, chunk_y, start_x, start_y, end_x, end_y); chunk_textures[chunk_idx]->clear(sf::Color::Transparent); sf::RectangleShape rect; rect.setSize(sf::Vector2f(cell_width, cell_height)); rect.setOutlineThickness(0); // Render only cells within this chunk (local coordinates in texture) for (int x = start_x; x < end_x; ++x) { for (int y = start_y; y < end_y; ++y) { const sf::Color& color = at(x, y); if (color.a == 0) continue; // Skip fully transparent // Position relative to chunk origin rect.setPosition(sf::Vector2f((x - start_x) * cell_width, (y - start_y) * cell_height)); rect.setFillColor(color); chunk_textures[chunk_idx]->draw(rect); } } chunk_textures[chunk_idx]->display(); chunk_dirty[chunk_idx] = false; } // Legacy: render all chunks (used by fill, resize, etc.) void ColorLayer::renderToTexture(int cell_width, int cell_height) { for (int cy = 0; cy < chunks_y; ++cy) { for (int cx = 0; cx < chunks_x; ++cx) { renderChunkToTexture(cx, cy, cell_width, cell_height); } } } 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; // Calculate visible chunk range int chunk_left = std::max(0, left_edge / CHUNK_SIZE); int chunk_top = std::max(0, top_edge / CHUNK_SIZE); int chunk_right = std::min(chunks_x - 1, (x_limit + CHUNK_SIZE - 1) / CHUNK_SIZE); int chunk_bottom = std::min(chunks_y - 1, (y_limit + CHUNK_SIZE - 1) / CHUNK_SIZE); // Iterate only over visible chunks for (int cy = chunk_top; cy <= chunk_bottom; ++cy) { for (int cx = chunk_left; cx <= chunk_right; ++cx) { int chunk_idx = cy * chunks_x + cx; // Re-render chunk only if dirty AND visible if (chunk_dirty[chunk_idx] || !chunk_texture_initialized[chunk_idx]) { renderChunkToTexture(cx, cy, cell_width, cell_height); } if (!chunk_texture_initialized[chunk_idx]) { // Fallback: direct rendering for this chunk int start_x, start_y, end_x, end_y; getChunkBounds(cx, cy, start_x, start_y, end_x, end_y); sf::RectangleShape rect; rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom)); rect.setOutlineThickness(0); for (int x = start_x; x < end_x; ++x) { for (int y = start_y; y < end_y; ++y) { const sf::Color& color = at(x, y); if (color.a == 0) continue; 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); } } continue; } // Blit this chunk's texture to target int start_x, start_y, end_x, end_y; getChunkBounds(cx, cy, start_x, start_y, end_x, end_y); // Chunk position in world pixel coordinates float chunk_world_x = start_x * cell_width; float chunk_world_y = start_y * cell_height; // Position in target (accounting for viewport offset and zoom) float dest_x = (chunk_world_x - left_spritepixels) * zoom; float dest_y = (chunk_world_y - top_spritepixels) * zoom; sf::Sprite chunk_sprite(chunk_textures[chunk_idx]->getTexture()); chunk_sprite.setPosition(sf::Vector2f(dest_x, dest_y)); chunk_sprite.setScale(sf::Vector2f(zoom, zoom)); target.draw(chunk_sprite); } } } // ============================================================================= // 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); markDirty(); // Mark ALL chunks for re-render } void TileLayer::fillRect(int x, int y, int width, int height, int tile_index) { // Clamp to valid bounds int x1 = std::max(0, x); int y1 = std::max(0, y); int x2 = std::min(grid_x, x + width); int y2 = std::min(grid_y, y + height); // Fill the rectangle for (int fy = y1; fy < y2; ++fy) { for (int fx = x1; fx < x2; ++fx) { tiles[fy * grid_x + fx] = tile_index; } } // Mark affected chunks dirty int chunk_x1 = x1 / CHUNK_SIZE; int chunk_y1 = y1 / CHUNK_SIZE; int chunk_x2 = (x2 - 1) / CHUNK_SIZE; int chunk_y2 = (y2 - 1) / CHUNK_SIZE; for (int cy = chunk_y1; cy <= chunk_y2; ++cy) { for (int cx = chunk_x1; cx <= chunk_x2; ++cx) { int idx = cy * chunks_x + cx; if (idx >= 0 && idx < static_cast(chunk_dirty.size())) { chunk_dirty[idx] = true; } } } } 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; // Reinitialize chunks for new dimensions initChunks(); } // Render a single chunk to its cached texture void TileLayer::renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) { if (!texture) return; int chunk_idx = chunk_y * chunks_x + chunk_x; if (chunk_idx < 0 || chunk_idx >= static_cast(chunk_textures.size())) return; if (!chunk_textures[chunk_idx]) return; ensureChunkTexture(chunk_idx, cell_width, cell_height); if (!chunk_texture_initialized[chunk_idx]) return; // Get chunk bounds int start_x, start_y, end_x, end_y; getChunkBounds(chunk_x, chunk_y, start_x, start_y, end_x, end_y); chunk_textures[chunk_idx]->clear(sf::Color::Transparent); // Render only tiles within this chunk (local coordinates in texture) for (int x = start_x; x < end_x; ++x) { for (int y = start_y; y < end_y; ++y) { int tile_index = at(x, y); if (tile_index < 0) continue; // No tile // Position relative to chunk origin auto pixel_pos = sf::Vector2f((x - start_x) * cell_width, (y - start_y) * cell_height); sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f)); chunk_textures[chunk_idx]->draw(sprite); } } chunk_textures[chunk_idx]->display(); chunk_dirty[chunk_idx] = false; } // Legacy: render all chunks (used by fill, resize, etc.) void TileLayer::renderToTexture(int cell_width, int cell_height) { for (int cy = 0; cy < chunks_y; ++cy) { for (int cx = 0; cx < chunks_x; ++cx) { renderChunkToTexture(cx, cy, cell_width, cell_height); } } } 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; // Calculate visible chunk range int chunk_left = std::max(0, left_edge / CHUNK_SIZE); int chunk_top = std::max(0, top_edge / CHUNK_SIZE); int chunk_right = std::min(chunks_x - 1, (x_limit + CHUNK_SIZE - 1) / CHUNK_SIZE); int chunk_bottom = std::min(chunks_y - 1, (y_limit + CHUNK_SIZE - 1) / CHUNK_SIZE); // Iterate only over visible chunks for (int cy = chunk_top; cy <= chunk_bottom; ++cy) { for (int cx = chunk_left; cx <= chunk_right; ++cx) { int chunk_idx = cy * chunks_x + cx; // Re-render chunk only if dirty AND visible if (chunk_dirty[chunk_idx] || !chunk_texture_initialized[chunk_idx]) { renderChunkToTexture(cx, cy, cell_width, cell_height); } if (!chunk_texture_initialized[chunk_idx]) { // Fallback: direct rendering for this chunk int start_x, start_y, end_x, end_y; getChunkBounds(cx, cy, start_x, start_y, end_x, end_y); for (int x = start_x; x < end_x; ++x) { for (int y = start_y; y < end_y; ++y) { int tile_index = at(x, y); if (tile_index < 0) continue; 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); } } continue; } // Blit this chunk's texture to target int start_x, start_y, end_x, end_y; getChunkBounds(cx, cy, start_x, start_y, end_x, end_y); // Chunk position in world pixel coordinates float chunk_world_x = start_x * cell_width; float chunk_world_y = start_y * cell_height; // Position in target (accounting for viewport offset and zoom) float dest_x = (chunk_world_x - left_spritepixels) * zoom; float dest_y = (chunk_world_y - top_spritepixels) * zoom; sf::Sprite chunk_sprite(chunk_textures[chunk_idx]->getTexture()); chunk_sprite.setPosition(sf::Vector2f(dest_x, dest_y)); chunk_sprite.setScale(sf::Vector2f(zoom, zoom)); target.draw(chunk_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."}, {"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS, "fill_rect(pos, size, color)\n\n" "Fill a rectangular region with a color.\n\n" "Args:\n" " pos (tuple): Top-left corner as (x, y)\n" " size (tuple): Dimensions as (width, height)\n" " color: Color object or (r, g, b[, a]) tuple"}, {"draw_fov", (PyCFunction)PyGridLayerAPI::ColorLayer_draw_fov, METH_VARARGS | METH_KEYWORDS, "draw_fov(source, radius=None, fov=None, visible=None, discovered=None, unknown=None)\n\n" "Paint cells based on field-of-view visibility from source position.\n\n" "Args:\n" " source (tuple): FOV origin as (x, y)\n" " radius (int): FOV radius. Default: grid's fov_radius\n" " fov (FOV): FOV algorithm. Default: grid's fov setting\n" " visible (Color): Color for currently visible cells\n" " discovered (Color): Color for previously seen cells\n" " unknown (Color): Color for never-seen cells\n\n" "Note: Layer must be attached to a grid for FOV calculation."}, {"apply_perspective", (PyCFunction)PyGridLayerAPI::ColorLayer_apply_perspective, METH_VARARGS | METH_KEYWORDS, "apply_perspective(entity, visible=None, discovered=None, unknown=None)\n\n" "Bind this layer to an entity for automatic FOV updates.\n\n" "Args:\n" " entity (Entity): The entity whose perspective to track\n" " visible (Color): Color for currently visible cells\n" " discovered (Color): Color for previously seen cells\n" " unknown (Color): Color for never-seen cells\n\n" "After binding, call update_perspective() when the entity moves."}, {"update_perspective", (PyCFunction)PyGridLayerAPI::ColorLayer_update_perspective, METH_NOARGS, "update_perspective()\n\n" "Redraw FOV based on the bound entity's current position.\n\n" "Call this after the entity moves to update the visibility layer."}, {"clear_perspective", (PyCFunction)PyGridLayerAPI::ColorLayer_clear_perspective, METH_NOARGS, "clear_perspective()\n\n" "Remove the perspective binding from this layer."}, {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; self->data->markDirty(x, y); // Mark only the affected chunk 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_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = {"pos", "size", "color", NULL}; PyObject* pos_obj; PyObject* size_obj; PyObject* color_obj; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO", const_cast(kwlist), &pos_obj, &size_obj, &color_obj)) { return NULL; } if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); return NULL; } // Parse pos int x, y; if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0)); y = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1)); if (PyErr_Occurred()) return NULL; } else { PyErr_SetString(PyExc_TypeError, "pos must be a (x, y) tuple"); return NULL; } // Parse size int width, height; if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { width = PyLong_AsLong(PyTuple_GetItem(size_obj, 0)); height = PyLong_AsLong(PyTuple_GetItem(size_obj, 1)); if (PyErr_Occurred()) return NULL; } else { PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple"); 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->fillRect(x, y, width, height, color); Py_RETURN_NONE; } PyObject* PyGridLayerAPI::ColorLayer_draw_fov(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = {"source", "radius", "fov", "visible", "discovered", "unknown", NULL}; PyObject* source_obj; int radius = -1; // -1 means use grid's default PyObject* fov_obj = Py_None; PyObject* visible_obj = nullptr; PyObject* discovered_obj = nullptr; PyObject* unknown_obj = nullptr; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|iOOOO", const_cast(kwlist), &source_obj, &radius, &fov_obj, &visible_obj, &discovered_obj, &unknown_obj)) { return NULL; } if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); return NULL; } if (!self->grid) { PyErr_SetString(PyExc_RuntimeError, "Layer is not attached to a grid"); return NULL; } // Parse source position int source_x, source_y; if (PyTuple_Check(source_obj) && PyTuple_Size(source_obj) == 2) { source_x = PyLong_AsLong(PyTuple_GetItem(source_obj, 0)); source_y = PyLong_AsLong(PyTuple_GetItem(source_obj, 1)); if (PyErr_Occurred()) return NULL; } else { PyErr_SetString(PyExc_TypeError, "source must be a (x, y) tuple"); return NULL; } // Get radius from grid if not specified if (radius < 0) { radius = self->grid->fov_radius; } // Get FOV algorithm TCOD_fov_algorithm_t algorithm; bool was_none = false; if (!PyFOV::from_arg(fov_obj, &algorithm, &was_none)) { return NULL; } if (was_none) { algorithm = self->grid->fov_algorithm; } // Helper lambda to parse color auto parse_color = [](PyObject* obj, sf::Color& out, const sf::Color& default_val, const char* name) -> bool { if (!obj || obj == Py_None) { out = default_val; return true; } auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); if (!mcrfpy_module) return false; auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color"); Py_DECREF(mcrfpy_module); if (!color_type) return false; if (PyObject_IsInstance(obj, color_type)) { out = ((PyColorObject*)obj)->data; Py_DECREF(color_type); return true; } else if (PyTuple_Check(obj)) { int r, g, b, a = 255; if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) { Py_DECREF(color_type); return false; } out = sf::Color(r, g, b, a); Py_DECREF(color_type); return true; } Py_DECREF(color_type); PyErr_Format(PyExc_TypeError, "%s must be a Color object or (r, g, b[, a]) tuple", name); return false; }; // Default colors for FOV visualization sf::Color visible_color(255, 255, 200, 64); // Light yellow tint sf::Color discovered_color(128, 128, 128, 128); // Gray sf::Color unknown_color(0, 0, 0, 255); // Black if (!parse_color(visible_obj, visible_color, visible_color, "visible")) return NULL; if (!parse_color(discovered_obj, discovered_color, discovered_color, "discovered")) return NULL; if (!parse_color(unknown_obj, unknown_color, unknown_color, "unknown")) return NULL; self->data->drawFOV(source_x, source_y, radius, algorithm, visible_color, discovered_color, unknown_color); Py_RETURN_NONE; } PyObject* PyGridLayerAPI::ColorLayer_apply_perspective(PyColorLayerObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = {"entity", "visible", "discovered", "unknown", NULL}; PyObject* entity_obj; PyObject* visible_obj = nullptr; PyObject* discovered_obj = nullptr; PyObject* unknown_obj = nullptr; if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast(kwlist), &entity_obj, &visible_obj, &discovered_obj, &unknown_obj)) { return NULL; } if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); return NULL; } if (!self->grid) { PyErr_SetString(PyExc_RuntimeError, "Layer is not attached to a grid"); return NULL; } // Get the Entity type auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); if (!mcrfpy_module) return NULL; auto* entity_type = PyObject_GetAttrString(mcrfpy_module, "Entity"); Py_DECREF(mcrfpy_module); if (!entity_type) return NULL; if (!PyObject_IsInstance(entity_obj, entity_type)) { Py_DECREF(entity_type); PyErr_SetString(PyExc_TypeError, "entity must be an Entity object"); return NULL; } Py_DECREF(entity_type); // Get the shared_ptr to the entity PyUIEntityObject* py_entity = (PyUIEntityObject*)entity_obj; if (!py_entity->data) { PyErr_SetString(PyExc_RuntimeError, "Entity has no data"); return NULL; } // Helper lambda to parse color auto parse_color = [](PyObject* obj, sf::Color& out, const sf::Color& default_val, const char* name) -> bool { if (!obj || obj == Py_None) { out = default_val; return true; } auto* mcrfpy_module = PyImport_ImportModule("mcrfpy"); if (!mcrfpy_module) return false; auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color"); Py_DECREF(mcrfpy_module); if (!color_type) return false; if (PyObject_IsInstance(obj, color_type)) { out = ((PyColorObject*)obj)->data; Py_DECREF(color_type); return true; } else if (PyTuple_Check(obj)) { int r, g, b, a = 255; if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) { Py_DECREF(color_type); return false; } out = sf::Color(r, g, b, a); Py_DECREF(color_type); return true; } Py_DECREF(color_type); PyErr_Format(PyExc_TypeError, "%s must be a Color object or (r, g, b[, a]) tuple", name); return false; }; // Parse colors with defaults sf::Color visible_color(255, 255, 200, 64); sf::Color discovered_color(100, 100, 100, 128); sf::Color unknown_color(0, 0, 0, 255); if (!parse_color(visible_obj, visible_color, visible_color, "visible")) return NULL; if (!parse_color(discovered_obj, discovered_color, discovered_color, "discovered")) return NULL; if (!parse_color(unknown_obj, unknown_color, unknown_color, "unknown")) return NULL; self->data->applyPerspective(py_entity->data, visible_color, discovered_color, unknown_color); Py_RETURN_NONE; } PyObject* PyGridLayerAPI::ColorLayer_update_perspective(PyColorLayerObject* self, PyObject* args) { if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); return NULL; } if (!self->data->has_perspective) { PyErr_SetString(PyExc_RuntimeError, "Layer has no perspective binding. Call apply_perspective() first."); return NULL; } self->data->updatePerspective(); Py_RETURN_NONE; } PyObject* PyGridLayerAPI::ColorLayer_clear_perspective(PyColorLayerObject* self, PyObject* args) { if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); return NULL; } self->data->clearPerspective(); 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."}, {"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS, "fill_rect(pos, size, index)\n\n" "Fill a rectangular region with a tile index.\n\n" "Args:\n" " pos (tuple): Top-left corner as (x, y)\n" " size (tuple): Dimensions as (width, height)\n" " index (int): Tile index to fill with (-1 for no tile)"}, {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; self->data->markDirty(x, y); // Mark only the affected chunk 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_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds) { static const char* kwlist[] = {"pos", "size", "index", NULL}; PyObject* pos_obj; PyObject* size_obj; int tile_index; if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi", const_cast(kwlist), &pos_obj, &size_obj, &tile_index)) { return NULL; } if (!self->data) { PyErr_SetString(PyExc_RuntimeError, "Layer has no data"); return NULL; } // Parse pos int x, y; if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0)); y = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1)); if (PyErr_Occurred()) return NULL; } else { PyErr_SetString(PyExc_TypeError, "pos must be a (x, y) tuple"); return NULL; } // Parse size int width, height; if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) { width = PyLong_AsLong(PyTuple_GetItem(size_obj, 0)); height = PyLong_AsLong(PyTuple_GetItem(size_obj, 1)); if (PyErr_Occurred()) return NULL; } else { PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple"); return NULL; } self->data->fillRect(x, y, width, height, tile_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(); self->data->markDirty(); // Mark ALL chunks for re-render (texture change affects all) 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; self->data->markDirty(); // Mark ALL chunks for re-render (texture change affects all) 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()); }