Phase 4.1: Extract GridData base class from UIGrid (#252, #270, #271, #277)

Extract all grid data members and methods into GridData base class.
UIGrid now inherits from both UIDrawable (rendering) and GridData (state).

- GridData holds: grid dimensions, cell storage (flat/chunked), entities,
  spatial hash, TCOD map, FOV state, Dijkstra caches, layers, cell
  callbacks, children collection
- GridData provides: at(), syncTCODMap/Cell(), computeFOV(), isInFOV(),
  layer management (add/remove/sort/getByName), initStorage()
- UIGrid retains: texture, box, sprites, renderTexture, camera (center,
  zoom, rotation), fill_color, perspective, cell hover/click dispatch,
  all Python API static methods, render()

Fix dangling parent_grid pointers: change UIGrid* to GridData* in
GridLayer, UIGridPoint, GridChunk, ChunkManager (closes #270, closes
#271, closes #277). All 258 tests pass unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-03-16 07:45:12 -04:00
commit 13d5512a41
9 changed files with 379 additions and 340 deletions

View file

@ -9,7 +9,7 @@
// =============================================================================
GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height,
int world_x, int world_y, UIGrid* parent)
int world_x, int world_y, GridData* parent)
: chunk_x(chunk_x), chunk_y(chunk_y),
width(width), height(height),
world_x(world_x), world_y(world_y),
@ -47,7 +47,7 @@ bool GridChunk::isVisible(float left_edge, float top_edge,
// ChunkManager implementation
// =============================================================================
ChunkManager::ChunkManager(int grid_x, int grid_y, UIGrid* parent)
ChunkManager::ChunkManager(int grid_x, int grid_y, GridData* parent)
: grid_x(grid_x), grid_y(grid_y), parent_grid(parent)
{
// Calculate number of chunks needed

View file

@ -6,6 +6,7 @@
// Forward declarations
class UIGrid;
class GridData;
class PyTexture;
/**
@ -36,11 +37,11 @@ public:
bool dirty;
// Parent grid reference
UIGrid* parent_grid;
GridData* parent_grid;
// Constructor
GridChunk(int chunk_x, int chunk_y, int width, int height,
int world_x, int world_y, UIGrid* parent);
int world_x, int world_y, GridData* parent);
// Access cell at local chunk coordinates
UIGridPoint& at(int local_x, int local_y);
@ -69,10 +70,10 @@ public:
std::vector<std::unique_ptr<GridChunk>> chunks;
// Parent grid
UIGrid* parent_grid;
GridData* parent_grid;
// Constructor - creates chunks for given grid dimensions
ChunkManager(int grid_x, int grid_y, UIGrid* parent);
ChunkManager(int grid_x, int grid_y, GridData* parent);
// Get chunk containing cell (x, y)
GridChunk* getChunkForCell(int x, int y);

184
src/GridData.cpp Normal file
View file

@ -0,0 +1,184 @@
// GridData.cpp - Pure data layer implementation (#252)
#include "GridData.h"
#include "UIEntity.h"
#include "PyTexture.h"
#include <algorithm>
GridData::GridData()
{
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
}
GridData::~GridData()
{
cleanupTCOD();
}
void GridData::cleanupTCOD()
{
dijkstra_maps.clear();
if (tcod_map) {
delete tcod_map;
tcod_map = nullptr;
}
}
void GridData::initStorage(int gx, int gy, GridData* parent_ref)
{
grid_w = gx;
grid_h = gy;
use_chunks = (gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD);
if (tcod_map) delete tcod_map;
tcod_map = new TCODMap(gx, gy);
if (use_chunks) {
chunk_manager = std::make_unique<ChunkManager>(gx, gy, parent_ref);
for (int cy = 0; cy < chunk_manager->chunks_y; ++cy) {
for (int cx = 0; cx < chunk_manager->chunks_x; ++cx) {
GridChunk* chunk = chunk_manager->getChunk(cx, cy);
if (!chunk) continue;
for (int ly = 0; ly < chunk->height; ++ly) {
for (int lx = 0; lx < chunk->width; ++lx) {
auto& cell = chunk->at(lx, ly);
cell.grid_x = chunk->world_x + lx;
cell.grid_y = chunk->world_y + ly;
cell.parent_grid = parent_ref;
}
}
}
}
} else {
points.resize(gx * gy);
for (int y = 0; y < gy; y++) {
for (int x = 0; x < gx; x++) {
int idx = y * gx + x;
points[idx].grid_x = x;
points[idx].grid_y = y;
points[idx].parent_grid = parent_ref;
}
}
}
syncTCODMap();
}
// Cell access
UIGridPoint& GridData::at(int x, int y)
{
if (use_chunks && chunk_manager) {
return chunk_manager->at(x, y);
}
return points[y * grid_w + x];
}
// TCOD integration
void GridData::syncTCODMap()
{
if (!tcod_map) return;
for (int y = 0; y < grid_h; y++) {
for (int x = 0; x < grid_w; x++) {
const UIGridPoint& point = at(x, y);
tcod_map->setProperties(x, y, point.transparent, point.walkable);
}
}
fov_dirty = true;
}
void GridData::syncTCODMapCell(int x, int y)
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return;
const UIGridPoint& point = at(x, y);
tcod_map->setProperties(x, y, point.transparent, point.walkable);
fov_dirty = true;
}
void GridData::computeFOV(int x, int y, int radius, bool light_walls, TCOD_fov_algorithm_t algo)
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return;
if (!fov_dirty &&
x == fov_last_x && y == fov_last_y &&
radius == fov_last_radius &&
light_walls == fov_last_light_walls &&
algo == fov_last_algo) {
return;
}
std::lock_guard<std::mutex> lock(fov_mutex);
tcod_map->computeFov(x, y, radius, light_walls, algo);
fov_dirty = false;
fov_last_x = x;
fov_last_y = y;
fov_last_radius = radius;
fov_last_light_walls = light_walls;
fov_last_algo = algo;
}
bool GridData::isInFOV(int x, int y) const
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return false;
std::lock_guard<std::mutex> lock(fov_mutex);
return tcod_map->isInFov(x, y);
}
// Layer management
std::shared_ptr<ColorLayer> GridData::addColorLayer(int z_index, const std::string& name)
{
auto layer = std::make_shared<ColorLayer>(z_index, grid_w, grid_h, this);
layer->name = name;
layers.push_back(layer);
layers_need_sort = true;
return layer;
}
std::shared_ptr<TileLayer> GridData::addTileLayer(int z_index, std::shared_ptr<PyTexture> texture, const std::string& name)
{
auto layer = std::make_shared<TileLayer>(z_index, grid_w, grid_h, this, texture);
layer->name = name;
layers.push_back(layer);
layers_need_sort = true;
return layer;
}
void GridData::removeLayer(std::shared_ptr<GridLayer> layer)
{
auto it = std::find(layers.begin(), layers.end(), layer);
if (it != layers.end()) {
layers.erase(it);
}
if (layer) {
layer->parent_grid = nullptr;
}
}
void GridData::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;
}
}
std::shared_ptr<GridLayer> GridData::getLayerByName(const std::string& name)
{
if (name.empty()) return nullptr;
for (auto& layer : layers) {
if (layer->name == name) return layer;
}
return nullptr;
}
bool GridData::isProtectedLayerName(const std::string& name)
{
static const std::vector<std::string> protected_names = {
"walkable", "transparent"
};
for (const auto& pn : protected_names) {
if (name == pn) return true;
}
return false;
}

131
src/GridData.h Normal file
View file

@ -0,0 +1,131 @@
#pragma once
// GridData.h - Pure data layer for grid state (#252)
//
// GridData holds all non-rendering grid state: cells, entities, TCOD map,
// spatial hash, layers, FOV, pathfinding caches. UIGrid inherits from this
// and adds rendering. GridView can also reference GridData for multi-view.
#include "Common.h"
#include "Python.h"
#include <list>
#include <libtcod.h>
#include <mutex>
#include <optional>
#include <map>
#include <memory>
#include <vector>
#include "PyCallable.h"
#include "UIGridPoint.h"
#include "SpatialHash.h"
#include "GridLayers.h"
#include "GridChunk.h"
// Forward declarations
class DijkstraMap;
class UIEntity;
class UIDrawable;
class PyTexture;
class GridData {
public:
GridData();
virtual ~GridData();
// =========================================================================
// Grid dimensions and cell storage
// =========================================================================
int grid_w = 0, grid_h = 0;
// #123 - Chunk-based storage for large grid support
std::unique_ptr<ChunkManager> chunk_manager;
// Legacy flat storage (kept for small grids or compatibility)
std::vector<UIGridPoint> points;
// Use chunks for grids larger than this threshold
static constexpr int CHUNK_THRESHOLD = 64;
bool use_chunks = false;
// Cell access (handles both flat and chunked storage)
UIGridPoint& at(int x, int y);
// =========================================================================
// Entity management
// =========================================================================
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
SpatialHash spatial_hash; // O(1) entity queries (#115)
// =========================================================================
// TCOD integration (FOV and pathfinding base)
// =========================================================================
TCODMap* tcod_map = nullptr;
mutable std::mutex fov_mutex;
void syncTCODMap();
void syncTCODMapCell(int x, int y);
void computeFOV(int x, int y, int radius, bool light_walls = true,
TCOD_fov_algorithm_t algo = FOV_BASIC);
bool isInFOV(int x, int y) const;
TCODMap* getTCODMap() const { return tcod_map; }
// #114 - FOV algorithm and radius defaults
TCOD_fov_algorithm_t fov_algorithm = FOV_BASIC;
int fov_radius = 10;
// #292 - FOV deduplication
bool fov_dirty = true;
int fov_last_x = -1, fov_last_y = -1;
int fov_last_radius = -1;
bool fov_last_light_walls = true;
TCOD_fov_algorithm_t fov_last_algo = FOV_BASIC;
// =========================================================================
// Pathfinding caches
// =========================================================================
std::map<std::pair<int,int>, std::shared_ptr<DijkstraMap>> dijkstra_maps;
// =========================================================================
// Layer system (#147, #150)
// =========================================================================
std::vector<std::shared_ptr<GridLayer>> layers;
bool layers_need_sort = true;
std::shared_ptr<ColorLayer> addColorLayer(int z_index, const std::string& name = "");
std::shared_ptr<TileLayer> addTileLayer(int z_index, std::shared_ptr<PyTexture> texture = nullptr,
const std::string& name = "");
void removeLayer(std::shared_ptr<GridLayer> layer);
void sortLayers();
std::shared_ptr<GridLayer> getLayerByName(const std::string& name);
static bool isProtectedLayerName(const std::string& name);
// =========================================================================
// Cell callbacks (#142, #230)
// =========================================================================
std::unique_ptr<PyCellHoverCallable> on_cell_enter_callable;
std::unique_ptr<PyCellHoverCallable> on_cell_exit_callable;
std::unique_ptr<PyClickCallable> on_cell_click_callable;
std::optional<sf::Vector2i> hovered_cell;
std::optional<sf::Vector2i> last_clicked_cell;
struct CellCallbackCache {
uint32_t generation = 0;
bool valid = false;
bool has_on_cell_click = false;
bool has_on_cell_enter = false;
bool has_on_cell_exit = false;
};
CellCallbackCache cell_callback_cache;
// fireCellClick/Enter/Exit and refreshCellCallbackCache are on UIGrid
// because they need access to UIDrawable::serial_number/is_python_subclass
// =========================================================================
// UIDrawable children (speech bubbles, effects, overlays)
// =========================================================================
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
bool children_need_sort = true;
protected:
// Initialize grid storage (flat or chunked) and TCOD map
void initStorage(int gx, int gy, GridData* parent_ref);
void cleanupTCOD();
};

View file

@ -150,7 +150,7 @@ static sf::Color LerpColor(const sf::Color& a, const sf::Color& b, float t) {
// GridLayer base class
// =============================================================================
GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent)
GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, GridData* 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),
@ -244,7 +244,7 @@ void GridLayer::ensureChunkTexture(int chunk_idx, int cell_width, int cell_heigh
// ColorLayer implementation
// =============================================================================
ColorLayer::ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent)
ColorLayer::ColorLayer(int z_index, int grid_x, int grid_y, GridData* 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),
@ -515,7 +515,7 @@ void ColorLayer::render(sf::RenderTarget& target,
// TileLayer implementation
// =============================================================================
TileLayer::TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
TileLayer::TileLayer(int z_index, int grid_x, int grid_y, GridData* 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

View file

@ -9,6 +9,7 @@
// Forward declarations
class UIGrid;
class GridData;
class PyTexture;
class UIEntity;
@ -31,7 +32,7 @@ public:
std::string name; // #150 - Layer name for GridPoint property access
int z_index; // Negative = below entities, >= 0 = above entities
int grid_x, grid_y; // Dimensions
UIGrid* parent_grid; // Parent grid reference
GridData* parent_grid; // Parent grid reference (#252: GridData, not UIGrid)
bool visible; // Visibility flag
// Chunk dimensions
@ -43,7 +44,7 @@ public:
std::vector<bool> chunk_texture_initialized; // Track which textures are created
int cached_cell_width, cached_cell_height; // Cell size used for cached textures
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, GridData* parent);
virtual ~GridLayer() = default;
// Mark entire layer as needing re-render
@ -93,7 +94,7 @@ public:
sf::Color perspective_unknown;
bool has_perspective;
ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent);
ColorLayer(int z_index, int grid_x, int grid_y, GridData* parent);
// Access color at position
sf::Color& at(int x, int y);
@ -145,7 +146,7 @@ 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,
TileLayer(int z_index, int grid_x, int grid_y, GridData* parent,
std::shared_ptr<PyTexture> texture = nullptr);
// Access tile index at position

View file

@ -22,17 +22,11 @@
// UIEntityCollection code moved to UIEntityCollection.cpp
UIGrid::UIGrid()
: grid_w(0), grid_h(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
fill_color(8, 8, 8, 255), tcod_map(nullptr),
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
use_chunks(false) // Default to omniscient view
: GridData(), // Initialize data layer (entities, children, FOV defaults)
zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
fill_color(8, 8, 8, 255),
perspective_enabled(false)
{
// Initialize entities list
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
// Initialize children collection (for UIDrawables like speech bubbles, effects)
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
// Initialize box with safe defaults
box.setSize(sf::Vector2f(0, 0));
position = sf::Vector2f(0, 0); // Set base class position
@ -53,12 +47,11 @@ UIGrid::UIGrid()
}
UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
: grid_w(gx), grid_h(gy),
: GridData(), // Initialize data layer
zoom(1.0f),
ptex(_ptex),
fill_color(8, 8, 8, 255), tcod_map(nullptr),
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
fill_color(8, 8, 8, 255),
perspective_enabled(false)
{
// Use texture dimensions if available, otherwise use defaults
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
@ -66,10 +59,6 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
center_x = (gx/2) * cell_width;
center_y = (gy/2) * cell_height;
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
// Initialize children collection (for UIDrawables like speech bubbles, effects)
children = std::make_shared<std::vector<std::shared_ptr<UIDrawable>>>();
box.setSize(_wh);
position = _xy; // Set base class position
@ -91,47 +80,8 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
// textures are upside-down inside renderTexture
output.setTexture(renderTexture.getTexture());
// Create TCOD map for FOV and as source for pathfinding
tcod_map = new TCODMap(gx, gy);
// Note: DijkstraMap objects are created on-demand via get_dijkstra_map()
// A* paths are computed on-demand via find_path()
// #123 - Initialize storage based on grid size
if (use_chunks) {
// Large grid: use chunk-based storage
chunk_manager = std::make_unique<ChunkManager>(gx, gy, this);
// Initialize all cells with parent reference
for (int cy = 0; cy < chunk_manager->chunks_y; ++cy) {
for (int cx = 0; cx < chunk_manager->chunks_x; ++cx) {
GridChunk* chunk = chunk_manager->getChunk(cx, cy);
if (!chunk) continue;
for (int ly = 0; ly < chunk->height; ++ly) {
for (int lx = 0; lx < chunk->width; ++lx) {
auto& cell = chunk->at(lx, ly);
cell.grid_x = chunk->world_x + lx;
cell.grid_y = chunk->world_y + ly;
cell.parent_grid = this;
}
}
}
}
} else {
// Small grid: use flat storage (original behavior)
points.resize(gx * gy);
for (int y = 0; y < gy; y++) {
for (int x = 0; x < gx; x++) {
int idx = y * gx + x;
points[idx].grid_x = x;
points[idx].grid_y = y;
points[idx].parent_grid = this;
}
}
}
// Initial sync of TCOD map
syncTCODMap();
// Initialize grid storage, TCOD map, and sync (#252: delegated to GridData)
initStorage(gx, gy, static_cast<GridData*>(this));
}
void UIGrid::update() {}
@ -467,24 +417,12 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
}
}
UIGridPoint& UIGrid::at(int x, int y)
{
// #123 - Route through chunk manager for large grids
if (use_chunks && chunk_manager) {
return chunk_manager->at(x, y);
}
return points[y * grid_w + x];
}
// at(), syncTCODMap(), computeFOV(), isInFOV(), layer management methods
// are now in GridData.cpp (#252)
UIGrid::~UIGrid()
{
// Clear Dijkstra maps first (they reference tcod_map)
dijkstra_maps.clear();
if (tcod_map) {
delete tcod_map;
tcod_map = nullptr;
}
// GridData destructor handles TCOD map and Dijkstra cleanup
}
void UIGrid::ensureRenderTextureSize()
@ -512,114 +450,6 @@ PyObjectsEnum UIGrid::derived_type()
return PyObjectsEnum::UIGRID;
}
// #147 - Layer management methods
std::shared_ptr<ColorLayer> UIGrid::addColorLayer(int z_index, const std::string& name) {
auto layer = std::make_shared<ColorLayer>(z_index, grid_w, grid_h, this);
layer->name = name;
layers.push_back(layer);
layers_need_sort = true;
return layer;
}
std::shared_ptr<TileLayer> UIGrid::addTileLayer(int z_index, std::shared_ptr<PyTexture> texture, const std::string& name) {
auto layer = std::make_shared<TileLayer>(z_index, grid_w, grid_h, this, texture);
layer->name = name;
layers.push_back(layer);
layers_need_sort = true;
return layer;
}
std::shared_ptr<GridLayer> UIGrid::getLayerByName(const std::string& name) {
for (auto& layer : layers) {
if (layer->name == name) {
return layer;
}
}
return nullptr;
}
bool UIGrid::isProtectedLayerName(const std::string& name) {
// #150 - These names are reserved for GridPoint pathfinding properties
static const std::vector<std::string> protected_names = {
"walkable", "transparent"
};
for (const auto& pn : protected_names) {
if (name == pn) return true;
}
return false;
}
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()
{
if (!tcod_map) return;
for (int y = 0; y < grid_h; y++) {
for (int x = 0; x < grid_w; x++) {
const UIGridPoint& point = at(x, y);
tcod_map->setProperties(x, y, point.transparent, point.walkable);
}
}
fov_dirty = true; // #292: map changed, FOV needs recomputation
}
void UIGrid::syncTCODMapCell(int x, int y)
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return;
const UIGridPoint& point = at(x, y);
tcod_map->setProperties(x, y, point.transparent, point.walkable);
fov_dirty = true; // #292: cell changed, FOV needs recomputation
}
void UIGrid::computeFOV(int x, int y, int radius, bool light_walls, TCOD_fov_algorithm_t algo)
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return;
// #292: Skip redundant FOV computation if map hasn't changed and params match
if (!fov_dirty &&
x == fov_last_x && y == fov_last_y &&
radius == fov_last_radius &&
light_walls == fov_last_light_walls &&
algo == fov_last_algo) {
return;
}
std::lock_guard<std::mutex> lock(fov_mutex);
tcod_map->computeFov(x, y, radius, light_walls, algo);
// Cache parameters for deduplication
fov_dirty = false;
fov_last_x = x;
fov_last_y = y;
fov_last_radius = radius;
fov_last_light_walls = light_walls;
fov_last_algo = algo;
}
bool UIGrid::isInFOV(int x, int y) const
{
if (!tcod_map || x < 0 || x >= grid_w || y < 0 || y >= grid_h) return false;
std::lock_guard<std::mutex> lock(fov_mutex);
return tcod_map->isInFov(x, y);
}
// Pathfinding methods moved to UIGridPathfinding.cpp
// - Grid.find_path() returns AStarPath objects
// - Grid.get_dijkstra_map() returns DijkstraMap objects (cached)

View file

@ -26,169 +26,82 @@
#include "GridChunk.h"
#include "SpatialHash.h"
#include "UIEntityCollection.h" // EntityCollection types (extracted from UIGrid)
#include "GridData.h" // #252 - Data layer base class
// Forward declaration for pathfinding
class DijkstraMap;
class UIGrid: public UIDrawable
// UIGrid inherits both UIDrawable (rendering) and GridData (state).
// This allows GridData to be shared with GridView for multi-view support (#252).
class UIGrid: public UIDrawable, public GridData
{
private:
std::shared_ptr<PyTexture> ptex;
// Default cell dimensions when no texture is provided
static constexpr int DEFAULT_CELL_WIDTH = 16;
static constexpr int DEFAULT_CELL_HEIGHT = 16;
TCODMap* tcod_map; // TCOD map for FOV and pathfinding
mutable std::mutex fov_mutex; // Mutex for thread-safe FOV operations
public:
// Dijkstra map cache - keyed by root position
// Public so UIGridPathfinding can access it
std::map<std::pair<int,int>, std::shared_ptr<DijkstraMap>> dijkstra_maps;
public:
UIGrid();
//UIGrid(int, int, IndexTexture*, float, float, float, float);
UIGrid(int, int, std::shared_ptr<PyTexture>, sf::Vector2f, sf::Vector2f);
~UIGrid(); // Destructor to clean up TCOD map
~UIGrid();
void update();
void render(sf::Vector2f, sf::RenderTarget&) override final;
UIGridPoint& at(int, int);
PyObjectsEnum derived_type() override final;
//void setSprite(int);
virtual UIDrawable* click_at(sf::Vector2f point) override final;
// TCOD integration methods
void syncTCODMap(); // Sync entire map with current grid state
void syncTCODMapCell(int x, int y); // Sync a single cell to TCOD map
void computeFOV(int x, int y, int radius, bool light_walls = true, TCOD_fov_algorithm_t algo = FOV_BASIC);
bool isInFOV(int x, int y) const;
TCODMap* getTCODMap() const { return tcod_map; } // Access for pathfinding
// Pathfinding - new API creates AStarPath/DijkstraMap objects
// See UIGridPathfinding.h for the new pathfinding API
// Grid.find_path() now returns AStarPath objects
// Grid.get_dijkstra_map() returns DijkstraMap objects (cached by root position)
// Phase 1 virtual method implementations
sf::FloatRect get_bounds() const override;
void move(float dx, float dy) override;
void resize(float w, float h) override;
void onPositionChanged() override;
int grid_w, grid_h;
//int grid_size; // grid sizes are implied by IndexTexture now
// =========================================================================
// Rendering-only members (NOT in GridData)
// =========================================================================
sf::RectangleShape box;
float center_x, center_y, zoom;
float camera_rotation = 0.0f; // Rotation of grid contents around camera center (degrees)
//IndexTexture* itex;
float camera_rotation = 0.0f;
std::shared_ptr<PyTexture> getTexture();
sf::Sprite sprite, output;
sf::RenderTexture renderTexture;
sf::Vector2u renderTextureSize{0, 0}; // Track current allocation for resize detection
// Helper to ensure renderTexture matches game resolution
sf::Vector2u renderTextureSize{0, 0};
void ensureRenderTextureSize();
// Intermediate texture for camera_rotation (larger than viewport to hold rotated content)
sf::RenderTexture rotationTexture;
unsigned int rotationTextureSize = 0; // Track current allocation size
// #123 - Chunk-based storage for large grid support
std::unique_ptr<ChunkManager> chunk_manager;
// Legacy flat storage (kept for small grids or compatibility)
std::vector<UIGridPoint> points;
// Use chunks for grids larger than this threshold
static constexpr int CHUNK_THRESHOLD = 64;
bool use_chunks;
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
// Spatial hash for O(1) entity queries (#115)
SpatialHash spatial_hash;
// UIDrawable children collection (speech bubbles, effects, overlays, etc.)
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 (#150 - extended with names)
std::shared_ptr<ColorLayer> addColorLayer(int z_index, const std::string& name = "");
std::shared_ptr<TileLayer> addTileLayer(int z_index, std::shared_ptr<PyTexture> texture = nullptr, const std::string& name = "");
void removeLayer(std::shared_ptr<GridLayer> layer);
void sortLayers();
std::shared_ptr<GridLayer> getLayerByName(const std::string& name);
// #150 - Protected layer names (reserved for GridPoint properties)
static bool isProtectedLayerName(const std::string& name);
unsigned int rotationTextureSize = 0;
// Background rendering
sf::Color fill_color;
// Perspective system - entity whose view to render
std::weak_ptr<UIEntity> perspective_entity; // Weak reference to perspective entity
bool perspective_enabled; // Whether to use perspective rendering
// Perspective system
std::weak_ptr<UIEntity> perspective_entity;
bool perspective_enabled;
// #114 - FOV algorithm and radius for this grid
TCOD_fov_algorithm_t fov_algorithm; // Default FOV algorithm (from mcrfpy.default_fov)
int fov_radius; // Default FOV radius
// #292 - FOV deduplication: skip redundant computations
bool fov_dirty = true; // Set true when TCOD map changes
int fov_last_x = -1, fov_last_y = -1; // Last FOV computation parameters
int fov_last_radius = -1;
bool fov_last_light_walls = true;
TCOD_fov_algorithm_t fov_last_algo = FOV_BASIC;
// #142, #230 - Grid cell mouse events
// Cell hover callbacks take only (cell_pos); cell click still takes (cell_pos, button, action)
std::unique_ptr<PyCellHoverCallable> on_cell_enter_callable;
std::unique_ptr<PyCellHoverCallable> on_cell_exit_callable;
std::unique_ptr<PyClickCallable> on_cell_click_callable;
std::optional<sf::Vector2i> hovered_cell; // Currently hovered cell or nullopt
std::optional<sf::Vector2i> last_clicked_cell; // Cell clicked during click_at
// Grid-specific cell callback cache (separate from UIDrawable::CallbackCache)
struct CellCallbackCache {
uint32_t generation = 0;
bool valid = false;
bool has_on_cell_click = false;
bool has_on_cell_enter = false;
bool has_on_cell_exit = false;
};
CellCallbackCache cell_callback_cache;
// #142 - Cell coordinate conversion (screen pos -> cell coords)
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
// #221 - Get effective cell size (texture size * zoom)
sf::Vector2f getEffectiveCellSize() const;
// #142 - Update cell hover state (called from PyScene)
// Now takes button/action for consistent callback signatures
void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action);
// Fire cell callbacks
// #230: Cell hover callbacks (enter/exit) now take only (cell_pos)
// Cell click still takes (cell_pos, button, action)
// Returns true if a callback was fired
// Cell callback firing (needs UIDrawable::is_python_subclass, serial_number)
bool fireCellClick(sf::Vector2i cell, const std::string& button, const std::string& action);
bool fireCellEnter(sf::Vector2i cell);
bool fireCellExit(sf::Vector2i cell);
// Refresh cell callback cache for subclass method support
void refreshCellCallbackCache(PyObject* pyObj);
// #142 - Cell coordinate conversion (needs texture for cell size)
std::optional<sf::Vector2i> screenToCell(sf::Vector2f screen_pos) const;
sf::Vector2f getEffectiveCellSize() const;
void updateCellHover(sf::Vector2f mousepos, const std::string& button, const std::string& action);
// Property system for animations
bool setProperty(const std::string& name, float value) override;
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
bool getProperty(const std::string& name, float& value) const override;
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
bool hasProperty(const std::string& name) const override;
// #169 - Camera positioning
void center_camera();
void center_camera(float tile_x, float tile_y);
// =========================================================================
// Python API (static methods)
// =========================================================================
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
static PyObject* get_grid_w(PyUIGridObject* self, void* closure);
@ -215,35 +128,22 @@ public:
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
// Pathfinding methods moved to UIGridPathfinding.cpp
// py_find_path -> UIGridPathfinding::Grid_find_path (returns AStarPath)
// py_get_dijkstra_map -> UIGridPathfinding::Grid_get_dijkstra_map (returns DijkstraMap)
// py_clear_dijkstra_maps -> UIGridPathfinding::Grid_clear_dijkstra_maps
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args);
static PyObject* get_camera_rotation(PyUIGridObject* self, void* closure);
static int set_camera_rotation(PyUIGridObject* self, PyObject* value, void* closure);
// #199 - HeightMap application methods
static PyObject* py_apply_threshold(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_apply_ranges(PyUIGridObject* self, PyObject* args);
// #169 - Camera positioning
void center_camera(); // Center on grid's middle tile
void center_camera(float tile_x, float tile_y); // Center on specific tile
// #301 - Turn management
static PyObject* py_step(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyMappingMethods mpmethods; // For grid[x, y] subscript access
static PyObject* subscript(PyUIGridObject* self, PyObject* key); // __getitem__
static PyMappingMethods mpmethods;
static PyObject* subscript(PyUIGridObject* self, PyObject* key);
static PyObject* get_entities(PyUIGridObject* self, void* closure);
static PyObject* get_children(PyUIGridObject* self, void* closure);
static PyObject* repr(PyUIGridObject* self);
// #142 - Grid cell mouse event Python API
static PyObject* get_on_cell_enter(PyUIGridObject* self, void* closure);
static int set_on_cell_enter(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* get_on_cell_exit(PyUIGridObject* self, void* closure);
@ -252,7 +152,6 @@ 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);
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
static PyObject* get_layers(PyUIGridObject* self, void* closure);
@ -285,7 +184,7 @@ namespace mcrfpydef {
obj->data->on_enter_unregister();
obj->data->on_exit_unregister();
obj->data->on_move_unregister();
// Grid-specific cell callbacks
// Grid-specific cell callbacks (now on GridData base)
obj->data->on_cell_enter_callable.reset();
obj->data->on_cell_exit_callable.reset();
obj->data->on_cell_click_callable.reset();
@ -294,7 +193,7 @@ namespace mcrfpydef {
Py_TYPE(self)->tp_free(self);
},
.tp_repr = (reprfunc)UIGrid::repr,
.tp_as_mapping = &UIGrid::mpmethods, // Enable grid[x, y] subscript access
.tp_as_mapping = &UIGrid::mpmethods,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
.tp_doc = PyDoc_STR("Grid(pos=None, size=None, grid_size=None, texture=None, **kwargs)\n\n"
"A grid-based UI element for tile-based rendering and entity management.\n\n"
@ -347,11 +246,9 @@ namespace mcrfpydef {
" margin (float): General margin for alignment\n"
" horiz_margin (float): Horizontal margin override\n"
" vert_margin (float): Vertical margin override"),
// tp_traverse visits Python object references for GC cycle detection
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
PyUIGridObject* obj = (PyUIGridObject*)self;
if (obj->data) {
// Base class callbacks
if (obj->data->click_callable) {
PyObject* callback = obj->data->click_callable->borrow();
if (callback && callback != Py_None) Py_VISIT(callback);
@ -368,7 +265,6 @@ namespace mcrfpydef {
PyObject* callback = obj->data->on_move_callable->borrow();
if (callback && callback != Py_None) Py_VISIT(callback);
}
// Grid-specific cell callbacks
if (obj->data->on_cell_enter_callable) {
PyObject* callback = obj->data->on_cell_enter_callable->borrow();
if (callback && callback != Py_None) Py_VISIT(callback);
@ -384,7 +280,6 @@ namespace mcrfpydef {
}
return 0;
},
// tp_clear breaks reference cycles by clearing Python references
.tp_clear = [](PyObject* self) -> int {
PyUIGridObject* obj = (PyUIGridObject*)self;
if (obj->data) {
@ -399,7 +294,6 @@ namespace mcrfpydef {
return 0;
},
.tp_methods = UIGrid_all_methods,
//.tp_members = UIGrid::members,
.tp_getset = UIGrid::getsetters,
.tp_base = &mcrfpydef::PyDrawableType,
.tp_init = (initproc)UIGrid::init,
@ -410,7 +304,4 @@ namespace mcrfpydef {
return (PyObject*)self;
}
};
// EntityCollection types moved to UIEntityCollection.h
}

View file

@ -16,6 +16,7 @@ static PyObject* sfColor_to_PyObject(sf::Color color);
static sf::Color PyObject_to_sfColor(PyObject* obj);
class UIGrid;
class GridData;
class UIEntity;
class UIGridPoint;
class UIGridPointState;
@ -40,7 +41,7 @@ class UIGridPoint
public:
bool walkable, transparent; // Pathfinding/FOV properties
int grid_x, grid_y; // Position in parent grid
UIGrid* parent_grid; // Parent grid reference for TCOD sync
GridData* parent_grid; // Parent grid reference for TCOD sync (#252)
UIGridPoint();
// Built-in property accessors (walkable, transparent only)