Compare commits
7 Commits
68f8349fe8
...
0545dd4861
| Author | SHA1 | Date |
|---|---|---|
|
|
0545dd4861 | |
|
|
42fcd3417e | |
|
|
a258613faa | |
|
|
9469c04b01 | |
|
|
abb3316ac1 | |
|
|
4b05a95efe | |
|
|
f769c6c5f5 |
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
511
src/UIGrid.cpp
511
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()
|
||||
: 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>>>();
|
||||
|
|
@ -40,9 +41,10 @@ UIGrid::UIGrid()
|
|||
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),
|
||||
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;
|
||||
|
|
@ -84,13 +86,37 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -693,14 +749,16 @@ int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
|||
// 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;
|
||||
}
|
||||
|
||||
|
|
@ -834,6 +892,54 @@ 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;
|
||||
}
|
||||
|
|
@ -1184,40 +1291,9 @@ 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(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}
|
||||
};
|
||||
|
||||
|
|
@ -1421,20 +1705,15 @@ PyMethodDef UIGrid_all_methods[] = {
|
|||
UIDRAWABLE_METHODS,
|
||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
||||
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> List[Tuple[int, int, bool, bool]]\n\n"
|
||||
"Compute field of view from a position and return visible cells.\n\n"
|
||||
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
|
||||
"Compute field of view from a position.\n\n"
|
||||
"Args:\n"
|
||||
" x: X coordinate of the viewer\n"
|
||||
" y: Y coordinate of the viewer\n"
|
||||
" radius: Maximum view distance (0 = unlimited)\n"
|
||||
" light_walls: Whether walls are lit when visible\n"
|
||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
|
||||
"Returns:\n"
|
||||
" List of tuples (x, y, visible, discovered) for all visible cells:\n"
|
||||
" - x, y: Grid coordinates\n"
|
||||
" - visible: True (all returned cells are visible)\n"
|
||||
" - discovered: True (FOV implies discovery)\n\n"
|
||||
"Also updates the internal FOV state for use with is_in_fov()."},
|
||||
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
|
||||
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
|
||||
"is_in_fov(x: int, y: int) -> bool\n\n"
|
||||
"Check if a cell is in the field of view.\n\n"
|
||||
|
|
@ -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)},
|
||||
|
|
|
|||
29
src/UIGrid.h
29
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;
|
||||
|
||||
|
|
@ -148,6 +172,11 @@ public:
|
|||
static int set_on_cell_click(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_hovered_cell(PyUIGridObject* self, void* closure);
|
||||
|
||||
// #147 - Layer system Python API
|
||||
static PyObject* py_add_layer(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* py_remove_layer(PyUIGridObject* self, PyObject* args);
|
||||
static PyObject* get_layers(PyUIGridObject* self, void* closure);
|
||||
static PyObject* py_layer(PyUIGridObject* self, PyObject* args);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
@ -199,3 +154,97 @@ PyObject* UIGridPointState::repr(PyUIGridPointStateObject* self) {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -108,13 +108,13 @@ UITestScene::UITestScene(GameEngine* g) : Scene(g)
|
|||
// 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"
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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…
Reference in New Issue