Compare commits
7 commits
68f8349fe8
...
0545dd4861
| Author | SHA1 | Date | |
|---|---|---|---|
| 0545dd4861 | |||
| 42fcd3417e | |||
| a258613faa | |||
| 9469c04b01 | |||
| abb3316ac1 | |||
| 4b05a95efe | |||
| f769c6c5f5 |
37 changed files with 1743629 additions and 197 deletions
201
src/GridChunk.cpp
Normal file
201
src/GridChunk.cpp
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
#include "GridChunk.h"
|
||||
#include "UIGrid.h"
|
||||
#include "PyTexture.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// =============================================================================
|
||||
// GridChunk implementation
|
||||
// =============================================================================
|
||||
|
||||
GridChunk::GridChunk(int chunk_x, int chunk_y, int width, int height,
|
||||
int world_x, int world_y, UIGrid* parent)
|
||||
: chunk_x(chunk_x), chunk_y(chunk_y),
|
||||
width(width), height(height),
|
||||
world_x(world_x), world_y(world_y),
|
||||
cells(width * height),
|
||||
dirty(true),
|
||||
parent_grid(parent)
|
||||
{}
|
||||
|
||||
UIGridPoint& GridChunk::at(int local_x, int local_y) {
|
||||
return cells[local_y * width + local_x];
|
||||
}
|
||||
|
||||
const UIGridPoint& GridChunk::at(int local_x, int local_y) const {
|
||||
return cells[local_y * width + local_x];
|
||||
}
|
||||
|
||||
void GridChunk::markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
// #150 - Removed ensureTexture/renderToTexture - base layer rendering removed
|
||||
// GridChunk now only provides data storage for GridPoints
|
||||
|
||||
sf::FloatRect GridChunk::getWorldBounds(int cell_width, int cell_height) const {
|
||||
return sf::FloatRect(
|
||||
sf::Vector2f(world_x * cell_width, world_y * cell_height),
|
||||
sf::Vector2f(width * cell_width, height * cell_height)
|
||||
);
|
||||
}
|
||||
|
||||
bool GridChunk::isVisible(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) const {
|
||||
// Check if chunk's cell range overlaps with viewport's cell range
|
||||
float chunk_right = world_x + width;
|
||||
float chunk_bottom = world_y + height;
|
||||
|
||||
return !(world_x >= right_edge || chunk_right <= left_edge ||
|
||||
world_y >= bottom_edge || chunk_bottom <= top_edge);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ChunkManager implementation
|
||||
// =============================================================================
|
||||
|
||||
ChunkManager::ChunkManager(int grid_x, int grid_y, UIGrid* parent)
|
||||
: grid_x(grid_x), grid_y(grid_y), parent_grid(parent)
|
||||
{
|
||||
// Calculate number of chunks needed
|
||||
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
|
||||
chunks.reserve(chunks_x * chunks_y);
|
||||
|
||||
// Create chunks
|
||||
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||
// Calculate world position
|
||||
int world_x = cx * GridChunk::CHUNK_SIZE;
|
||||
int world_y = cy * GridChunk::CHUNK_SIZE;
|
||||
|
||||
// Calculate actual size (may be smaller at edges)
|
||||
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
|
||||
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
|
||||
|
||||
chunks.push_back(std::make_unique<GridChunk>(
|
||||
cx, cy, chunk_width, chunk_height, world_x, world_y, parent
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GridChunk* ChunkManager::getChunkForCell(int x, int y) {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int chunk_x = x / GridChunk::CHUNK_SIZE;
|
||||
int chunk_y = y / GridChunk::CHUNK_SIZE;
|
||||
return getChunk(chunk_x, chunk_y);
|
||||
}
|
||||
|
||||
const GridChunk* ChunkManager::getChunkForCell(int x, int y) const {
|
||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int chunk_x = x / GridChunk::CHUNK_SIZE;
|
||||
int chunk_y = y / GridChunk::CHUNK_SIZE;
|
||||
return getChunk(chunk_x, chunk_y);
|
||||
}
|
||||
|
||||
GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) {
|
||||
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
|
||||
return nullptr;
|
||||
}
|
||||
return chunks[chunk_y * chunks_x + chunk_x].get();
|
||||
}
|
||||
|
||||
const GridChunk* ChunkManager::getChunk(int chunk_x, int chunk_y) const {
|
||||
if (chunk_x < 0 || chunk_x >= chunks_x || chunk_y < 0 || chunk_y >= chunks_y) {
|
||||
return nullptr;
|
||||
}
|
||||
return chunks[chunk_y * chunks_x + chunk_x].get();
|
||||
}
|
||||
|
||||
UIGridPoint& ChunkManager::at(int x, int y) {
|
||||
GridChunk* chunk = getChunkForCell(x, y);
|
||||
if (!chunk) {
|
||||
// Return a static dummy point for out-of-bounds access
|
||||
// This matches the original behavior of UIGrid::at()
|
||||
static UIGridPoint dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
// Convert to local coordinates within chunk
|
||||
int local_x = x % GridChunk::CHUNK_SIZE;
|
||||
int local_y = y % GridChunk::CHUNK_SIZE;
|
||||
|
||||
// Mark chunk dirty when accessed for modification
|
||||
chunk->markDirty();
|
||||
|
||||
return chunk->at(local_x, local_y);
|
||||
}
|
||||
|
||||
const UIGridPoint& ChunkManager::at(int x, int y) const {
|
||||
const GridChunk* chunk = getChunkForCell(x, y);
|
||||
if (!chunk) {
|
||||
static UIGridPoint dummy;
|
||||
return dummy;
|
||||
}
|
||||
|
||||
int local_x = x % GridChunk::CHUNK_SIZE;
|
||||
int local_y = y % GridChunk::CHUNK_SIZE;
|
||||
|
||||
return chunk->at(local_x, local_y);
|
||||
}
|
||||
|
||||
void ChunkManager::markAllDirty() {
|
||||
for (auto& chunk : chunks) {
|
||||
chunk->markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<GridChunk*> ChunkManager::getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) {
|
||||
std::vector<GridChunk*> visible;
|
||||
visible.reserve(chunks.size()); // Pre-allocate for worst case
|
||||
|
||||
for (auto& chunk : chunks) {
|
||||
if (chunk->isVisible(left_edge, top_edge, right_edge, bottom_edge)) {
|
||||
visible.push_back(chunk.get());
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
void ChunkManager::resize(int new_grid_x, int new_grid_y) {
|
||||
// For now, simple rebuild - could be optimized to preserve data
|
||||
grid_x = new_grid_x;
|
||||
grid_y = new_grid_y;
|
||||
|
||||
chunks_x = (grid_x + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
chunks_y = (grid_y + GridChunk::CHUNK_SIZE - 1) / GridChunk::CHUNK_SIZE;
|
||||
|
||||
chunks.clear();
|
||||
chunks.reserve(chunks_x * chunks_y);
|
||||
|
||||
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||
int world_x = cx * GridChunk::CHUNK_SIZE;
|
||||
int world_y = cy * GridChunk::CHUNK_SIZE;
|
||||
int chunk_width = std::min(GridChunk::CHUNK_SIZE, grid_x - world_x);
|
||||
int chunk_height = std::min(GridChunk::CHUNK_SIZE, grid_y - world_y);
|
||||
|
||||
chunks.push_back(std::make_unique<GridChunk>(
|
||||
cx, cy, chunk_width, chunk_height, world_x, world_y, parent_grid
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ChunkManager::dirtyChunks() const {
|
||||
int count = 0;
|
||||
for (const auto& chunk : chunks) {
|
||||
if (chunk->dirty) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
108
src/GridChunk.h
Normal file
108
src/GridChunk.h
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "UIGridPoint.h"
|
||||
|
||||
// Forward declarations
|
||||
class UIGrid;
|
||||
class PyTexture;
|
||||
|
||||
/**
|
||||
* #123 - Grid chunk for sub-grid data storage
|
||||
* #150 - Rendering removed; layers now handle all rendering
|
||||
*
|
||||
* Each chunk represents a CHUNK_SIZE x CHUNK_SIZE portion of the grid.
|
||||
* Chunks store GridPoint data for pathfinding and game logic.
|
||||
*/
|
||||
class GridChunk {
|
||||
public:
|
||||
// Compile-time configurable chunk size (power of 2 recommended)
|
||||
static constexpr int CHUNK_SIZE = 64;
|
||||
|
||||
// Position of this chunk in chunk coordinates
|
||||
int chunk_x, chunk_y;
|
||||
|
||||
// Actual dimensions (may be less than CHUNK_SIZE at grid edges)
|
||||
int width, height;
|
||||
|
||||
// World position (in cell coordinates)
|
||||
int world_x, world_y;
|
||||
|
||||
// Cell data for this chunk (pathfinding properties only)
|
||||
std::vector<UIGridPoint> cells;
|
||||
|
||||
// Dirty flag (for layer sync if needed)
|
||||
bool dirty;
|
||||
|
||||
// Parent grid reference
|
||||
UIGrid* parent_grid;
|
||||
|
||||
// Constructor
|
||||
GridChunk(int chunk_x, int chunk_y, int width, int height,
|
||||
int world_x, int world_y, UIGrid* parent);
|
||||
|
||||
// Access cell at local chunk coordinates
|
||||
UIGridPoint& at(int local_x, int local_y);
|
||||
const UIGridPoint& at(int local_x, int local_y) const;
|
||||
|
||||
// Mark chunk as dirty
|
||||
void markDirty();
|
||||
|
||||
// Get pixel bounds of this chunk in world coordinates
|
||||
sf::FloatRect getWorldBounds(int cell_width, int cell_height) const;
|
||||
|
||||
// Check if chunk overlaps with viewport
|
||||
bool isVisible(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages a 2D array of chunks for a grid
|
||||
*/
|
||||
class ChunkManager {
|
||||
public:
|
||||
// Dimensions in chunks
|
||||
int chunks_x, chunks_y;
|
||||
|
||||
// Grid dimensions in cells
|
||||
int grid_x, grid_y;
|
||||
|
||||
// All chunks (row-major order)
|
||||
std::vector<std::unique_ptr<GridChunk>> chunks;
|
||||
|
||||
// Parent grid
|
||||
UIGrid* parent_grid;
|
||||
|
||||
// Constructor - creates chunks for given grid dimensions
|
||||
ChunkManager(int grid_x, int grid_y, UIGrid* parent);
|
||||
|
||||
// Get chunk containing cell (x, y)
|
||||
GridChunk* getChunkForCell(int x, int y);
|
||||
const GridChunk* getChunkForCell(int x, int y) const;
|
||||
|
||||
// Get chunk at chunk coordinates
|
||||
GridChunk* getChunk(int chunk_x, int chunk_y);
|
||||
const GridChunk* getChunk(int chunk_x, int chunk_y) const;
|
||||
|
||||
// Access cell at grid coordinates (routes through chunk)
|
||||
UIGridPoint& at(int x, int y);
|
||||
const UIGridPoint& at(int x, int y) const;
|
||||
|
||||
// Mark all chunks dirty (for full rebuild)
|
||||
void markAllDirty();
|
||||
|
||||
// Get chunks that overlap with viewport
|
||||
std::vector<GridChunk*> getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge);
|
||||
|
||||
// Resize grid (rebuilds chunks)
|
||||
void resize(int new_grid_x, int new_grid_y);
|
||||
|
||||
// Get total number of chunks
|
||||
int totalChunks() const { return chunks_x * chunks_y; }
|
||||
|
||||
// Get number of dirty chunks
|
||||
int dirtyChunks() const;
|
||||
};
|
||||
794
src/GridLayers.cpp
Normal file
794
src/GridLayers.cpp
Normal file
|
|
@ -0,0 +1,794 @@
|
|||
#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),
|
||||
dirty(true), texture_initialized(false),
|
||||
cached_cell_width(0), cached_cell_height(0)
|
||||
{}
|
||||
|
||||
void GridLayer::markDirty() {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void GridLayer::ensureTextureSize(int cell_width, int cell_height) {
|
||||
// Check if we need to resize/create the texture
|
||||
unsigned int required_width = grid_x * cell_width;
|
||||
unsigned int required_height = grid_y * cell_height;
|
||||
|
||||
// Maximum texture size limit (prevent excessive memory usage)
|
||||
const unsigned int MAX_TEXTURE_SIZE = 4096;
|
||||
if (required_width > MAX_TEXTURE_SIZE) required_width = MAX_TEXTURE_SIZE;
|
||||
if (required_height > MAX_TEXTURE_SIZE) required_height = MAX_TEXTURE_SIZE;
|
||||
|
||||
// Skip if already properly sized
|
||||
if (texture_initialized &&
|
||||
cached_texture.getSize().x == required_width &&
|
||||
cached_texture.getSize().y == required_height &&
|
||||
cached_cell_width == cell_width &&
|
||||
cached_cell_height == cell_height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create or resize the texture (SFML uses .create() not .resize())
|
||||
if (!cached_texture.create(required_width, required_height)) {
|
||||
// Creation failed - texture will remain uninitialized
|
||||
texture_initialized = false;
|
||||
return;
|
||||
}
|
||||
|
||||
cached_cell_width = cell_width;
|
||||
cached_cell_height = cell_height;
|
||||
texture_initialized = true;
|
||||
dirty = true; // Force re-render after resize
|
||||
|
||||
// Setup the sprite to use the texture
|
||||
cached_sprite.setTexture(cached_texture.getTexture());
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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);
|
||||
markDirty(); // #148 - Mark for re-render
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// #148 - Invalidate cached texture (will be resized on next render)
|
||||
texture_initialized = false;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// #148 - Render all cells to cached texture (called when dirty)
|
||||
void ColorLayer::renderToTexture(int cell_width, int cell_height) {
|
||||
ensureTextureSize(cell_width, cell_height);
|
||||
if (!texture_initialized) return;
|
||||
|
||||
cached_texture.clear(sf::Color::Transparent);
|
||||
|
||||
sf::RectangleShape rect;
|
||||
rect.setSize(sf::Vector2f(cell_width, cell_height));
|
||||
rect.setOutlineThickness(0);
|
||||
|
||||
// Render all cells to cached texture (no zoom - 1:1 pixel mapping)
|
||||
for (int x = 0; x < grid_x; ++x) {
|
||||
for (int y = 0; y < grid_y; ++y) {
|
||||
const sf::Color& color = at(x, y);
|
||||
if (color.a == 0) continue; // Skip fully transparent
|
||||
|
||||
rect.setPosition(sf::Vector2f(x * cell_width, y * cell_height));
|
||||
rect.setFillColor(color);
|
||||
cached_texture.draw(rect);
|
||||
}
|
||||
}
|
||||
|
||||
cached_texture.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// #148 - Use cached texture rendering
|
||||
// Re-render to texture only if dirty
|
||||
if (dirty || !texture_initialized) {
|
||||
renderToTexture(cell_width, cell_height);
|
||||
}
|
||||
|
||||
if (!texture_initialized) {
|
||||
// Fallback to direct rendering if texture creation failed
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Blit visible portion of cached texture with zoom applied
|
||||
// Calculate source rectangle (unzoomed pixel coordinates in cached texture)
|
||||
int src_left = std::max(0, (int)left_spritepixels);
|
||||
int src_top = std::max(0, (int)top_spritepixels);
|
||||
int src_width = std::min((int)cached_texture.getSize().x - src_left,
|
||||
(int)((x_limit - left_edge + 2) * cell_width));
|
||||
int src_height = std::min((int)cached_texture.getSize().y - src_top,
|
||||
(int)((y_limit - top_edge + 2) * cell_height));
|
||||
|
||||
if (src_width <= 0 || src_height <= 0) return;
|
||||
|
||||
// Set texture rect for visible portion
|
||||
cached_sprite.setTextureRect(sf::IntRect({src_left, src_top}, {src_width, src_height}));
|
||||
|
||||
// Position in target (offset for partial cell visibility)
|
||||
float dest_x = (src_left - left_spritepixels) * zoom;
|
||||
float dest_y = (src_top - top_spritepixels) * zoom;
|
||||
cached_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
|
||||
|
||||
// Apply zoom via scale
|
||||
cached_sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||
|
||||
target.draw(cached_sprite);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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);
|
||||
markDirty(); // #148 - Mark for re-render
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// #148 - Invalidate cached texture (will be resized on next render)
|
||||
texture_initialized = false;
|
||||
markDirty();
|
||||
}
|
||||
|
||||
// #148 - Render all cells to cached texture (called when dirty)
|
||||
void TileLayer::renderToTexture(int cell_width, int cell_height) {
|
||||
ensureTextureSize(cell_width, cell_height);
|
||||
if (!texture_initialized || !texture) return;
|
||||
|
||||
cached_texture.clear(sf::Color::Transparent);
|
||||
|
||||
// Render all tiles to cached texture (no zoom - 1:1 pixel mapping)
|
||||
for (int x = 0; x < grid_x; ++x) {
|
||||
for (int y = 0; y < grid_y; ++y) {
|
||||
int tile_index = at(x, y);
|
||||
if (tile_index < 0) continue; // No tile
|
||||
|
||||
auto pixel_pos = sf::Vector2f(x * cell_width, y * cell_height);
|
||||
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f));
|
||||
cached_texture.draw(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
cached_texture.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// #148 - Use cached texture rendering
|
||||
// Re-render to texture only if dirty
|
||||
if (dirty || !texture_initialized) {
|
||||
renderToTexture(cell_width, cell_height);
|
||||
}
|
||||
|
||||
if (!texture_initialized) {
|
||||
// Fallback to direct rendering if texture creation failed
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Blit visible portion of cached texture with zoom applied
|
||||
// Calculate source rectangle (unzoomed pixel coordinates in cached texture)
|
||||
int src_left = std::max(0, (int)left_spritepixels);
|
||||
int src_top = std::max(0, (int)top_spritepixels);
|
||||
int src_width = std::min((int)cached_texture.getSize().x - src_left,
|
||||
(int)((x_limit - left_edge + 2) * cell_width));
|
||||
int src_height = std::min((int)cached_texture.getSize().y - src_top,
|
||||
(int)((y_limit - top_edge + 2) * cell_height));
|
||||
|
||||
if (src_width <= 0 || src_height <= 0) return;
|
||||
|
||||
// Set texture rect for visible portion
|
||||
cached_sprite.setTextureRect(sf::IntRect({src_left, src_top}, {src_width, src_height}));
|
||||
|
||||
// Position in target (offset for partial cell visibility)
|
||||
float dest_x = (src_left - left_spritepixels) * zoom;
|
||||
float dest_y = (src_top - top_spritepixels) * zoom;
|
||||
cached_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
|
||||
|
||||
// Apply zoom via scale
|
||||
cached_sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||
|
||||
target.draw(cached_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;
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
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;
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
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();
|
||||
self->data->markDirty(); // #148 - Mark for re-render
|
||||
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(); // #148 - Mark for re-render
|
||||
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());
|
||||
}
|
||||
244
src/GridLayers.h
Normal file
244
src/GridLayers.h
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
// 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;
|
||||
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
|
||||
bool visible; // Visibility flag
|
||||
|
||||
// #148 - Dirty flag and RenderTexture caching
|
||||
bool dirty; // True if layer needs re-render
|
||||
sf::RenderTexture cached_texture; // Cached layer content
|
||||
sf::Sprite cached_sprite; // Sprite for blitting cached texture
|
||||
bool texture_initialized; // True if RenderTexture has been created
|
||||
int cached_cell_width, cached_cell_height; // Cell size used for cached texture
|
||||
|
||||
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
|
||||
virtual ~GridLayer() = default;
|
||||
|
||||
// Mark layer as needing re-render
|
||||
void markDirty();
|
||||
|
||||
// Ensure cached texture is properly sized for current grid dimensions
|
||||
void ensureTextureSize(int cell_width, int cell_height);
|
||||
|
||||
// Render the layer content to the cached texture (called when dirty)
|
||||
virtual void renderToTexture(int cell_width, int cell_height) = 0;
|
||||
|
||||
// Render the layer to a RenderTarget with the given transformation parameters
|
||||
// Uses cached texture if available, only re-renders when dirty
|
||||
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);
|
||||
|
||||
// #148 - Render all content to cached texture
|
||||
void renderToTexture(int cell_width, int cell_height) override;
|
||||
|
||||
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);
|
||||
|
||||
// #148 - Render all content to cached texture
|
||||
void renderToTexture(int cell_width, int cell_height) override;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
547
src/UIGrid.cpp
547
src/UIGrid.cpp
|
|
@ -5,13 +5,14 @@
|
|||
#include "UIEntity.h"
|
||||
#include "Profiler.h"
|
||||
#include <algorithm>
|
||||
#include <cmath> // #142 - for std::floor
|
||||
#include <cmath> // #142 - for std::floor
|
||||
#include <cstring> // #150 - for strcmp
|
||||
// UIDrawable methods now in UIBase.h
|
||||
|
||||
UIGrid::UIGrid()
|
||||
UIGrid::UIGrid()
|
||||
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||
perspective_enabled(false) // Default to omniscient view
|
||||
perspective_enabled(false), use_chunks(false) // Default to omniscient view
|
||||
{
|
||||
// Initialize entities list
|
||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||
|
|
@ -24,30 +25,31 @@ UIGrid::UIGrid()
|
|||
position = sf::Vector2f(0, 0); // Set base class position
|
||||
box.setPosition(position); // Sync box position
|
||||
box.setFillColor(sf::Color(0, 0, 0, 0));
|
||||
|
||||
|
||||
// Initialize render texture (small default size)
|
||||
renderTexture.create(1, 1);
|
||||
|
||||
|
||||
// Initialize output sprite
|
||||
output.setTextureRect(sf::IntRect(0, 0, 0, 0));
|
||||
output.setPosition(0, 0);
|
||||
output.setTexture(renderTexture.getTexture());
|
||||
|
||||
|
||||
// Points vector starts empty (grid_x * grid_y = 0)
|
||||
// TCOD map will be created when grid is resized
|
||||
}
|
||||
|
||||
UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
|
||||
: grid_x(gx), grid_y(gy),
|
||||
zoom(1.0f),
|
||||
ptex(_ptex), points(gx * gy),
|
||||
zoom(1.0f),
|
||||
ptex(_ptex),
|
||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||
perspective_enabled(false) // Default to omniscient view
|
||||
perspective_enabled(false),
|
||||
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
|
||||
{
|
||||
// Use texture dimensions if available, otherwise use defaults
|
||||
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
||||
int cell_height = _ptex ? _ptex->sprite_height : DEFAULT_CELL_HEIGHT;
|
||||
|
||||
|
||||
center_x = (gx/2) * cell_width;
|
||||
center_y = (gy/2) * cell_height;
|
||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||
|
|
@ -57,12 +59,12 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
|
||||
box.setSize(_wh);
|
||||
position = _xy; // Set base class position
|
||||
box.setPosition(position); // Sync box position
|
||||
box.setPosition(position); // Sync box position
|
||||
|
||||
box.setFillColor(sf::Color(0,0,0,0));
|
||||
// create renderTexture with maximum theoretical size; sprite can resize to show whatever amount needs to be rendered
|
||||
renderTexture.create(1920, 1080); // TODO - renderTexture should be window size; above 1080p this will cause rendering errors
|
||||
|
||||
|
||||
// Only initialize sprite if texture is available
|
||||
if (ptex) {
|
||||
sprite = ptex->sprite(0);
|
||||
|
|
@ -77,23 +79,47 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
|
||||
// Create TCOD map
|
||||
tcod_map = new TCODMap(gx, gy);
|
||||
|
||||
|
||||
// Create TCOD dijkstra pathfinder
|
||||
tcod_dijkstra = new TCODDijkstra(tcod_map);
|
||||
|
||||
|
||||
// Create TCOD A* pathfinder
|
||||
tcod_path = new TCODPath(tcod_map);
|
||||
|
||||
// Initialize grid points with parent reference
|
||||
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;
|
||||
|
||||
// #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();
|
||||
}
|
||||
|
|
@ -134,55 +160,21 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom);
|
||||
int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom);
|
||||
|
||||
//sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||
sf::RectangleShape r; // for colors and overlays
|
||||
r.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||
r.setOutlineThickness(0);
|
||||
|
||||
int x_limit = left_edge + width_sq + 2;
|
||||
if (x_limit > grid_x) x_limit = grid_x;
|
||||
|
||||
int y_limit = top_edge + height_sq + 2;
|
||||
if (y_limit > grid_y) y_limit = grid_y;
|
||||
|
||||
// base layer - bottom color, tile sprite ("ground")
|
||||
int cellsRendered = 0;
|
||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0);
|
||||
x < x_limit; //x < view_width;
|
||||
x+=1)
|
||||
{
|
||||
//for (float y = (top_edge >= 0 ? top_edge : 0);
|
||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0);
|
||||
y < y_limit; //y < view_height;
|
||||
y+=1)
|
||||
{
|
||||
auto pixel_pos = sf::Vector2f(
|
||||
(x*cell_width - left_spritepixels) * zoom,
|
||||
(y*cell_height - top_spritepixels) * zoom );
|
||||
|
||||
auto gridpoint = at(std::floor(x), std::floor(y));
|
||||
|
||||
//sprite.setPosition(pixel_pos);
|
||||
|
||||
r.setPosition(pixel_pos);
|
||||
r.setFillColor(gridpoint.color);
|
||||
renderTexture.draw(r);
|
||||
|
||||
// tilesprite - only draw if texture is available
|
||||
// if discovered but not visible, set opacity to 90%
|
||||
// if not discovered... just don't draw it?
|
||||
if (ptex && gridpoint.tilesprite != -1) {
|
||||
sprite = ptex->sprite(gridpoint.tilesprite, pixel_pos, sf::Vector2f(zoom, zoom)); //setSprite(gridpoint.tilesprite);;
|
||||
renderTexture.draw(sprite);
|
||||
}
|
||||
|
||||
cellsRendered++;
|
||||
}
|
||||
// #150 - Layers are now the sole source of grid rendering (base layer removed)
|
||||
// Render 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);
|
||||
}
|
||||
|
||||
// Record how many cells were rendered
|
||||
Resources::game->metrics.gridCellsRendered += cellsRendered;
|
||||
|
||||
// middle layer - entities
|
||||
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
||||
{
|
||||
|
|
@ -217,6 +209,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()) {
|
||||
|
|
@ -353,6 +352,10 @@ 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_x + x];
|
||||
}
|
||||
|
||||
|
|
@ -377,6 +380,58 @@ 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_x, grid_y, 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_x, grid_y, 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()
|
||||
{
|
||||
|
|
@ -677,6 +732,7 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
PyObject* textureObj = nullptr;
|
||||
PyObject* fill_color = nullptr;
|
||||
PyObject* click_handler = nullptr;
|
||||
PyObject* layers_obj = nullptr; // #150 - layers dict
|
||||
float center_x = 0.0f, center_y = 0.0f;
|
||||
float zoom = 1.0f;
|
||||
// perspective is now handled via properties, not init args
|
||||
|
|
@ -686,21 +742,23 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
const char* name = nullptr;
|
||||
float x = 0.0f, y = 0.0f, w = 0.0f, h = 0.0f;
|
||||
int grid_x = 2, grid_y = 2; // Default to 2x2 grid
|
||||
|
||||
|
||||
// Keywords list matches the new spec: positional args first, then all keyword args
|
||||
static const char* kwlist[] = {
|
||||
"pos", "size", "grid_size", "texture", // Positional args (as per spec)
|
||||
// Keyword-only args
|
||||
"fill_color", "click", "center_x", "center_y", "zoom",
|
||||
"visible", "opacity", "z_index", "name", "x", "y", "w", "h", "grid_x", "grid_y",
|
||||
"layers", // #150 - layers dict parameter
|
||||
nullptr
|
||||
};
|
||||
|
||||
|
||||
// Parse arguments with | for optional positional args
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffii", const_cast<char**>(kwlist),
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOOOOfffifizffffiiO", const_cast<char**>(kwlist),
|
||||
&pos_obj, &size_obj, &grid_size_obj, &textureObj, // Positional
|
||||
&fill_color, &click_handler, ¢er_x, ¢er_y, &zoom,
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y)) {
|
||||
&visible, &opacity, &z_index, &name, &x, &y, &w, &h, &grid_x, &grid_y,
|
||||
&layers_obj)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
|
@ -833,7 +891,55 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
}
|
||||
self->data->click_register(click_handler);
|
||||
}
|
||||
|
||||
|
||||
// #150 - Handle layers dict
|
||||
// Default: {"tilesprite": "tile"} when layers not provided
|
||||
// Empty dict: no rendering layers (entity storage + pathfinding only)
|
||||
if (layers_obj == nullptr) {
|
||||
// Default layer: single TileLayer named "tilesprite" (z_index -1 = below entities)
|
||||
self->data->addTileLayer(-1, texture_ptr, "tilesprite");
|
||||
} else if (layers_obj != Py_None) {
|
||||
if (!PyDict_Check(layers_obj)) {
|
||||
PyErr_SetString(PyExc_TypeError, "layers must be a dict mapping names to types ('color' or 'tile')");
|
||||
return -1;
|
||||
}
|
||||
|
||||
PyObject* key;
|
||||
PyObject* value;
|
||||
Py_ssize_t pos = 0;
|
||||
int layer_z = -1; // Start at -1 (below entities), decrement for each layer
|
||||
|
||||
while (PyDict_Next(layers_obj, &pos, &key, &value)) {
|
||||
if (!PyUnicode_Check(key)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Layer names must be strings");
|
||||
return -1;
|
||||
}
|
||||
if (!PyUnicode_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Layer types must be strings ('color' or 'tile')");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* layer_name = PyUnicode_AsUTF8(key);
|
||||
const char* layer_type = PyUnicode_AsUTF8(value);
|
||||
|
||||
// Check for protected names
|
||||
if (UIGrid::isProtectedLayerName(layer_name)) {
|
||||
PyErr_Format(PyExc_ValueError, "Layer name '%s' is reserved", layer_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strcmp(layer_type, "color") == 0) {
|
||||
self->data->addColorLayer(layer_z--, layer_name);
|
||||
} else if (strcmp(layer_type, "tile") == 0) {
|
||||
self->data->addTileLayer(layer_z--, texture_ptr, layer_name);
|
||||
} else {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown layer type '%s' (expected 'color' or 'tile')", layer_type);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// else: layers_obj is Py_None - explicit empty, no layers created
|
||||
|
||||
// Initialize weak reference list
|
||||
self->weakreflist = NULL;
|
||||
|
||||
|
|
@ -1064,7 +1170,8 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
|||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPoint");
|
||||
auto obj = (PyUIGridPointObject*)type->tp_alloc(type, 0);
|
||||
//auto target = std::static_pointer_cast<UIEntity>(target);
|
||||
obj->data = &(self->data->points[x + self->data->grid_x * y]);
|
||||
// #123 - Use at() method to route through chunks for large grids
|
||||
obj->data = &(self->data->at(x, y));
|
||||
obj->grid = self->data;
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
|
@ -1183,41 +1290,10 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
|
|||
|
||||
// Compute FOV
|
||||
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
|
||||
|
||||
// Build list of visible cells as tuples (x, y, visible, discovered)
|
||||
PyObject* result_list = PyList_New(0);
|
||||
if (!result_list) return NULL;
|
||||
|
||||
// Iterate through grid and collect visible cells
|
||||
for (int gy = 0; gy < self->data->grid_y; gy++) {
|
||||
for (int gx = 0; gx < self->data->grid_x; gx++) {
|
||||
if (self->data->isInFOV(gx, gy)) {
|
||||
// Create tuple (x, y, visible, discovered)
|
||||
PyObject* cell_tuple = PyTuple_New(4);
|
||||
if (!cell_tuple) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyTuple_SET_ITEM(cell_tuple, 0, PyLong_FromLong(gx));
|
||||
PyTuple_SET_ITEM(cell_tuple, 1, PyLong_FromLong(gy));
|
||||
PyTuple_SET_ITEM(cell_tuple, 2, Py_True); // visible
|
||||
PyTuple_SET_ITEM(cell_tuple, 3, Py_True); // discovered
|
||||
Py_INCREF(Py_True); // Need to increment ref count for True
|
||||
Py_INCREF(Py_True);
|
||||
|
||||
// Append to list
|
||||
if (PyList_Append(result_list, cell_tuple) < 0) {
|
||||
Py_DECREF(cell_tuple);
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(cell_tuple); // List now owns the reference
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result_list;
|
||||
|
||||
// Return None - use is_in_fov() to query visibility
|
||||
// See issue #146: returning a list had O(grid_size) performance
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
|
||||
|
|
@ -1332,23 +1408,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", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\n"
|
||||
" light_walls: Whether walls are lit when visible\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||
"Returns:\n"
|
||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
||||
" - x, y: Grid coordinates\n"
|
||||
" - visible: True (all returned cells are visible)\n"
|
||||
" - discovered: True (FOV implies discovery)\n\n"
|
||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
||||
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
|
|
@ -1410,6 +1688,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}
|
||||
};
|
||||
|
||||
|
|
@ -1420,21 +1704,16 @@ typedef PyUIGridObject PyObjectType;
|
|||
PyMethodDef UIGrid_all_methods[] = {
|
||||
UIDRAWABLE_METHODS,
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n"
|
||||
"Compute field of view from a position and return visible cells.\n\n"
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\n"
|
||||
" light_walls: Whether walls are lit when visible\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||
"Returns:\n"
|
||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
||||
" - x, y: Grid coordinates\n"
|
||||
" - visible: True (all returned cells are visible)\n"
|
||||
" - discovered: True (FOV implies discovery)\n\n"
|
||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
||||
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
|
|
@ -1496,6 +1775,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
|
||||
};
|
||||
|
||||
|
|
@ -1512,6 +1812,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)},
|
||||
|
|
|
|||
31
src/UIGrid.h
31
src/UIGrid.h
|
|
@ -20,6 +20,8 @@
|
|||
#include "UIEntity.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIBase.h"
|
||||
#include "GridLayers.h"
|
||||
#include "GridChunk.h"
|
||||
|
||||
class UIGrid: public UIDrawable
|
||||
{
|
||||
|
|
@ -74,13 +76,35 @@ public:
|
|||
std::shared_ptr<PyTexture> getTexture();
|
||||
sf::Sprite sprite, output;
|
||||
sf::RenderTexture renderTexture;
|
||||
|
||||
// #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;
|
||||
|
||||
// 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);
|
||||
|
||||
// Background rendering
|
||||
sf::Color fill_color;
|
||||
|
||||
|
|
@ -147,7 +171,12 @@ public:
|
|||
static PyObject* get_on_cell_click(PyUIGridObject* self, void* closure);
|
||||
static int set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_hovered_cell(PyUIGridObject* self, void* closure);
|
||||
|
||||
|
||||
// #147 - Layer system Python API
|
||||
static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
||||
static PyObject* py_layer(PyUIGridObject* self, PyObject* args);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
#include "UIGridPoint.h"
|
||||
#include "UIGrid.h"
|
||||
#include "GridLayers.h" // #150 - for GridLayerType, ColorLayer, TileLayer
|
||||
#include <cstring> // #150 - for strcmp
|
||||
|
||||
UIGridPoint::UIGridPoint()
|
||||
: color(1.0f, 1.0f, 1.0f), color_overlay(0.0f, 0.0f, 0.0f), walkable(false), transparent(false),
|
||||
tilesprite(-1), tile_overlay(-1), uisprite(-1), grid_x(-1), grid_y(-1), parent_grid(nullptr)
|
||||
: walkable(false), transparent(false), grid_x(-1), grid_y(-1), parent_grid(nullptr)
|
||||
{}
|
||||
|
||||
// Utility function to convert sf::Color to PyObject*
|
||||
|
|
@ -51,28 +52,7 @@ sf::Color PyObject_to_sfColor(PyObject* obj) {
|
|||
return sf::Color(r, g, b, a);
|
||||
}
|
||||
|
||||
PyObject* UIGridPoint::get_color(PyUIGridPointObject* self, void* closure) {
|
||||
if (reinterpret_cast<long>(closure) == 0) { // color
|
||||
return sfColor_to_PyObject(self->data->color);
|
||||
} else { // color_overlay
|
||||
return sfColor_to_PyObject(self->data->color_overlay);
|
||||
}
|
||||
}
|
||||
|
||||
int UIGridPoint::set_color(PyUIGridPointObject* self, PyObject* value, void* closure) {
|
||||
sf::Color color = PyObject_to_sfColor(value);
|
||||
// Check if an error occurred during conversion
|
||||
if (PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (reinterpret_cast<long>(closure) == 0) { // color
|
||||
self->data->color = color;
|
||||
} else { // color_overlay
|
||||
self->data->color_overlay = color;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// #150 - Removed get_color/set_color - now handled by layers
|
||||
|
||||
PyObject* UIGridPoint::get_bool_member(PyUIGridPointObject* self, void* closure) {
|
||||
if (reinterpret_cast<long>(closure) == 0) { // walkable
|
||||
|
|
@ -108,36 +88,11 @@ int UIGridPoint::set_bool_member(PyUIGridPointObject* self, PyObject* value, voi
|
|||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIGridPoint::get_int_member(PyUIGridPointObject* self, void* closure) {
|
||||
switch(reinterpret_cast<long>(closure)) {
|
||||
case 0: return PyLong_FromLong(self->data->tilesprite);
|
||||
case 1: return PyLong_FromLong(self->data->tile_overlay);
|
||||
case 2: return PyLong_FromLong(self->data->uisprite);
|
||||
default: PyErr_SetString(PyExc_RuntimeError, "Invalid closure"); return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int UIGridPoint::set_int_member(PyUIGridPointObject* self, PyObject* value, void* closure) {
|
||||
long val = PyLong_AsLong(value);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
|
||||
switch(reinterpret_cast<long>(closure)) {
|
||||
case 0: self->data->tilesprite = val; break;
|
||||
case 1: self->data->tile_overlay = val; break;
|
||||
case 2: self->data->uisprite = val; break;
|
||||
default: PyErr_SetString(PyExc_RuntimeError, "Invalid closure"); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// #150 - Removed get_int_member/set_int_member - now handled by layers
|
||||
|
||||
PyGetSetDef UIGridPoint::getsetters[] = {
|
||||
{"color", (getter)UIGridPoint::get_color, (setter)UIGridPoint::set_color, "GridPoint color", (void*)0},
|
||||
{"color_overlay", (getter)UIGridPoint::get_color, (setter)UIGridPoint::set_color, "GridPoint color overlay", (void*)1},
|
||||
{"walkable", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member, "Is the GridPoint walkable", (void*)0},
|
||||
{"transparent", (getter)UIGridPoint::get_bool_member, (setter)UIGridPoint::set_bool_member, "Is the GridPoint transparent", (void*)1},
|
||||
{"tilesprite", (getter)UIGridPoint::get_int_member, (setter)UIGridPoint::set_int_member, "Tile sprite index", (void*)0},
|
||||
{"tile_overlay", (getter)UIGridPoint::get_int_member, (setter)UIGridPoint::set_int_member, "Tile overlay sprite index", (void*)1},
|
||||
{"uisprite", (getter)UIGridPoint::get_int_member, (setter)UIGridPoint::set_int_member, "UI sprite index", (void*)2},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
|
@ -146,9 +101,9 @@ PyObject* UIGridPoint::repr(PyUIGridPointObject* self) {
|
|||
if (!self->data) ss << "<GridPoint (invalid internal object)>";
|
||||
else {
|
||||
auto gp = self->data;
|
||||
ss << "<GridPoint (walkable=" << (gp->walkable ? "True" : "False") << ", transparent=" << (gp->transparent ? "True" : "False") <<
|
||||
", tilesprite=" << gp->tilesprite << ", tile_overlay=" << gp->tile_overlay << ", uisprite=" << gp->uisprite <<
|
||||
")>";
|
||||
ss << "<GridPoint (walkable=" << (gp->walkable ? "True" : "False")
|
||||
<< ", transparent=" << (gp->transparent ? "True" : "False")
|
||||
<< ") at (" << gp->grid_x << ", " << gp->grid_y << ")>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
|
|
@ -193,9 +148,103 @@ PyObject* UIGridPointState::repr(PyUIGridPointStateObject* self) {
|
|||
if (!self->data) ss << "<GridPointState (invalid internal object)>";
|
||||
else {
|
||||
auto gps = self->data;
|
||||
ss << "<GridPointState (visible=" << (gps->visible ? "True" : "False") << ", discovered=" << (gps->discovered ? "True" : "False") <<
|
||||
ss << "<GridPointState (visible=" << (gps->visible ? "True" : "False") << ", discovered=" << (gps->discovered ? "True" : "False") <<
|
||||
")>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
// #150 - Dynamic attribute access for named layers
|
||||
PyObject* UIGridPoint::getattro(PyUIGridPointObject* self, PyObject* name) {
|
||||
// First try standard attribute lookup (built-in properties)
|
||||
PyObject* result = PyObject_GenericGetAttr((PyObject*)self, name);
|
||||
if (result != nullptr || !PyErr_ExceptionMatches(PyExc_AttributeError)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Clear the AttributeError and check for layer name
|
||||
PyErr_Clear();
|
||||
|
||||
if (!self->grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const char* attr_name = PyUnicode_AsUTF8(name);
|
||||
if (!attr_name) return nullptr;
|
||||
|
||||
// Look up layer by name
|
||||
auto layer = self->grid->getLayerByName(attr_name);
|
||||
if (!layer) {
|
||||
PyErr_Format(PyExc_AttributeError, "'GridPoint' object has no attribute '%s'", attr_name);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int x = self->data->grid_x;
|
||||
int y = self->data->grid_y;
|
||||
|
||||
// Get value based on layer type
|
||||
if (layer->type == GridLayerType::Color) {
|
||||
auto color_layer = std::static_pointer_cast<ColorLayer>(layer);
|
||||
return sfColor_to_PyObject(color_layer->at(x, y));
|
||||
} else if (layer->type == GridLayerType::Tile) {
|
||||
auto tile_layer = std::static_pointer_cast<TileLayer>(layer);
|
||||
return PyLong_FromLong(tile_layer->at(x, y));
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unknown layer type");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int UIGridPoint::setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value) {
|
||||
// First try standard attribute setting (built-in properties)
|
||||
// We need to check if this is a known attribute first
|
||||
const char* attr_name = PyUnicode_AsUTF8(name);
|
||||
if (!attr_name) return -1;
|
||||
|
||||
// Check if it's a built-in property (defined in getsetters)
|
||||
for (PyGetSetDef* gsd = UIGridPoint::getsetters; gsd->name != nullptr; gsd++) {
|
||||
if (strcmp(gsd->name, attr_name) == 0) {
|
||||
// It's a built-in property, use standard setter
|
||||
return PyObject_GenericSetAttr((PyObject*)self, name, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Not a built-in property - try layer lookup
|
||||
if (!self->grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "GridPoint has no parent grid");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto layer = self->grid->getLayerByName(attr_name);
|
||||
if (!layer) {
|
||||
PyErr_Format(PyExc_AttributeError, "'GridPoint' object has no attribute '%s'", attr_name);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int x = self->data->grid_x;
|
||||
int y = self->data->grid_y;
|
||||
|
||||
// Set value based on layer type
|
||||
if (layer->type == GridLayerType::Color) {
|
||||
auto color_layer = std::static_pointer_cast<ColorLayer>(layer);
|
||||
sf::Color color = PyObject_to_sfColor(value);
|
||||
if (PyErr_Occurred()) return -1;
|
||||
color_layer->at(x, y) = color;
|
||||
color_layer->markDirty();
|
||||
return 0;
|
||||
} else if (layer->type == GridLayerType::Tile) {
|
||||
auto tile_layer = std::static_pointer_cast<TileLayer>(layer);
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Tile layer values must be integers");
|
||||
return -1;
|
||||
}
|
||||
tile_layer->at(x, y) = PyLong_AsLong(value);
|
||||
tile_layer->markDirty();
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unknown layer type");
|
||||
return -1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,25 +33,25 @@ typedef struct {
|
|||
std::shared_ptr<UIEntity> entity;
|
||||
} PyUIGridPointStateObject;
|
||||
|
||||
// UIGridPoint - revised grid data for each point
|
||||
// UIGridPoint - grid cell data for pathfinding and layer access
|
||||
// #150 - Layer-related properties (color, tilesprite, etc.) removed; now handled by layers
|
||||
class UIGridPoint
|
||||
{
|
||||
public:
|
||||
sf::Color color, color_overlay;
|
||||
bool walkable, transparent;
|
||||
int tilesprite, tile_overlay, uisprite;
|
||||
int grid_x, grid_y; // Position in parent grid
|
||||
UIGrid* parent_grid; // Parent grid reference for TCOD sync
|
||||
bool walkable, transparent; // Pathfinding/FOV properties
|
||||
int grid_x, grid_y; // Position in parent grid
|
||||
UIGrid* parent_grid; // Parent grid reference for TCOD sync
|
||||
UIGridPoint();
|
||||
|
||||
static int set_int_member(PyUIGridPointObject* self, PyObject* value, void* closure);
|
||||
// Built-in property accessors (walkable, transparent only)
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* get_color(PyUIGridPointObject* self, void* closure);
|
||||
static PyObject* get_int_member(PyUIGridPointObject* self, void* closure);
|
||||
static int set_bool_member(PyUIGridPointObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_bool_member(PyUIGridPointObject* self, void* closure);
|
||||
static int set_color(PyUIGridPointObject* self, PyObject* value, void* closure);
|
||||
static PyObject* repr(PyUIGridPointObject* self);
|
||||
|
||||
// #150 - Dynamic property access for named layers
|
||||
static PyObject* getattro(PyUIGridPointObject* self, PyObject* name);
|
||||
static int setattro(PyUIGridPointObject* self, PyObject* name, PyObject* value);
|
||||
};
|
||||
|
||||
// UIGridPointState - entity-specific info for each cell
|
||||
|
|
@ -73,6 +73,9 @@ namespace mcrfpydef {
|
|||
.tp_basicsize = sizeof(PyUIGridPointObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_repr = (reprfunc)UIGridPoint::repr,
|
||||
// #150 - Dynamic attribute access for named layers
|
||||
.tp_getattro = (getattrofunc)UIGridPoint::getattro,
|
||||
.tp_setattro = (setattrofunc)UIGridPoint::setattro,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "UIGridPoint object",
|
||||
.tp_getset = UIGridPoint::getsetters,
|
||||
|
|
|
|||
|
|
@ -105,16 +105,16 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
|
|||
*/
|
||||
|
||||
// UIGrid test: (in grid cells) ( in screen pixels )
|
||||
// constructor args: w h texture x y w h
|
||||
// constructor args: w h texture x y w h
|
||||
auto e5 = std::make_shared<UIGrid>(4, 4, ptex, sf::Vector2f(550, 150), sf::Vector2f(200, 200));
|
||||
e5->zoom=2.0;
|
||||
e5->points[0].color = sf::Color(255, 0, 0);
|
||||
e5->points[1].tilesprite = 1;
|
||||
e5->points[5].color = sf::Color(0, 255, 0);
|
||||
e5->points[6].tilesprite = 2;
|
||||
e5->points[10].color = sf::Color(0, 0, 255);
|
||||
e5->points[11].tilesprite = 3;
|
||||
e5->points[15].color = sf::Color(255, 255, 255);
|
||||
|
||||
// #150 - GridPoint no longer has color/tilesprite properties
|
||||
// Use layers for visual rendering; GridPoint only has walkable/transparent
|
||||
// The default "tilesprite" TileLayer is created automatically
|
||||
// Example: e5->layers[0]->at(x, y) = tile_index for TileLayer
|
||||
e5->points[0].walkable = true;
|
||||
e5->points[0].transparent = true;
|
||||
|
||||
ui_elements->push_back(e5);
|
||||
|
||||
|
|
|
|||
|
|
@ -105,7 +105,9 @@ class Level:
|
|||
self.height = height
|
||||
#self.graph = [(0, 0, width, height)]
|
||||
self.graph = RoomGraph( (0, 0, width, height) )
|
||||
self.grid = mcrfpy.Grid(grid_size=(width, height), texture=t, pos=(10, 5), size=(1014, 700))
|
||||
# #150 - Create grid with explicit layers for color and tilesprite
|
||||
self.grid = mcrfpy.Grid(grid_size=(width, height), texture=t, pos=(10, 5), size=(1014, 700),
|
||||
layers={"color": "color", "tilesprite": "tile"})
|
||||
self.highlighted = -1 #debug view feature
|
||||
self.walled_rooms = [] # for tracking "hallway rooms" vs "walled rooms"
|
||||
|
||||
|
|
|
|||
407010
tests/benchmarks/baseline/headless_animation_stress.json
Normal file
407010
tests/benchmarks/baseline/headless_animation_stress.json
Normal file
File diff suppressed because it is too large
Load diff
194685
tests/benchmarks/baseline/headless_deep_nesting.json
Normal file
194685
tests/benchmarks/baseline/headless_deep_nesting.json
Normal file
File diff suppressed because it is too large
Load diff
677304
tests/benchmarks/baseline/headless_deep_nesting_cached.json
Normal file
677304
tests/benchmarks/baseline/headless_deep_nesting_cached.json
Normal file
File diff suppressed because it is too large
Load diff
16522
tests/benchmarks/baseline/headless_large_grid.json
Normal file
16522
tests/benchmarks/baseline/headless_large_grid.json
Normal file
File diff suppressed because it is too large
Load diff
74757
tests/benchmarks/baseline/headless_many_captions.json
Normal file
74757
tests/benchmarks/baseline/headless_many_captions.json
Normal file
File diff suppressed because it is too large
Load diff
67005
tests/benchmarks/baseline/headless_many_frames.json
Normal file
67005
tests/benchmarks/baseline/headless_many_frames.json
Normal file
File diff suppressed because it is too large
Load diff
253604
tests/benchmarks/baseline/headless_many_sprites.json
Normal file
253604
tests/benchmarks/baseline/headless_many_sprites.json
Normal file
File diff suppressed because it is too large
Load diff
18688
tests/benchmarks/baseline/headless_static_scene.json
Normal file
18688
tests/benchmarks/baseline/headless_static_scene.json
Normal file
File diff suppressed because it is too large
Load diff
14375
tests/benchmarks/baseline/headless_static_scene_cached.json
Normal file
14375
tests/benchmarks/baseline/headless_static_scene_cached.json
Normal file
File diff suppressed because it is too large
Load diff
51
tests/benchmarks/baseline/headless_summary.json
Normal file
51
tests/benchmarks/baseline/headless_summary.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"timestamp": "2025-11-28T19:22:01.900442",
|
||||
"mode": "headless",
|
||||
"results": {
|
||||
"many_frames": {
|
||||
"avg_work_ms": 0.5644053203661328,
|
||||
"max_work_ms": 1.78,
|
||||
"frame_count": 3496
|
||||
},
|
||||
"many_sprites": {
|
||||
"avg_work_ms": 0.14705301494330555,
|
||||
"max_work_ms": 11.814,
|
||||
"frame_count": 13317
|
||||
},
|
||||
"many_captions": {
|
||||
"avg_work_ms": 0.49336296106557376,
|
||||
"max_work_ms": 2.202,
|
||||
"frame_count": 3904
|
||||
},
|
||||
"deep_nesting": {
|
||||
"avg_work_ms": 0.3517734925606891,
|
||||
"max_work_ms": 145.75,
|
||||
"frame_count": 10216
|
||||
},
|
||||
"deep_nesting_cached": {
|
||||
"avg_work_ms": 0.0942947468905298,
|
||||
"max_work_ms": 100.242,
|
||||
"frame_count": 35617
|
||||
},
|
||||
"large_grid": {
|
||||
"avg_work_ms": 2.2851537544696066,
|
||||
"max_work_ms": 11.534,
|
||||
"frame_count": 839
|
||||
},
|
||||
"animation_stress": {
|
||||
"avg_work_ms": 0.0924456547145996,
|
||||
"max_work_ms": 11.933,
|
||||
"frame_count": 21391
|
||||
},
|
||||
"static_scene": {
|
||||
"avg_work_ms": 2.022726128016789,
|
||||
"max_work_ms": 17.275,
|
||||
"frame_count": 953
|
||||
},
|
||||
"static_scene_cached": {
|
||||
"avg_work_ms": 2.694431129476584,
|
||||
"max_work_ms": 22.059,
|
||||
"frame_count": 726
|
||||
}
|
||||
}
|
||||
}
|
||||
2291
tests/benchmarks/baseline/windowed_animation_stress.json
Normal file
2291
tests/benchmarks/baseline/windowed_animation_stress.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_deep_nesting.json
Normal file
2291
tests/benchmarks/baseline/windowed_deep_nesting.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_large_grid.json
Normal file
2291
tests/benchmarks/baseline/windowed_large_grid.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_captions.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_captions.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_frames.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_frames.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_many_sprites.json
Normal file
2291
tests/benchmarks/baseline/windowed_many_sprites.json
Normal file
File diff suppressed because it is too large
Load diff
2291
tests/benchmarks/baseline/windowed_static_scene.json
Normal file
2291
tests/benchmarks/baseline/windowed_static_scene.json
Normal file
File diff suppressed because it is too large
Load diff
41
tests/benchmarks/baseline/windowed_summary.json
Normal file
41
tests/benchmarks/baseline/windowed_summary.json
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"timestamp": "2025-11-28T16:53:30.850948",
|
||||
"mode": "windowed",
|
||||
"results": {
|
||||
"many_frames": {
|
||||
"avg_work_ms": 1.5756444444444444,
|
||||
"max_work_ms": 3.257,
|
||||
"frame_count": 90
|
||||
},
|
||||
"many_sprites": {
|
||||
"avg_work_ms": 0.6889555555555555,
|
||||
"max_work_ms": 1.533,
|
||||
"frame_count": 90
|
||||
},
|
||||
"many_captions": {
|
||||
"avg_work_ms": 1.2975777777777777,
|
||||
"max_work_ms": 3.386,
|
||||
"frame_count": 90
|
||||
},
|
||||
"deep_nesting": {
|
||||
"avg_work_ms": 0.6173444444444445,
|
||||
"max_work_ms": 1.4,
|
||||
"frame_count": 90
|
||||
},
|
||||
"large_grid": {
|
||||
"avg_work_ms": 3.6094,
|
||||
"max_work_ms": 6.631,
|
||||
"frame_count": 90
|
||||
},
|
||||
"animation_stress": {
|
||||
"avg_work_ms": 0.5419333333333334,
|
||||
"max_work_ms": 1.081,
|
||||
"frame_count": 90
|
||||
},
|
||||
"static_scene": {
|
||||
"avg_work_ms": 3.321588888888889,
|
||||
"max_work_ms": 11.905,
|
||||
"frame_count": 90
|
||||
}
|
||||
}
|
||||
}
|
||||
385
tests/benchmarks/layer_performance_test.py
Normal file
385
tests/benchmarks/layer_performance_test.py
Normal file
|
|
@ -0,0 +1,385 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Layer Performance Benchmark for McRogueFace (#147, #148, #123)
|
||||
|
||||
Uses C++ benchmark logger (start_benchmark/end_benchmark) for accurate timing.
|
||||
Results written to JSON files for analysis.
|
||||
|
||||
Compares rendering performance between:
|
||||
1. Traditional grid.at(x,y).color API (no caching)
|
||||
2. New layer system with dirty flag caching
|
||||
3. Various layer configurations
|
||||
|
||||
Usage:
|
||||
./mcrogueface --exec tests/benchmarks/layer_performance_test.py
|
||||
# Results in benchmark_*.json files
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
|
||||
# Test configuration
|
||||
GRID_SIZE = 100 # 100x100 = 10,000 cells
|
||||
MEASURE_FRAMES = 120
|
||||
WARMUP_FRAMES = 30
|
||||
|
||||
current_test = None
|
||||
frame_count = 0
|
||||
test_results = {} # Store filenames for each test
|
||||
|
||||
|
||||
def run_test_phase(runtime):
|
||||
"""Run through warmup and measurement phases."""
|
||||
global frame_count
|
||||
|
||||
frame_count += 1
|
||||
|
||||
if frame_count == WARMUP_FRAMES:
|
||||
# Start benchmark after warmup
|
||||
mcrfpy.start_benchmark()
|
||||
mcrfpy.log_benchmark(f"Test: {current_test}")
|
||||
|
||||
elif frame_count == WARMUP_FRAMES + MEASURE_FRAMES:
|
||||
# End benchmark and store filename
|
||||
filename = mcrfpy.end_benchmark()
|
||||
test_results[current_test] = filename
|
||||
print(f" {current_test}: saved to {filename}")
|
||||
|
||||
mcrfpy.delTimer("test_phase")
|
||||
run_next_test()
|
||||
|
||||
|
||||
def run_next_test():
|
||||
"""Run next test in sequence."""
|
||||
global current_test, frame_count
|
||||
|
||||
tests = [
|
||||
('1_base_static', setup_base_layer_static),
|
||||
('2_base_modified', setup_base_layer_modified),
|
||||
('3_layer_static', setup_color_layer_static),
|
||||
('4_layer_modified', setup_color_layer_modified),
|
||||
('5_tile_static', setup_tile_layer_static),
|
||||
('6_tile_modified', setup_tile_layer_modified),
|
||||
('7_multi_layer', setup_multi_layer_static),
|
||||
('8_comparison', setup_base_vs_layer_comparison),
|
||||
]
|
||||
|
||||
# Find current
|
||||
current_idx = -1
|
||||
if current_test:
|
||||
for i, (name, _) in enumerate(tests):
|
||||
if name == current_test:
|
||||
current_idx = i
|
||||
break
|
||||
|
||||
next_idx = current_idx + 1
|
||||
|
||||
if next_idx >= len(tests):
|
||||
analyze_results()
|
||||
return
|
||||
|
||||
current_test = tests[next_idx][0]
|
||||
frame_count = 0
|
||||
|
||||
print(f"\n[{next_idx + 1}/{len(tests)}] Running: {current_test}")
|
||||
tests[next_idx][1]()
|
||||
|
||||
mcrfpy.setTimer("test_phase", run_test_phase, 1)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Test Scenarios
|
||||
# ============================================================================
|
||||
|
||||
def setup_base_layer_static():
|
||||
"""Traditional grid.at(x,y).color API - no modifications during render."""
|
||||
mcrfpy.createScene("test_base_static")
|
||||
ui = mcrfpy.sceneUI("test_base_static")
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600))
|
||||
ui.append(grid)
|
||||
|
||||
# Fill base layer using traditional API
|
||||
for y in range(GRID_SIZE):
|
||||
for x in range(GRID_SIZE):
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255)
|
||||
|
||||
mcrfpy.setScene("test_base_static")
|
||||
|
||||
|
||||
def setup_base_layer_modified():
|
||||
"""Traditional API with single cell modified each frame."""
|
||||
mcrfpy.createScene("test_base_mod")
|
||||
ui = mcrfpy.sceneUI("test_base_mod")
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600))
|
||||
ui.append(grid)
|
||||
|
||||
# Fill base layer
|
||||
for y in range(GRID_SIZE):
|
||||
for x in range(GRID_SIZE):
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color(100, 100, 100, 255)
|
||||
|
||||
# Timer to modify one cell per frame
|
||||
mod_counter = [0]
|
||||
def modify_cell(runtime):
|
||||
x = mod_counter[0] % GRID_SIZE
|
||||
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color(255, 0, 0, 255)
|
||||
mod_counter[0] += 1
|
||||
|
||||
mcrfpy.setScene("test_base_mod")
|
||||
mcrfpy.setTimer("modify", modify_cell, 1)
|
||||
|
||||
|
||||
def setup_color_layer_static():
|
||||
"""New ColorLayer with dirty flag caching - static after fill."""
|
||||
mcrfpy.createScene("test_color_static")
|
||||
ui = mcrfpy.sceneUI("test_color_static")
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600))
|
||||
ui.append(grid)
|
||||
|
||||
# Add color layer and fill once
|
||||
layer = grid.add_layer("color", z_index=-1)
|
||||
layer.fill(mcrfpy.Color(100, 150, 200, 128))
|
||||
|
||||
mcrfpy.setScene("test_color_static")
|
||||
|
||||
|
||||
def setup_color_layer_modified():
|
||||
"""ColorLayer with single cell modified each frame - tests dirty flag."""
|
||||
mcrfpy.createScene("test_color_mod")
|
||||
ui = mcrfpy.sceneUI("test_color_mod")
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600))
|
||||
ui.append(grid)
|
||||
|
||||
layer = grid.add_layer("color", z_index=-1)
|
||||
layer.fill(mcrfpy.Color(100, 100, 100, 128))
|
||||
|
||||
# Timer to modify one cell per frame - triggers re-render
|
||||
mod_counter = [0]
|
||||
def modify_cell(runtime):
|
||||
x = mod_counter[0] % GRID_SIZE
|
||||
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
|
||||
layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
|
||||
mod_counter[0] += 1
|
||||
|
||||
mcrfpy.setScene("test_color_mod")
|
||||
mcrfpy.setTimer("modify", modify_cell, 1)
|
||||
|
||||
|
||||
def setup_tile_layer_static():
|
||||
"""TileLayer with caching - static after fill."""
|
||||
mcrfpy.createScene("test_tile_static")
|
||||
ui = mcrfpy.sceneUI("test_tile_static")
|
||||
|
||||
try:
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
except:
|
||||
texture = None
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600), texture=texture)
|
||||
ui.append(grid)
|
||||
|
||||
if texture:
|
||||
layer = grid.add_layer("tile", z_index=-1, texture=texture)
|
||||
layer.fill(5)
|
||||
|
||||
mcrfpy.setScene("test_tile_static")
|
||||
|
||||
|
||||
def setup_tile_layer_modified():
|
||||
"""TileLayer with single cell modified each frame."""
|
||||
mcrfpy.createScene("test_tile_mod")
|
||||
ui = mcrfpy.sceneUI("test_tile_mod")
|
||||
|
||||
try:
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
except:
|
||||
texture = None
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600), texture=texture)
|
||||
ui.append(grid)
|
||||
|
||||
layer = None
|
||||
if texture:
|
||||
layer = grid.add_layer("tile", z_index=-1, texture=texture)
|
||||
layer.fill(5)
|
||||
|
||||
# Timer to modify one cell per frame
|
||||
mod_counter = [0]
|
||||
def modify_cell(runtime):
|
||||
if layer:
|
||||
x = mod_counter[0] % GRID_SIZE
|
||||
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
|
||||
layer.set(x, y, (mod_counter[0] % 20))
|
||||
mod_counter[0] += 1
|
||||
|
||||
mcrfpy.setScene("test_tile_mod")
|
||||
mcrfpy.setTimer("modify", modify_cell, 1)
|
||||
|
||||
|
||||
def setup_multi_layer_static():
|
||||
"""Multiple layers (5 color, 5 tile) - all static."""
|
||||
mcrfpy.createScene("test_multi_static")
|
||||
ui = mcrfpy.sceneUI("test_multi_static")
|
||||
|
||||
try:
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
except:
|
||||
texture = None
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600), texture=texture)
|
||||
ui.append(grid)
|
||||
|
||||
# Add 5 color layers with different z_indices and colors
|
||||
for i in range(5):
|
||||
layer = grid.add_layer("color", z_index=-(i+1)*2)
|
||||
layer.fill(mcrfpy.Color(50 + i*30, 100 + i*20, 150 - i*20, 50))
|
||||
|
||||
# Add 5 tile layers
|
||||
if texture:
|
||||
for i in range(5):
|
||||
layer = grid.add_layer("tile", z_index=-(i+1)*2 - 1, texture=texture)
|
||||
layer.fill(i * 4)
|
||||
|
||||
print(f" Created {len(grid.layers)} layers")
|
||||
mcrfpy.setScene("test_multi_static")
|
||||
|
||||
|
||||
def setup_base_vs_layer_comparison():
|
||||
"""Direct comparison: same visual using base API vs layer API."""
|
||||
mcrfpy.createScene("test_comparison")
|
||||
ui = mcrfpy.sceneUI("test_comparison")
|
||||
|
||||
# Grid using ONLY the new layer system (no base layer colors)
|
||||
grid = mcrfpy.Grid(grid_size=(GRID_SIZE, GRID_SIZE),
|
||||
pos=(10, 10), size=(600, 600))
|
||||
ui.append(grid)
|
||||
|
||||
# Single color layer that covers everything
|
||||
layer = grid.add_layer("color", z_index=-1)
|
||||
|
||||
# Fill with pattern (same as base_layer_static but via layer)
|
||||
for y in range(GRID_SIZE):
|
||||
for x in range(GRID_SIZE):
|
||||
layer.set(x, y, mcrfpy.Color((x * 2) % 256, (y * 2) % 256, 128, 255))
|
||||
|
||||
mcrfpy.setScene("test_comparison")
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Results Analysis
|
||||
# ============================================================================
|
||||
|
||||
def analyze_results():
|
||||
"""Read JSON files and print comparison."""
|
||||
print("\n" + "=" * 70)
|
||||
print("LAYER PERFORMANCE BENCHMARK RESULTS")
|
||||
print("=" * 70)
|
||||
print(f"Grid size: {GRID_SIZE}x{GRID_SIZE} = {GRID_SIZE*GRID_SIZE:,} cells")
|
||||
print(f"Samples per test: {MEASURE_FRAMES} frames")
|
||||
|
||||
results = {}
|
||||
|
||||
for test_name, filename in test_results.items():
|
||||
if not os.path.exists(filename):
|
||||
print(f" WARNING: {filename} not found")
|
||||
continue
|
||||
|
||||
with open(filename, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
frames = data.get('frames', [])
|
||||
if not frames:
|
||||
continue
|
||||
|
||||
# Calculate averages
|
||||
avg_grid = sum(f['grid_render_ms'] for f in frames) / len(frames)
|
||||
avg_frame = sum(f['frame_time_ms'] for f in frames) / len(frames)
|
||||
avg_cells = sum(f['grid_cells_rendered'] for f in frames) / len(frames)
|
||||
avg_work = sum(f.get('work_time_ms', 0) for f in frames) / len(frames)
|
||||
|
||||
results[test_name] = {
|
||||
'avg_grid_ms': avg_grid,
|
||||
'avg_frame_ms': avg_frame,
|
||||
'avg_work_ms': avg_work,
|
||||
'avg_cells': avg_cells,
|
||||
'samples': len(frames),
|
||||
}
|
||||
|
||||
print(f"\n{'Test':<20} {'Grid (ms)':>10} {'Work (ms)':>10} {'Cells':>10}")
|
||||
print("-" * 70)
|
||||
|
||||
for name in sorted(results.keys()):
|
||||
r = results[name]
|
||||
print(f"{name:<20} {r['avg_grid_ms']:>10.3f} {r['avg_work_ms']:>10.3f} {r['avg_cells']:>10.0f}")
|
||||
|
||||
print("\n" + "-" * 70)
|
||||
print("ANALYSIS:")
|
||||
|
||||
# Compare base static vs layer static
|
||||
if '1_base_static' in results and '3_layer_static' in results:
|
||||
base = results['1_base_static']['avg_grid_ms']
|
||||
layer = results['3_layer_static']['avg_grid_ms']
|
||||
if base > 0.001:
|
||||
improvement = ((base - layer) / base) * 100
|
||||
print(f" Static ColorLayer vs Base: {improvement:+.1f}% "
|
||||
f"({'FASTER' if improvement > 0 else 'slower'})")
|
||||
print(f" Base: {base:.3f}ms, Layer: {layer:.3f}ms")
|
||||
|
||||
# Compare base modified vs layer modified
|
||||
if '2_base_modified' in results and '4_layer_modified' in results:
|
||||
base = results['2_base_modified']['avg_grid_ms']
|
||||
layer = results['4_layer_modified']['avg_grid_ms']
|
||||
if base > 0.001:
|
||||
improvement = ((base - layer) / base) * 100
|
||||
print(f" Modified ColorLayer vs Base: {improvement:+.1f}% "
|
||||
f"({'FASTER' if improvement > 0 else 'slower'})")
|
||||
print(f" Base: {base:.3f}ms, Layer: {layer:.3f}ms")
|
||||
|
||||
# Cache benefit (static vs modified for layers)
|
||||
if '3_layer_static' in results and '4_layer_modified' in results:
|
||||
static = results['3_layer_static']['avg_grid_ms']
|
||||
modified = results['4_layer_modified']['avg_grid_ms']
|
||||
if static > 0.001:
|
||||
overhead = ((modified - static) / static) * 100
|
||||
print(f" Layer cache hit vs miss: {overhead:+.1f}% "
|
||||
f"({'overhead when dirty' if overhead > 0 else 'benefit'})")
|
||||
print(f" Static: {static:.3f}ms, Modified: {modified:.3f}ms")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("Benchmark JSON files saved for detailed analysis.")
|
||||
print("Key insight: Base layer has NO caching; layers require opt-in.")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print("Layer Performance Benchmark (C++ timing)")
|
||||
print("=" * 70)
|
||||
print("\nThis benchmark compares:")
|
||||
print(" - Traditional grid.at(x,y).color API (renders every frame)")
|
||||
print(" - New layer system with dirty flag caching (#147, #148)")
|
||||
print(f"\nEach test: {WARMUP_FRAMES} warmup + {MEASURE_FRAMES} measured frames")
|
||||
|
||||
run_next_test()
|
||||
343
tests/benchmarks/stress_test_suite.py
Normal file
343
tests/benchmarks/stress_test_suite.py
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Stress Test Suite for McRogueFace Performance Analysis
|
||||
|
||||
Establishes baseline performance data before implementing texture caching (#144).
|
||||
Uses a single repeating timer pattern to avoid callback chain issues.
|
||||
|
||||
Usage:
|
||||
./mcrogueface --headless --exec tests/benchmarks/stress_test_suite.py
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Configuration
|
||||
TEST_DURATION_MS = 2000
|
||||
TIMER_INTERVAL_MS = 50
|
||||
OUTPUT_DIR = "../tests/benchmarks/baseline"
|
||||
IS_HEADLESS = True # Assume headless for automated testing
|
||||
|
||||
class StressTestRunner:
|
||||
def __init__(self):
|
||||
self.tests = []
|
||||
self.current_test = -1
|
||||
self.results = {}
|
||||
self.frames_counted = 0
|
||||
self.mode = "headless" if IS_HEADLESS else "windowed"
|
||||
|
||||
def add_test(self, name, setup_fn, description=""):
|
||||
self.tests.append({'name': name, 'setup': setup_fn, 'description': description})
|
||||
|
||||
def tick(self, runtime):
|
||||
"""Single timer callback that manages all test flow"""
|
||||
self.frames_counted += 1
|
||||
|
||||
# Check if current test should end
|
||||
if self.current_test >= 0 and self.frames_counted * TIMER_INTERVAL_MS >= TEST_DURATION_MS:
|
||||
self.end_current_test()
|
||||
self.start_next_test()
|
||||
elif self.current_test < 0:
|
||||
self.start_next_test()
|
||||
|
||||
def start_next_test(self):
|
||||
self.current_test += 1
|
||||
|
||||
if self.current_test >= len(self.tests):
|
||||
self.finish_suite()
|
||||
return
|
||||
|
||||
test = self.tests[self.current_test]
|
||||
print(f"\n[{self.current_test + 1}/{len(self.tests)}] {test['name']}")
|
||||
print(f" {test['description']}")
|
||||
|
||||
# Setup scene
|
||||
scene_name = f"stress_{self.current_test}"
|
||||
mcrfpy.createScene(scene_name)
|
||||
|
||||
# Start benchmark
|
||||
mcrfpy.start_benchmark()
|
||||
mcrfpy.log_benchmark(f"TEST: {test['name']}")
|
||||
|
||||
# Run setup
|
||||
try:
|
||||
test['setup'](scene_name)
|
||||
except Exception as e:
|
||||
print(f" SETUP ERROR: {e}")
|
||||
|
||||
mcrfpy.setScene(scene_name)
|
||||
self.frames_counted = 0
|
||||
|
||||
def end_current_test(self):
|
||||
if self.current_test < 0:
|
||||
return
|
||||
|
||||
test = self.tests[self.current_test]
|
||||
try:
|
||||
filename = mcrfpy.end_benchmark()
|
||||
|
||||
with open(filename, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
frames = data['frames'][30:] # Skip warmup
|
||||
if frames:
|
||||
avg_work = sum(f['work_time_ms'] for f in frames) / len(frames)
|
||||
avg_frame = sum(f['frame_time_ms'] for f in frames) / len(frames)
|
||||
max_work = max(f['work_time_ms'] for f in frames)
|
||||
|
||||
self.results[test['name']] = {
|
||||
'avg_work_ms': avg_work,
|
||||
'max_work_ms': max_work,
|
||||
'frame_count': len(frames),
|
||||
}
|
||||
print(f" Work: {avg_work:.2f}ms avg, {max_work:.2f}ms max ({len(frames)} frames)")
|
||||
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
new_name = f"{OUTPUT_DIR}/{self.mode}_{test['name']}.json"
|
||||
os.rename(filename, new_name)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
self.results[test['name']] = {'error': str(e)}
|
||||
|
||||
def finish_suite(self):
|
||||
mcrfpy.delTimer("tick")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("STRESS TEST COMPLETE")
|
||||
print("="*50)
|
||||
|
||||
for name, r in self.results.items():
|
||||
if 'error' in r:
|
||||
print(f" {name}: ERROR")
|
||||
else:
|
||||
print(f" {name}: {r['avg_work_ms']:.2f}ms avg")
|
||||
|
||||
# Save summary
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
with open(f"{OUTPUT_DIR}/{self.mode}_summary.json", 'w') as f:
|
||||
json.dump({
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'mode': self.mode,
|
||||
'results': self.results
|
||||
}, f, indent=2)
|
||||
|
||||
print(f"\nResults saved to {OUTPUT_DIR}/")
|
||||
sys.exit(0)
|
||||
|
||||
def start(self):
|
||||
print("="*50)
|
||||
print("McRogueFace Stress Test Suite")
|
||||
print("="*50)
|
||||
print(f"Tests: {len(self.tests)}, Duration: {TEST_DURATION_MS}ms each")
|
||||
|
||||
mcrfpy.createScene("init")
|
||||
ui = mcrfpy.sceneUI("init")
|
||||
ui.append(mcrfpy.Frame(pos=(0,0), size=(10,10))) # Required for timer to fire
|
||||
mcrfpy.setScene("init")
|
||||
mcrfpy.setTimer("tick", self.tick, TIMER_INTERVAL_MS)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TEST SETUP FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def test_many_frames(scene_name):
|
||||
"""1000 Frame elements"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
for i in range(1000):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=((i % 32) * 32, (i // 32) * 24),
|
||||
size=(30, 22),
|
||||
fill_color=mcrfpy.Color((i*7)%256, (i*13)%256, (i*17)%256)
|
||||
)
|
||||
ui.append(frame)
|
||||
mcrfpy.log_benchmark("1000 frames created")
|
||||
|
||||
def test_many_sprites(scene_name):
|
||||
"""500 Sprite elements"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
for i in range(500):
|
||||
sprite = mcrfpy.Sprite(
|
||||
pos=((i % 20) * 48 + 10, (i // 20) * 28 + 10),
|
||||
texture=texture,
|
||||
sprite_index=i % 128
|
||||
)
|
||||
sprite.scale_x = 2.0
|
||||
sprite.scale_y = 2.0
|
||||
ui.append(sprite)
|
||||
mcrfpy.log_benchmark("500 sprites created")
|
||||
|
||||
def test_many_captions(scene_name):
|
||||
"""500 Caption elements"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
for i in range(500):
|
||||
caption = mcrfpy.Caption(
|
||||
text=f"Text #{i}",
|
||||
pos=((i % 20) * 50 + 5, (i // 20) * 28 + 5)
|
||||
)
|
||||
ui.append(caption)
|
||||
mcrfpy.log_benchmark("500 captions created")
|
||||
|
||||
def test_deep_nesting(scene_name):
|
||||
"""15-level nested frames"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
current = ui
|
||||
for level in range(15):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(20, 20),
|
||||
size=(1024 - level * 60, 768 - level * 45),
|
||||
fill_color=mcrfpy.Color((level * 17) % 256, 100, (255 - level * 17) % 256, 200)
|
||||
)
|
||||
current.append(frame)
|
||||
# Add children at each level
|
||||
for j in range(3):
|
||||
child = mcrfpy.Frame(pos=(50 + j * 80, 50), size=(60, 30))
|
||||
frame.children.append(child)
|
||||
current = frame.children
|
||||
mcrfpy.log_benchmark("15-level nesting created")
|
||||
|
||||
def test_large_grid(scene_name):
|
||||
"""100x100 grid with 500 entities"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
grid = mcrfpy.Grid(pos=(50, 50), size=(900, 650), grid_size=(100, 100), texture=texture)
|
||||
ui.append(grid)
|
||||
|
||||
for y in range(100):
|
||||
for x in range(100):
|
||||
cell = grid.at(x, y)
|
||||
cell.tilesprite = (x + y) % 64
|
||||
|
||||
for i in range(500):
|
||||
entity = mcrfpy.Entity(
|
||||
grid_pos=((i * 7) % 100, (i * 11) % 100),
|
||||
texture=texture,
|
||||
sprite_index=(i * 3) % 128,
|
||||
grid=grid
|
||||
)
|
||||
mcrfpy.log_benchmark("100x100 grid with 500 entities created")
|
||||
|
||||
def test_animation_stress(scene_name):
|
||||
"""100 frames with 200 animations"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
for i in range(100):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=((i % 10) * 100 + 10, (i // 10) * 70 + 10),
|
||||
size=(80, 50),
|
||||
fill_color=mcrfpy.Color(100, 150, 200)
|
||||
)
|
||||
ui.append(frame)
|
||||
|
||||
# Two animations per frame
|
||||
anim_x = mcrfpy.Animation("x", float((i % 10) * 100 + 50), 1.5, "easeInOut")
|
||||
anim_x.start(frame)
|
||||
anim_o = mcrfpy.Animation("fill_color.a", 128 + (i % 128), 2.0, "linear")
|
||||
anim_o.start(frame)
|
||||
mcrfpy.log_benchmark("100 frames with 200 animations")
|
||||
|
||||
def test_static_scene(scene_name):
|
||||
"""Static game scene (ideal for caching)"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 50))
|
||||
ui.append(bg)
|
||||
|
||||
# UI panel
|
||||
panel = mcrfpy.Frame(pos=(10, 10), size=(200, 300), fill_color=mcrfpy.Color(50, 50, 70))
|
||||
ui.append(panel)
|
||||
for i in range(10):
|
||||
caption = mcrfpy.Caption(text=f"Status {i}", pos=(10, 10 + i * 25))
|
||||
panel.children.append(caption)
|
||||
|
||||
# Grid
|
||||
grid = mcrfpy.Grid(pos=(220, 10), size=(790, 600), grid_size=(40, 30), texture=texture)
|
||||
ui.append(grid)
|
||||
for y in range(30):
|
||||
for x in range(40):
|
||||
grid.at(x, y).tilesprite = ((x + y) % 4) + 1
|
||||
|
||||
for i in range(20):
|
||||
entity = mcrfpy.Entity(grid_pos=((i*2) % 40, (i*3) % 30),
|
||||
texture=texture, sprite_index=64 + i % 16, grid=grid)
|
||||
mcrfpy.log_benchmark("Static game scene created")
|
||||
|
||||
|
||||
def test_static_scene_cached(scene_name):
|
||||
"""Static game scene with cache_subtree enabled (#144)"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Background with caching enabled
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(30, 30, 50), cache_subtree=True)
|
||||
ui.append(bg)
|
||||
|
||||
# UI panel with caching enabled
|
||||
panel = mcrfpy.Frame(pos=(10, 10), size=(200, 300), fill_color=mcrfpy.Color(50, 50, 70), cache_subtree=True)
|
||||
ui.append(panel)
|
||||
for i in range(10):
|
||||
caption = mcrfpy.Caption(text=f"Status {i}", pos=(10, 10 + i * 25))
|
||||
panel.children.append(caption)
|
||||
|
||||
# Grid (not cached - grids handle their own optimization)
|
||||
grid = mcrfpy.Grid(pos=(220, 10), size=(790, 600), grid_size=(40, 30), texture=texture)
|
||||
ui.append(grid)
|
||||
for y in range(30):
|
||||
for x in range(40):
|
||||
grid.at(x, y).tilesprite = ((x + y) % 4) + 1
|
||||
|
||||
for i in range(20):
|
||||
entity = mcrfpy.Entity(grid_pos=((i*2) % 40, (i*3) % 30),
|
||||
texture=texture, sprite_index=64 + i % 16, grid=grid)
|
||||
mcrfpy.log_benchmark("Static game scene with cache_subtree created")
|
||||
|
||||
|
||||
def test_deep_nesting_cached(scene_name):
|
||||
"""15-level nested frames with cache_subtree on outer frame (#144)"""
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
|
||||
# Outer frame with caching - entire subtree cached
|
||||
outer = mcrfpy.Frame(
|
||||
pos=(0, 0),
|
||||
size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(0, 100, 255, 200),
|
||||
cache_subtree=True # Cache entire nested hierarchy
|
||||
)
|
||||
ui.append(outer)
|
||||
|
||||
current = outer.children
|
||||
for level in range(15):
|
||||
frame = mcrfpy.Frame(
|
||||
pos=(20, 20),
|
||||
size=(1024 - level * 60, 768 - level * 45),
|
||||
fill_color=mcrfpy.Color((level * 17) % 256, 100, (255 - level * 17) % 256, 200)
|
||||
)
|
||||
current.append(frame)
|
||||
# Add children at each level
|
||||
for j in range(3):
|
||||
child = mcrfpy.Frame(pos=(50 + j * 80, 50), size=(60, 30))
|
||||
frame.children.append(child)
|
||||
current = frame.children
|
||||
mcrfpy.log_benchmark("15-level nesting with cache_subtree created")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN
|
||||
# =============================================================================
|
||||
|
||||
runner = StressTestRunner()
|
||||
runner.add_test("many_frames", test_many_frames, "1000 Frame elements")
|
||||
runner.add_test("many_sprites", test_many_sprites, "500 Sprite elements")
|
||||
runner.add_test("many_captions", test_many_captions, "500 Caption elements")
|
||||
runner.add_test("deep_nesting", test_deep_nesting, "15-level nested hierarchy")
|
||||
runner.add_test("deep_nesting_cached", test_deep_nesting_cached, "15-level nested (cache_subtree)")
|
||||
runner.add_test("large_grid", test_large_grid, "100x100 grid, 500 entities")
|
||||
runner.add_test("animation_stress", test_animation_stress, "100 frames, 200 animations")
|
||||
runner.add_test("static_scene", test_static_scene, "Static game scene (no caching)")
|
||||
runner.add_test("static_scene_cached", test_static_scene_cached, "Static game scene (cache_subtree)")
|
||||
runner.start()
|
||||
99
tests/benchmarks/tcod_fov_isolated.py
Normal file
99
tests/benchmarks/tcod_fov_isolated.py
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Isolated FOV benchmark - test if the slowdown is TCOD or Python wrapper
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import time
|
||||
|
||||
def run_test(runtime):
|
||||
print("=" * 60)
|
||||
print("FOV Isolation Test - Is TCOD slow, or is it the Python wrapper?")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a 1000x1000 grid
|
||||
mcrfpy.createScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
print("\nCreating 1000x1000 grid...")
|
||||
t0 = time.perf_counter()
|
||||
grid = mcrfpy.Grid(pos=(0,0), size=(800,600), grid_size=(1000, 1000), texture=texture)
|
||||
ui.append(grid)
|
||||
print(f" Grid creation: {(time.perf_counter() - t0)*1000:.1f}ms")
|
||||
|
||||
# Set walkability
|
||||
print("Setting walkability (this takes a while)...")
|
||||
t0 = time.perf_counter()
|
||||
for y in range(0, 1000, 10): # Sample every 10th row for speed
|
||||
for x in range(1000):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
print(f" Partial walkability: {(time.perf_counter() - t0)*1000:.1f}ms")
|
||||
|
||||
# Test 1: compute_fov (now returns None - fast path after #146 fix)
|
||||
print("\n--- Test 1: grid.compute_fov() [returns None after #146 fix] ---")
|
||||
times = []
|
||||
for i in range(5):
|
||||
t0 = time.perf_counter()
|
||||
result = grid.compute_fov(500, 500, radius=15)
|
||||
elapsed = (time.perf_counter() - t0) * 1000
|
||||
times.append(elapsed)
|
||||
# Count visible cells using is_in_fov (the correct pattern)
|
||||
visible = sum(1 for dy in range(-15, 16) for dx in range(-15, 16)
|
||||
if 0 <= 500+dx < 1000 and 0 <= 500+dy < 1000
|
||||
and grid.is_in_fov(500+dx, 500+dy))
|
||||
print(f" Run {i+1}: {elapsed:.3f}ms, result={result}, ~{visible} visible cells")
|
||||
print(f" Average: {sum(times)/len(times):.3f}ms")
|
||||
|
||||
# Test 2: Just check is_in_fov for cells in radius (what rendering would do)
|
||||
print("\n--- Test 2: Simulated render check (only radius cells) ---")
|
||||
times = []
|
||||
for i in range(5):
|
||||
# First compute FOV (we need to do this)
|
||||
grid.compute_fov(500, 500, radius=15)
|
||||
|
||||
# Now simulate what rendering would do - check only nearby cells
|
||||
t0 = time.perf_counter()
|
||||
visible_count = 0
|
||||
for dy in range(-15, 16):
|
||||
for dx in range(-15, 16):
|
||||
x, y = 500 + dx, 500 + dy
|
||||
if 0 <= x < 1000 and 0 <= y < 1000:
|
||||
if grid.is_in_fov(x, y):
|
||||
visible_count += 1
|
||||
elapsed = (time.perf_counter() - t0) * 1000
|
||||
times.append(elapsed)
|
||||
print(f" Run {i+1}: {elapsed:.2f}ms checking ~961 cells, {visible_count} visible")
|
||||
print(f" Average: {sum(times)/len(times):.2f}ms")
|
||||
|
||||
# Test 3: Time just the iteration overhead (no FOV, just grid access)
|
||||
print("\n--- Test 3: Grid iteration baseline (no FOV) ---")
|
||||
times = []
|
||||
for i in range(5):
|
||||
t0 = time.perf_counter()
|
||||
count = 0
|
||||
for dy in range(-15, 16):
|
||||
for dx in range(-15, 16):
|
||||
x, y = 500 + dx, 500 + dy
|
||||
if 0 <= x < 1000 and 0 <= y < 1000:
|
||||
cell = grid.at(x, y)
|
||||
if cell.walkable:
|
||||
count += 1
|
||||
elapsed = (time.perf_counter() - t0) * 1000
|
||||
times.append(elapsed)
|
||||
print(f" Average: {sum(times)/len(times):.2f}ms for ~961 grid.at() calls")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("CONCLUSION:")
|
||||
print("After #146 fix, compute_fov() returns None instead of building")
|
||||
print("a list. Test 1 and Test 2 should now have similar performance.")
|
||||
print("The TCOD FOV algorithm is O(radius²) and fast.")
|
||||
print("=" * 60)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.createScene("init")
|
||||
mcrfpy.setScene("init")
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
||||
140
tests/benchmarks/tcod_scale_test.py
Normal file
140
tests/benchmarks/tcod_scale_test.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
TCOD Scaling Benchmark - Test pathfinding/FOV on large grids
|
||||
|
||||
Tests whether TCOD operations scale acceptably on 1000x1000 grids,
|
||||
to determine if TCOD data needs chunking or can stay as single logical grid.
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Grid sizes to test
|
||||
SIZES = [(100, 100), (250, 250), (500, 500), (1000, 1000)]
|
||||
ITERATIONS = 10
|
||||
|
||||
def benchmark_grid_size(grid_x, grid_y):
|
||||
"""Benchmark TCOD operations for a given grid size"""
|
||||
results = {}
|
||||
|
||||
# Create scene and grid
|
||||
scene_name = f"bench_{grid_x}x{grid_y}"
|
||||
mcrfpy.createScene(scene_name)
|
||||
ui = mcrfpy.sceneUI(scene_name)
|
||||
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Time grid creation
|
||||
t0 = time.perf_counter()
|
||||
grid = mcrfpy.Grid(
|
||||
pos=(0, 0),
|
||||
size=(800, 600),
|
||||
grid_size=(grid_x, grid_y),
|
||||
texture=texture
|
||||
)
|
||||
ui.append(grid)
|
||||
results['create_ms'] = (time.perf_counter() - t0) * 1000
|
||||
|
||||
# Set up some walkability (maze-like pattern)
|
||||
t0 = time.perf_counter()
|
||||
for y in range(grid_y):
|
||||
for x in range(grid_x):
|
||||
cell = grid.at(x, y)
|
||||
# Create a simple maze: every 3rd cell is a wall
|
||||
cell.walkable = not ((x % 3 == 0) and (y % 3 == 0))
|
||||
cell.transparent = cell.walkable
|
||||
results['setup_walkability_ms'] = (time.perf_counter() - t0) * 1000
|
||||
|
||||
# Add an entity for FOV perspective
|
||||
entity = mcrfpy.Entity(
|
||||
grid_pos=(grid_x // 2, grid_y // 2),
|
||||
texture=texture,
|
||||
sprite_index=64,
|
||||
grid=grid
|
||||
)
|
||||
|
||||
# Benchmark FOV computation
|
||||
fov_times = []
|
||||
for i in range(ITERATIONS):
|
||||
# Move entity to different positions
|
||||
ex, ey = (i * 7) % (grid_x - 20) + 10, (i * 11) % (grid_y - 20) + 10
|
||||
t0 = time.perf_counter()
|
||||
grid.compute_fov(ex, ey, radius=15)
|
||||
fov_times.append((time.perf_counter() - t0) * 1000)
|
||||
results['fov_avg_ms'] = sum(fov_times) / len(fov_times)
|
||||
results['fov_max_ms'] = max(fov_times)
|
||||
|
||||
# Benchmark A* pathfinding (corner to corner)
|
||||
path_times = []
|
||||
for i in range(ITERATIONS):
|
||||
# Path from near origin to near opposite corner
|
||||
x1, y1 = 1, 1
|
||||
x2, y2 = grid_x - 2, grid_y - 2
|
||||
t0 = time.perf_counter()
|
||||
path = grid.compute_astar_path(x1, y1, x2, y2)
|
||||
path_times.append((time.perf_counter() - t0) * 1000)
|
||||
results['astar_avg_ms'] = sum(path_times) / len(path_times)
|
||||
results['astar_max_ms'] = max(path_times)
|
||||
results['astar_path_len'] = len(path) if path else 0
|
||||
|
||||
# Benchmark Dijkstra (full map distance calculation)
|
||||
dijkstra_times = []
|
||||
for i in range(ITERATIONS):
|
||||
cx, cy = grid_x // 2, grid_y // 2
|
||||
t0 = time.perf_counter()
|
||||
grid.compute_dijkstra(cx, cy)
|
||||
dijkstra_times.append((time.perf_counter() - t0) * 1000)
|
||||
results['dijkstra_avg_ms'] = sum(dijkstra_times) / len(dijkstra_times)
|
||||
results['dijkstra_max_ms'] = max(dijkstra_times)
|
||||
|
||||
return results
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("TCOD Scaling Benchmark")
|
||||
print("=" * 60)
|
||||
print(f"Testing grid sizes: {SIZES}")
|
||||
print(f"Iterations per test: {ITERATIONS}")
|
||||
print()
|
||||
|
||||
all_results = {}
|
||||
|
||||
for grid_x, grid_y in SIZES:
|
||||
print(f"\n--- Grid {grid_x}x{grid_y} ({grid_x * grid_y:,} cells) ---")
|
||||
try:
|
||||
results = benchmark_grid_size(grid_x, grid_y)
|
||||
all_results[f"{grid_x}x{grid_y}"] = results
|
||||
|
||||
print(f" Creation: {results['create_ms']:.2f}ms")
|
||||
print(f" Walkability: {results['setup_walkability_ms']:.2f}ms")
|
||||
print(f" FOV (r=15): {results['fov_avg_ms']:.3f}ms avg, {results['fov_max_ms']:.3f}ms max")
|
||||
print(f" A* path: {results['astar_avg_ms']:.2f}ms avg, {results['astar_max_ms']:.2f}ms max (len={results['astar_path_len']})")
|
||||
print(f" Dijkstra: {results['dijkstra_avg_ms']:.2f}ms avg, {results['dijkstra_max_ms']:.2f}ms max")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ERROR: {e}")
|
||||
all_results[f"{grid_x}x{grid_y}"] = {'error': str(e)}
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("SUMMARY - Per-frame budget analysis (targeting 16ms for 60fps)")
|
||||
print("=" * 60)
|
||||
|
||||
for size, results in all_results.items():
|
||||
if 'error' in results:
|
||||
print(f" {size}: ERROR")
|
||||
else:
|
||||
total_logic = results['fov_avg_ms'] + results['astar_avg_ms']
|
||||
print(f" {size}: FOV+A* = {total_logic:.2f}ms ({total_logic/16*100:.0f}% of frame budget)")
|
||||
|
||||
print("\nDone.")
|
||||
sys.exit(0)
|
||||
|
||||
# Run immediately (no timer needed for this test)
|
||||
mcrfpy.createScene("init")
|
||||
mcrfpy.setScene("init")
|
||||
|
||||
# Use a timer to let the engine initialize
|
||||
def run_benchmark(runtime):
|
||||
main()
|
||||
|
||||
mcrfpy.setTimer("bench", run_benchmark, 100)
|
||||
187
tests/regression/issue_123_chunk_system_test.py
Normal file
187
tests/regression/issue_123_chunk_system_test.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Issue #123 Regression Test: Grid Sub-grid Chunk System
|
||||
|
||||
Tests that large grids (>64 cells) use chunk-based storage and rendering,
|
||||
while small grids use the original flat storage. Verifies that:
|
||||
1. Small grids work as before (no regression)
|
||||
2. Large grids work correctly with chunks
|
||||
3. Cell access (read/write) works for both modes
|
||||
4. Rendering displays correctly for both modes
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_small_grid():
|
||||
"""Test that small grids work (original flat storage)"""
|
||||
print("Testing small grid (50x50 < 64 threshold)...")
|
||||
|
||||
# Small grid should use flat storage
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(10, 10), size=(400, 400))
|
||||
|
||||
# Set some cells
|
||||
for y in range(50):
|
||||
for x in range(50):
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color((x * 5) % 256, (y * 5) % 256, 128, 255)
|
||||
cell.tilesprite = -1
|
||||
|
||||
# Verify cells
|
||||
cell = grid.at(25, 25)
|
||||
expected_r = (25 * 5) % 256
|
||||
expected_g = (25 * 5) % 256
|
||||
color = cell.color
|
||||
r, g = color[0], color[1]
|
||||
if r != expected_r or g != expected_g:
|
||||
print(f"FAIL: Small grid cell color mismatch. Expected ({expected_r}, {expected_g}), got ({r}, {g})")
|
||||
return False
|
||||
|
||||
print(" Small grid: PASS")
|
||||
return True
|
||||
|
||||
def test_large_grid():
|
||||
"""Test that large grids work (chunk-based storage)"""
|
||||
print("Testing large grid (100x100 > 64 threshold)...")
|
||||
|
||||
# Large grid should use chunk storage (100 > 64)
|
||||
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
|
||||
|
||||
# Set cells across multiple chunks
|
||||
# Chunks are 64x64, so a 100x100 grid has 2x2 = 4 chunks
|
||||
test_points = [
|
||||
(0, 0), # Chunk (0,0)
|
||||
(63, 63), # Chunk (0,0) - edge
|
||||
(64, 0), # Chunk (1,0) - start
|
||||
(64, 64), # Chunk (1,1) - start
|
||||
(99, 99), # Chunk (1,1) - edge
|
||||
(50, 50), # Chunk (0,0)
|
||||
(70, 80), # Chunk (1,1)
|
||||
]
|
||||
|
||||
for x, y in test_points:
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color(x, y, 100, 255)
|
||||
cell.tilesprite = -1
|
||||
|
||||
# Verify cells
|
||||
for x, y in test_points:
|
||||
cell = grid.at(x, y)
|
||||
color = cell.color
|
||||
if color[0] != x or color[1] != y:
|
||||
print(f"FAIL: Large grid cell ({x},{y}) color mismatch. Expected ({x}, {y}), got ({color[0]}, {color[1]})")
|
||||
return False
|
||||
|
||||
print(" Large grid cell access: PASS")
|
||||
return True
|
||||
|
||||
def test_very_large_grid():
|
||||
"""Test very large grid (500x500)"""
|
||||
print("Testing very large grid (500x500)...")
|
||||
|
||||
# 500x500 = 250,000 cells, should use ~64 chunks (8x8)
|
||||
grid = mcrfpy.Grid(grid_size=(500, 500), pos=(10, 10), size=(400, 400))
|
||||
|
||||
# Set some cells at various positions
|
||||
test_points = [
|
||||
(0, 0),
|
||||
(127, 127),
|
||||
(128, 128),
|
||||
(255, 255),
|
||||
(256, 256),
|
||||
(400, 400),
|
||||
(499, 499),
|
||||
]
|
||||
|
||||
for x, y in test_points:
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color(x % 256, y % 256, 200, 255)
|
||||
|
||||
# Verify
|
||||
for x, y in test_points:
|
||||
cell = grid.at(x, y)
|
||||
color = cell.color
|
||||
if color[0] != (x % 256) or color[1] != (y % 256):
|
||||
print(f"FAIL: Very large grid cell ({x},{y}) color mismatch")
|
||||
return False
|
||||
|
||||
print(" Very large grid: PASS")
|
||||
return True
|
||||
|
||||
def test_boundary_case():
|
||||
"""Test the exact boundary (64x64 should NOT use chunks, 65x65 should)"""
|
||||
print("Testing boundary cases...")
|
||||
|
||||
# 64x64 should use flat storage (not exceeding threshold)
|
||||
grid_64 = mcrfpy.Grid(grid_size=(64, 64), pos=(10, 10), size=(400, 400))
|
||||
cell = grid_64.at(63, 63)
|
||||
cell.color = mcrfpy.Color(255, 0, 0, 255)
|
||||
color = grid_64.at(63, 63).color
|
||||
if color[0] != 255:
|
||||
print(f"FAIL: 64x64 grid boundary cell not set correctly, got r={color[0]}")
|
||||
return False
|
||||
|
||||
# 65x65 should use chunk storage (exceeding threshold)
|
||||
grid_65 = mcrfpy.Grid(grid_size=(65, 65), pos=(10, 10), size=(400, 400))
|
||||
cell = grid_65.at(64, 64)
|
||||
cell.color = mcrfpy.Color(0, 255, 0, 255)
|
||||
color = grid_65.at(64, 64).color
|
||||
if color[1] != 255:
|
||||
print(f"FAIL: 65x65 grid cell not set correctly, got g={color[1]}")
|
||||
return False
|
||||
|
||||
print(" Boundary cases: PASS")
|
||||
return True
|
||||
|
||||
def test_edge_cases():
|
||||
"""Test edge cell access in chunked grid"""
|
||||
print("Testing edge cases...")
|
||||
|
||||
# Create 100x100 grid
|
||||
grid = mcrfpy.Grid(grid_size=(100, 100), pos=(10, 10), size=(400, 400))
|
||||
|
||||
# Test all corners
|
||||
corners = [(0, 0), (99, 0), (0, 99), (99, 99)]
|
||||
for i, (x, y) in enumerate(corners):
|
||||
cell = grid.at(x, y)
|
||||
cell.color = mcrfpy.Color(i * 60, i * 60, i * 60, 255)
|
||||
|
||||
for i, (x, y) in enumerate(corners):
|
||||
cell = grid.at(x, y)
|
||||
expected = i * 60
|
||||
color = cell.color
|
||||
if color[0] != expected:
|
||||
print(f"FAIL: Corner ({x},{y}) color mismatch, expected {expected}, got {color[0]}")
|
||||
return False
|
||||
|
||||
print(" Edge cases: PASS")
|
||||
return True
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback to run tests after scene is active"""
|
||||
results = []
|
||||
|
||||
results.append(test_small_grid())
|
||||
results.append(test_large_grid())
|
||||
results.append(test_very_large_grid())
|
||||
results.append(test_boundary_case())
|
||||
results.append(test_edge_cases())
|
||||
|
||||
if all(results):
|
||||
print("\n=== ALL TESTS PASSED ===")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\n=== SOME TESTS FAILED ===")
|
||||
sys.exit(1)
|
||||
|
||||
# Main
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Issue #123: Grid Sub-grid Chunk System Test")
|
||||
print("=" * 60)
|
||||
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# Run tests after scene is active
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
||||
114
tests/regression/issue_146_fov_returns_none.py
Normal file
114
tests/regression/issue_146_fov_returns_none.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regression test for issue #146: compute_fov() returns None
|
||||
|
||||
The compute_fov() method had O(n²) performance because it built a Python list
|
||||
of all visible cells by iterating the entire grid. The fix removes this
|
||||
list-building and returns None instead. Users should use is_in_fov() to query
|
||||
visibility.
|
||||
|
||||
Bug: 15.76ms for compute_fov() on 1000x1000 grid (iterating 1M cells)
|
||||
Fix: Return None, actual FOV check via is_in_fov() takes 0.21ms
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import time
|
||||
|
||||
def run_test(runtime):
|
||||
print("=" * 60)
|
||||
print("Issue #146 Regression Test: compute_fov() returns None")
|
||||
print("=" * 60)
|
||||
|
||||
# Create a test grid
|
||||
mcrfpy.createScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
grid = mcrfpy.Grid(pos=(0,0), size=(400,300), grid_size=(50, 50), texture=texture)
|
||||
ui.append(grid)
|
||||
|
||||
# Set walkability for center area
|
||||
for y in range(50):
|
||||
for x in range(50):
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
# Add some walls to test blocking
|
||||
for i in range(10, 20):
|
||||
grid.at(i, 25).transparent = False
|
||||
grid.at(i, 25).walkable = False
|
||||
|
||||
print("\n--- Test 1: compute_fov() returns None ---")
|
||||
result = grid.compute_fov(25, 25, radius=10)
|
||||
if result is None:
|
||||
print(" PASS: compute_fov() returned None")
|
||||
else:
|
||||
print(f" FAIL: compute_fov() returned {type(result).__name__} instead of None")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n--- Test 2: is_in_fov() works after compute_fov() ---")
|
||||
# Center should be visible
|
||||
if grid.is_in_fov(25, 25):
|
||||
print(" PASS: Center (25,25) is in FOV")
|
||||
else:
|
||||
print(" FAIL: Center should be in FOV")
|
||||
sys.exit(1)
|
||||
|
||||
# Cell within radius should be visible
|
||||
if grid.is_in_fov(20, 25):
|
||||
print(" PASS: Cell (20,25) within radius is in FOV")
|
||||
else:
|
||||
print(" FAIL: Cell (20,25) should be in FOV")
|
||||
sys.exit(1)
|
||||
|
||||
# Cell behind wall should NOT be visible
|
||||
if not grid.is_in_fov(15, 30):
|
||||
print(" PASS: Cell (15,30) behind wall is NOT in FOV")
|
||||
else:
|
||||
print(" FAIL: Cell behind wall should not be in FOV")
|
||||
sys.exit(1)
|
||||
|
||||
# Cell outside radius should NOT be visible
|
||||
if not grid.is_in_fov(0, 0):
|
||||
print(" PASS: Cell (0,0) outside radius is NOT in FOV")
|
||||
else:
|
||||
print(" FAIL: Cell outside radius should not be in FOV")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n--- Test 3: Performance sanity check ---")
|
||||
# Create larger grid for timing
|
||||
grid_large = mcrfpy.Grid(pos=(0,0), size=(400,300), grid_size=(200, 200), texture=texture)
|
||||
for y in range(0, 200, 5): # Sample for speed
|
||||
for x in range(200):
|
||||
cell = grid_large.at(x, y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
# Time compute_fov (should be fast now - no list building)
|
||||
times = []
|
||||
for i in range(5):
|
||||
t0 = time.perf_counter()
|
||||
grid_large.compute_fov(100, 100, radius=15)
|
||||
elapsed = (time.perf_counter() - t0) * 1000
|
||||
times.append(elapsed)
|
||||
|
||||
avg_time = sum(times) / len(times)
|
||||
print(f" compute_fov() on 200x200 grid: {avg_time:.3f}ms avg")
|
||||
|
||||
# Should be under 1ms without list building (was ~4ms with list on 200x200)
|
||||
if avg_time < 2.0:
|
||||
print(f" PASS: compute_fov() is fast (<2ms)")
|
||||
else:
|
||||
print(f" WARNING: compute_fov() took {avg_time:.3f}ms (expected <2ms)")
|
||||
# Not a hard failure, just a warning
|
||||
|
||||
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)
|
||||
193
tests/regression/issue_147_grid_layers.py
Normal file
193
tests/regression/issue_147_grid_layers.py
Normal 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 with explicit empty layers (#150 migration)
|
||||
grid = mcrfpy.Grid(pos=(50, 50), size=(400, 300), grid_size=(20, 15), texture=texture, layers={})
|
||||
ui.append(grid)
|
||||
|
||||
print("\n--- Test 1: Initial state (no layers) ---")
|
||||
if len(grid.layers) == 0:
|
||||
print(" PASS: Grid starts with no layers (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)
|
||||
157
tests/regression/issue_148_layer_dirty_flags.py
Normal file
157
tests/regression/issue_148_layer_dirty_flags.py
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Regression test for issue #148: Grid Layer Dirty Flags and RenderTexture Caching
|
||||
|
||||
Tests:
|
||||
1. Dirty flag is initially set (layers start dirty)
|
||||
2. Setting cell values marks layer dirty
|
||||
3. Fill operation marks layer dirty
|
||||
4. Texture change marks TileLayer dirty
|
||||
5. Viewport changes (center/zoom) don't trigger re-render (static benchmark)
|
||||
6. Performance improvement for static layers
|
||||
"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
import time
|
||||
|
||||
def run_test(runtime):
|
||||
print("=" * 60)
|
||||
print("Issue #148 Regression Test: Layer Dirty Flags and Caching")
|
||||
print("=" * 60)
|
||||
|
||||
# Create test scene
|
||||
mcrfpy.createScene("test")
|
||||
ui = mcrfpy.sceneUI("test")
|
||||
texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
|
||||
# Create grid with larger size for performance testing
|
||||
grid = mcrfpy.Grid(pos=(50, 50), size=(500, 400), grid_size=(50, 40), texture=texture)
|
||||
ui.append(grid)
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
print("\n--- Test 1: Layer creation (starts dirty) ---")
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
# The layer should be dirty initially
|
||||
# We can't directly check dirty flag from Python, but we verify the system works
|
||||
print(" ColorLayer created successfully")
|
||||
|
||||
tile_layer = grid.add_layer("tile", z_index=-2, texture=texture)
|
||||
print(" TileLayer created successfully")
|
||||
print(" PASS: Layers created")
|
||||
|
||||
print("\n--- Test 2: Fill operations work ---")
|
||||
# Fill with some data
|
||||
color_layer.fill(mcrfpy.Color(128, 0, 128, 64))
|
||||
print(" ColorLayer filled with purple overlay")
|
||||
|
||||
tile_layer.fill(5) # Fill with tile index 5
|
||||
print(" TileLayer filled with tile index 5")
|
||||
print(" PASS: Fill operations completed")
|
||||
|
||||
print("\n--- Test 3: Cell set operations work ---")
|
||||
# Set individual cells
|
||||
color_layer.set(10, 10, mcrfpy.Color(255, 255, 0, 128))
|
||||
color_layer.set(11, 10, mcrfpy.Color(255, 255, 0, 128))
|
||||
color_layer.set(10, 11, mcrfpy.Color(255, 255, 0, 128))
|
||||
color_layer.set(11, 11, mcrfpy.Color(255, 255, 0, 128))
|
||||
print(" Set 4 cells in ColorLayer to yellow")
|
||||
|
||||
tile_layer.set(15, 15, 10)
|
||||
tile_layer.set(16, 15, 11)
|
||||
tile_layer.set(15, 16, 10)
|
||||
tile_layer.set(16, 16, 11)
|
||||
print(" Set 4 cells in TileLayer to different tiles")
|
||||
print(" PASS: Cell set operations completed")
|
||||
|
||||
print("\n--- Test 4: Texture change on TileLayer ---")
|
||||
# Create a second texture and assign it
|
||||
texture2 = mcrfpy.Texture("assets/kenney_ice.png", 16, 16)
|
||||
tile_layer.texture = texture2
|
||||
print(" Changed TileLayer texture")
|
||||
|
||||
# Set back to original
|
||||
tile_layer.texture = texture
|
||||
print(" Restored original texture")
|
||||
print(" PASS: Texture changes work")
|
||||
|
||||
print("\n--- Test 5: Viewport changes (should use cached texture) ---")
|
||||
# Pan around - these should NOT cause layer re-renders (just blit different region)
|
||||
original_center = grid.center
|
||||
print(f" Original center: {original_center}")
|
||||
|
||||
# Perform multiple viewport changes
|
||||
for i in range(10):
|
||||
grid.center = (100 + i * 20, 80 + i * 10)
|
||||
print(" Performed 10 center changes")
|
||||
|
||||
# Zoom changes
|
||||
original_zoom = grid.zoom
|
||||
for z in [1.0, 0.8, 1.2, 0.5, 1.5, 1.0]:
|
||||
grid.zoom = z
|
||||
print(" Performed 6 zoom changes")
|
||||
|
||||
# Restore
|
||||
grid.center = original_center
|
||||
grid.zoom = original_zoom
|
||||
print(" PASS: Viewport changes completed without crashing")
|
||||
|
||||
print("\n--- Test 6: Performance benchmark ---")
|
||||
# Create a large layer for performance testing
|
||||
perf_grid = mcrfpy.Grid(pos=(50, 50), size=(600, 500), grid_size=(100, 80), texture=texture)
|
||||
ui.append(perf_grid)
|
||||
perf_layer = perf_grid.add_layer("tile", z_index=-1, texture=texture)
|
||||
|
||||
# Fill with data
|
||||
perf_layer.fill(1)
|
||||
|
||||
# First render will be slow (cache miss)
|
||||
start = time.time()
|
||||
mcrfpy.setScene("test") # Force render
|
||||
first_render = time.time() - start
|
||||
print(f" First render (cache build): {first_render*1000:.2f}ms")
|
||||
|
||||
# Subsequent viewport changes should be fast (cache hit)
|
||||
# We simulate by changing center multiple times
|
||||
start = time.time()
|
||||
for i in range(5):
|
||||
perf_grid.center = (200 + i * 10, 160 + i * 8)
|
||||
viewport_changes = time.time() - start
|
||||
print(f" 5 viewport changes: {viewport_changes*1000:.2f}ms")
|
||||
|
||||
print(" PASS: Performance benchmark completed")
|
||||
|
||||
print("\n--- Test 7: Layer visibility toggle ---")
|
||||
color_layer.visible = False
|
||||
print(" ColorLayer hidden")
|
||||
color_layer.visible = True
|
||||
print(" ColorLayer shown")
|
||||
print(" PASS: Visibility toggle works")
|
||||
|
||||
print("\n--- Test 8: Large grid stress test ---")
|
||||
# Test with maximum size grid to ensure texture caching works
|
||||
stress_grid = mcrfpy.Grid(pos=(10, 10), size=(200, 150), grid_size=(200, 150), texture=texture)
|
||||
ui.append(stress_grid)
|
||||
stress_layer = stress_grid.add_layer("color", z_index=-1)
|
||||
|
||||
# This would be 30,000 cells - should handle via caching
|
||||
stress_layer.fill(mcrfpy.Color(0, 100, 200, 100))
|
||||
|
||||
# Set a few specific cells
|
||||
for x in range(10):
|
||||
for y in range(10):
|
||||
stress_layer.set(x, y, mcrfpy.Color(255, 0, 0, 200))
|
||||
|
||||
print(" Created 200x150 grid with 30,000 cells")
|
||||
print(" PASS: Large grid handled successfully")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("All tests PASSED")
|
||||
print("=" * 60)
|
||||
print("\nNote: Dirty flag behavior is internal - tests verify API works")
|
||||
print("Actual caching benefits are measured by render performance.")
|
||||
sys.exit(0)
|
||||
|
||||
# Initialize and run
|
||||
mcrfpy.createScene("init")
|
||||
mcrfpy.setScene("init")
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
||||
Loading…
Add table
Add a link
Reference in a new issue