Phase 1 - FOV Enum System: - Create PyFOV.h/cpp with mcrfpy.FOV IntEnum (BASIC, DIAMOND, SHADOW, etc.) - Add mcrfpy.default_fov module property initialized to FOV.BASIC - Add grid.fov and grid.fov_radius properties for per-grid defaults - Remove deprecated module-level FOV_* constants (breaking change) Phase 2 - Layer Operations: - Implement ColorLayer.fill_rect(pos, size, color) for rectangle fills - Implement TileLayer.fill_rect(pos, size, index) for tile rectangle fills - Implement ColorLayer.draw_fov(source, radius, fov, visible, discovered, unknown) to paint FOV-based visibility on color layers using parent grid's TCOD map The FOV enum uses Python's IntEnum for type safety while maintaining backward compatibility with integer values. Tests updated to use new API. Addresses #114 (FOV enum), #113 (layer operations) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
0545dd4861
commit
018e73590f
|
|
@ -2,6 +2,7 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
#include "PyTexture.h"
|
#include "PyTexture.h"
|
||||||
|
#include "PyFOV.h"
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -11,47 +12,96 @@
|
||||||
GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent)
|
GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent)
|
||||||
: type(type), z_index(z_index), grid_x(grid_x), grid_y(grid_y),
|
: type(type), z_index(z_index), grid_x(grid_x), grid_y(grid_y),
|
||||||
parent_grid(parent), visible(true),
|
parent_grid(parent), visible(true),
|
||||||
dirty(true), texture_initialized(false),
|
chunks_x(0), chunks_y(0),
|
||||||
cached_cell_width(0), cached_cell_height(0)
|
cached_cell_width(0), cached_cell_height(0)
|
||||||
{}
|
{
|
||||||
|
initChunks();
|
||||||
void GridLayer::markDirty() {
|
|
||||||
dirty = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void GridLayer::ensureTextureSize(int cell_width, int cell_height) {
|
void GridLayer::initChunks() {
|
||||||
// Check if we need to resize/create the texture
|
// Calculate chunk dimensions
|
||||||
unsigned int required_width = grid_x * cell_width;
|
chunks_x = (grid_x + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||||
unsigned int required_height = grid_y * cell_height;
|
chunks_y = (grid_y + CHUNK_SIZE - 1) / CHUNK_SIZE;
|
||||||
|
int total_chunks = chunks_x * chunks_y;
|
||||||
|
|
||||||
// Maximum texture size limit (prevent excessive memory usage)
|
// Initialize per-chunk tracking
|
||||||
const unsigned int MAX_TEXTURE_SIZE = 4096;
|
chunk_dirty.assign(total_chunks, true); // All chunks start dirty
|
||||||
if (required_width > MAX_TEXTURE_SIZE) required_width = MAX_TEXTURE_SIZE;
|
chunk_texture_initialized.assign(total_chunks, false);
|
||||||
if (required_height > MAX_TEXTURE_SIZE) required_height = MAX_TEXTURE_SIZE;
|
chunk_textures.clear();
|
||||||
|
chunk_textures.reserve(total_chunks);
|
||||||
|
for (int i = 0; i < total_chunks; ++i) {
|
||||||
|
chunk_textures.push_back(std::make_unique<sf::RenderTexture>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if already properly sized
|
void GridLayer::markDirty() {
|
||||||
if (texture_initialized &&
|
// Mark ALL chunks as dirty
|
||||||
cached_texture.getSize().x == required_width &&
|
std::fill(chunk_dirty.begin(), chunk_dirty.end(), true);
|
||||||
cached_texture.getSize().y == required_height &&
|
}
|
||||||
|
|
||||||
|
void GridLayer::markDirty(int cell_x, int cell_y) {
|
||||||
|
// Mark only the specific chunk containing this cell
|
||||||
|
if (cell_x < 0 || cell_x >= grid_x || cell_y < 0 || cell_y >= grid_y) return;
|
||||||
|
|
||||||
|
int chunk_idx = getChunkIndex(cell_x, cell_y);
|
||||||
|
if (chunk_idx >= 0 && chunk_idx < static_cast<int>(chunk_dirty.size())) {
|
||||||
|
chunk_dirty[chunk_idx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int GridLayer::getChunkIndex(int cell_x, int cell_y) const {
|
||||||
|
int cx = cell_x / CHUNK_SIZE;
|
||||||
|
int cy = cell_y / CHUNK_SIZE;
|
||||||
|
return cy * chunks_x + cx;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridLayer::getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const {
|
||||||
|
chunk_x = cell_x / CHUNK_SIZE;
|
||||||
|
chunk_y = cell_y / CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridLayer::getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const {
|
||||||
|
start_x = chunk_x * CHUNK_SIZE;
|
||||||
|
start_y = chunk_y * CHUNK_SIZE;
|
||||||
|
end_x = std::min(start_x + CHUNK_SIZE, grid_x);
|
||||||
|
end_y = std::min(start_y + CHUNK_SIZE, grid_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void GridLayer::ensureChunkTexture(int chunk_idx, int cell_width, int cell_height) {
|
||||||
|
if (chunk_idx < 0 || chunk_idx >= static_cast<int>(chunk_textures.size())) return;
|
||||||
|
if (!chunk_textures[chunk_idx]) return;
|
||||||
|
|
||||||
|
// Calculate chunk dimensions in cells
|
||||||
|
int cx = chunk_idx % chunks_x;
|
||||||
|
int cy = chunk_idx / chunks_x;
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
int chunk_width_cells = end_x - start_x;
|
||||||
|
int chunk_height_cells = end_y - start_y;
|
||||||
|
|
||||||
|
unsigned int required_width = chunk_width_cells * cell_width;
|
||||||
|
unsigned int required_height = chunk_height_cells * cell_height;
|
||||||
|
|
||||||
|
// Check if texture needs (re)creation
|
||||||
|
if (chunk_texture_initialized[chunk_idx] &&
|
||||||
|
chunk_textures[chunk_idx]->getSize().x == required_width &&
|
||||||
|
chunk_textures[chunk_idx]->getSize().y == required_height &&
|
||||||
cached_cell_width == cell_width &&
|
cached_cell_width == cell_width &&
|
||||||
cached_cell_height == cell_height) {
|
cached_cell_height == cell_height) {
|
||||||
|
return; // Already properly sized
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the texture for this chunk
|
||||||
|
if (!chunk_textures[chunk_idx]->create(required_width, required_height)) {
|
||||||
|
chunk_texture_initialized[chunk_idx] = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or resize the texture (SFML uses .create() not .resize())
|
chunk_texture_initialized[chunk_idx] = true;
|
||||||
if (!cached_texture.create(required_width, required_height)) {
|
chunk_dirty[chunk_idx] = true; // Force re-render after resize
|
||||||
// Creation failed - texture will remain uninitialized
|
|
||||||
texture_initialized = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
cached_cell_width = cell_width;
|
cached_cell_width = cell_width;
|
||||||
cached_cell_height = cell_height;
|
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -73,7 +123,76 @@ const sf::Color& ColorLayer::at(int x, int y) const {
|
||||||
|
|
||||||
void ColorLayer::fill(const sf::Color& color) {
|
void ColorLayer::fill(const sf::Color& color) {
|
||||||
std::fill(colors.begin(), colors.end(), color);
|
std::fill(colors.begin(), colors.end(), color);
|
||||||
markDirty(); // #148 - Mark for re-render
|
markDirty(); // Mark ALL chunks for re-render
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLayer::fillRect(int x, int y, int width, int height, const sf::Color& color) {
|
||||||
|
// Clamp to valid bounds
|
||||||
|
int x1 = std::max(0, x);
|
||||||
|
int y1 = std::max(0, y);
|
||||||
|
int x2 = std::min(grid_x, x + width);
|
||||||
|
int y2 = std::min(grid_y, y + height);
|
||||||
|
|
||||||
|
// Fill the rectangle
|
||||||
|
for (int fy = y1; fy < y2; ++fy) {
|
||||||
|
for (int fx = x1; fx < x2; ++fx) {
|
||||||
|
colors[fy * grid_x + fx] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark affected chunks dirty
|
||||||
|
int chunk_x1 = x1 / CHUNK_SIZE;
|
||||||
|
int chunk_y1 = y1 / CHUNK_SIZE;
|
||||||
|
int chunk_x2 = (x2 - 1) / CHUNK_SIZE;
|
||||||
|
int chunk_y2 = (y2 - 1) / CHUNK_SIZE;
|
||||||
|
|
||||||
|
for (int cy = chunk_y1; cy <= chunk_y2; ++cy) {
|
||||||
|
for (int cx = chunk_x1; cx <= chunk_x2; ++cx) {
|
||||||
|
int idx = cy * chunks_x + cx;
|
||||||
|
if (idx >= 0 && idx < static_cast<int>(chunk_dirty.size())) {
|
||||||
|
chunk_dirty[idx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorLayer::drawFOV(int source_x, int source_y, int radius,
|
||||||
|
TCOD_fov_algorithm_t algorithm,
|
||||||
|
const sf::Color& visible_color,
|
||||||
|
const sf::Color& discovered_color,
|
||||||
|
const sf::Color& unknown_color) {
|
||||||
|
// Need parent grid for TCOD map access
|
||||||
|
if (!parent_grid) {
|
||||||
|
return; // Cannot compute FOV without parent grid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import UIGrid here to avoid circular dependency in header
|
||||||
|
// parent_grid is already a UIGrid*, we can use its tcod_map directly
|
||||||
|
// But we need to forward declare access to it...
|
||||||
|
|
||||||
|
// Compute FOV on the parent grid
|
||||||
|
parent_grid->computeFOV(source_x, source_y, radius, true, algorithm);
|
||||||
|
|
||||||
|
// Paint cells based on visibility
|
||||||
|
for (int cy = 0; cy < grid_y; ++cy) {
|
||||||
|
for (int cx = 0; cx < grid_x; ++cx) {
|
||||||
|
// Check if in FOV (visible right now)
|
||||||
|
if (parent_grid->isInFOV(cx, cy)) {
|
||||||
|
colors[cy * grid_x + cx] = visible_color;
|
||||||
|
}
|
||||||
|
// Check if previously discovered (current color != unknown)
|
||||||
|
else if (colors[cy * grid_x + cx] != unknown_color) {
|
||||||
|
colors[cy * grid_x + cx] = discovered_color;
|
||||||
|
}
|
||||||
|
// Otherwise leave as unknown (or set to unknown if first time)
|
||||||
|
else {
|
||||||
|
colors[cy * grid_x + cx] = unknown_color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark entire layer dirty
|
||||||
|
markDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorLayer::resize(int new_grid_x, int new_grid_y) {
|
void ColorLayer::resize(int new_grid_x, int new_grid_y) {
|
||||||
|
|
@ -92,36 +211,53 @@ void ColorLayer::resize(int new_grid_x, int new_grid_y) {
|
||||||
grid_x = new_grid_x;
|
grid_x = new_grid_x;
|
||||||
grid_y = new_grid_y;
|
grid_y = new_grid_y;
|
||||||
|
|
||||||
// #148 - Invalidate cached texture (will be resized on next render)
|
// Reinitialize chunks for new dimensions
|
||||||
texture_initialized = false;
|
initChunks();
|
||||||
markDirty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #148 - Render all cells to cached texture (called when dirty)
|
// Render a single chunk to its cached texture
|
||||||
void ColorLayer::renderToTexture(int cell_width, int cell_height) {
|
void ColorLayer::renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) {
|
||||||
ensureTextureSize(cell_width, cell_height);
|
int chunk_idx = chunk_y * chunks_x + chunk_x;
|
||||||
if (!texture_initialized) return;
|
if (chunk_idx < 0 || chunk_idx >= static_cast<int>(chunk_textures.size())) return;
|
||||||
|
if (!chunk_textures[chunk_idx]) return;
|
||||||
|
|
||||||
cached_texture.clear(sf::Color::Transparent);
|
ensureChunkTexture(chunk_idx, cell_width, cell_height);
|
||||||
|
if (!chunk_texture_initialized[chunk_idx]) return;
|
||||||
|
|
||||||
|
// Get chunk bounds
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(chunk_x, chunk_y, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
chunk_textures[chunk_idx]->clear(sf::Color::Transparent);
|
||||||
|
|
||||||
sf::RectangleShape rect;
|
sf::RectangleShape rect;
|
||||||
rect.setSize(sf::Vector2f(cell_width, cell_height));
|
rect.setSize(sf::Vector2f(cell_width, cell_height));
|
||||||
rect.setOutlineThickness(0);
|
rect.setOutlineThickness(0);
|
||||||
|
|
||||||
// Render all cells to cached texture (no zoom - 1:1 pixel mapping)
|
// Render only cells within this chunk (local coordinates in texture)
|
||||||
for (int x = 0; x < grid_x; ++x) {
|
for (int x = start_x; x < end_x; ++x) {
|
||||||
for (int y = 0; y < grid_y; ++y) {
|
for (int y = start_y; y < end_y; ++y) {
|
||||||
const sf::Color& color = at(x, y);
|
const sf::Color& color = at(x, y);
|
||||||
if (color.a == 0) continue; // Skip fully transparent
|
if (color.a == 0) continue; // Skip fully transparent
|
||||||
|
|
||||||
rect.setPosition(sf::Vector2f(x * cell_width, y * cell_height));
|
// Position relative to chunk origin
|
||||||
|
rect.setPosition(sf::Vector2f((x - start_x) * cell_width, (y - start_y) * cell_height));
|
||||||
rect.setFillColor(color);
|
rect.setFillColor(color);
|
||||||
cached_texture.draw(rect);
|
chunk_textures[chunk_idx]->draw(rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_texture.display();
|
chunk_textures[chunk_idx]->display();
|
||||||
dirty = false;
|
chunk_dirty[chunk_idx] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy: render all chunks (used by fill, resize, etc.)
|
||||||
|
void ColorLayer::renderToTexture(int cell_width, int cell_height) {
|
||||||
|
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||||
|
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||||
|
renderChunkToTexture(cx, cy, cell_width, cell_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ColorLayer::render(sf::RenderTarget& target,
|
void ColorLayer::render(sf::RenderTarget& target,
|
||||||
|
|
@ -130,61 +266,67 @@ void ColorLayer::render(sf::RenderTarget& target,
|
||||||
float zoom, int cell_width, int cell_height) {
|
float zoom, int cell_width, int cell_height) {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
|
|
||||||
// #148 - Use cached texture rendering
|
// Calculate visible chunk range
|
||||||
// Re-render to texture only if dirty
|
int chunk_left = std::max(0, left_edge / CHUNK_SIZE);
|
||||||
if (dirty || !texture_initialized) {
|
int chunk_top = std::max(0, top_edge / CHUNK_SIZE);
|
||||||
renderToTexture(cell_width, cell_height);
|
int chunk_right = std::min(chunks_x - 1, (x_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);
|
||||||
}
|
int chunk_bottom = std::min(chunks_y - 1, (y_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);
|
||||||
|
|
||||||
if (!texture_initialized) {
|
// Iterate only over visible chunks
|
||||||
// Fallback to direct rendering if texture creation failed
|
for (int cy = chunk_top; cy <= chunk_bottom; ++cy) {
|
||||||
sf::RectangleShape rect;
|
for (int cx = chunk_left; cx <= chunk_right; ++cx) {
|
||||||
rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
int chunk_idx = cy * chunks_x + cx;
|
||||||
rect.setOutlineThickness(0);
|
|
||||||
|
|
||||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) {
|
// Re-render chunk only if dirty AND visible
|
||||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) {
|
if (chunk_dirty[chunk_idx] || !chunk_texture_initialized[chunk_idx]) {
|
||||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
renderChunkToTexture(cx, cy, cell_width, cell_height);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!chunk_texture_initialized[chunk_idx]) {
|
||||||
|
// Fallback: direct rendering for this chunk
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
sf::RectangleShape rect;
|
||||||
|
rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
|
||||||
|
rect.setOutlineThickness(0);
|
||||||
|
|
||||||
|
for (int x = start_x; x < end_x; ++x) {
|
||||||
|
for (int y = start_y; y < end_y; ++y) {
|
||||||
|
const sf::Color& color = at(x, y);
|
||||||
|
if (color.a == 0) continue;
|
||||||
|
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(x * cell_width - left_spritepixels) * zoom,
|
||||||
|
(y * cell_height - top_spritepixels) * zoom
|
||||||
|
);
|
||||||
|
rect.setPosition(pixel_pos);
|
||||||
|
rect.setFillColor(color);
|
||||||
|
target.draw(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit this chunk's texture to target
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
// Chunk position in world pixel coordinates
|
||||||
|
float chunk_world_x = start_x * cell_width;
|
||||||
|
float chunk_world_y = start_y * cell_height;
|
||||||
|
|
||||||
|
// Position in target (accounting for viewport offset and zoom)
|
||||||
|
float dest_x = (chunk_world_x - left_spritepixels) * zoom;
|
||||||
|
float dest_y = (chunk_world_y - top_spritepixels) * zoom;
|
||||||
|
|
||||||
|
sf::Sprite chunk_sprite(chunk_textures[chunk_idx]->getTexture());
|
||||||
|
chunk_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
|
||||||
|
chunk_sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||||
|
|
||||||
|
target.draw(chunk_sprite);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -208,7 +350,37 @@ int TileLayer::at(int x, int y) const {
|
||||||
|
|
||||||
void TileLayer::fill(int tile_index) {
|
void TileLayer::fill(int tile_index) {
|
||||||
std::fill(tiles.begin(), tiles.end(), tile_index);
|
std::fill(tiles.begin(), tiles.end(), tile_index);
|
||||||
markDirty(); // #148 - Mark for re-render
|
markDirty(); // Mark ALL chunks for re-render
|
||||||
|
}
|
||||||
|
|
||||||
|
void TileLayer::fillRect(int x, int y, int width, int height, int tile_index) {
|
||||||
|
// Clamp to valid bounds
|
||||||
|
int x1 = std::max(0, x);
|
||||||
|
int y1 = std::max(0, y);
|
||||||
|
int x2 = std::min(grid_x, x + width);
|
||||||
|
int y2 = std::min(grid_y, y + height);
|
||||||
|
|
||||||
|
// Fill the rectangle
|
||||||
|
for (int fy = y1; fy < y2; ++fy) {
|
||||||
|
for (int fx = x1; fx < x2; ++fx) {
|
||||||
|
tiles[fy * grid_x + fx] = tile_index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark affected chunks dirty
|
||||||
|
int chunk_x1 = x1 / CHUNK_SIZE;
|
||||||
|
int chunk_y1 = y1 / CHUNK_SIZE;
|
||||||
|
int chunk_x2 = (x2 - 1) / CHUNK_SIZE;
|
||||||
|
int chunk_y2 = (y2 - 1) / CHUNK_SIZE;
|
||||||
|
|
||||||
|
for (int cy = chunk_y1; cy <= chunk_y2; ++cy) {
|
||||||
|
for (int cx = chunk_x1; cx <= chunk_x2; ++cx) {
|
||||||
|
int idx = cy * chunks_x + cx;
|
||||||
|
if (idx >= 0 && idx < static_cast<int>(chunk_dirty.size())) {
|
||||||
|
chunk_dirty[idx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileLayer::resize(int new_grid_x, int new_grid_y) {
|
void TileLayer::resize(int new_grid_x, int new_grid_y) {
|
||||||
|
|
@ -227,32 +399,51 @@ void TileLayer::resize(int new_grid_x, int new_grid_y) {
|
||||||
grid_x = new_grid_x;
|
grid_x = new_grid_x;
|
||||||
grid_y = new_grid_y;
|
grid_y = new_grid_y;
|
||||||
|
|
||||||
// #148 - Invalidate cached texture (will be resized on next render)
|
// Reinitialize chunks for new dimensions
|
||||||
texture_initialized = false;
|
initChunks();
|
||||||
markDirty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// #148 - Render all cells to cached texture (called when dirty)
|
// Render a single chunk to its cached texture
|
||||||
void TileLayer::renderToTexture(int cell_width, int cell_height) {
|
void TileLayer::renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) {
|
||||||
ensureTextureSize(cell_width, cell_height);
|
if (!texture) return;
|
||||||
if (!texture_initialized || !texture) return;
|
|
||||||
|
|
||||||
cached_texture.clear(sf::Color::Transparent);
|
int chunk_idx = chunk_y * chunks_x + chunk_x;
|
||||||
|
if (chunk_idx < 0 || chunk_idx >= static_cast<int>(chunk_textures.size())) return;
|
||||||
|
if (!chunk_textures[chunk_idx]) return;
|
||||||
|
|
||||||
// Render all tiles to cached texture (no zoom - 1:1 pixel mapping)
|
ensureChunkTexture(chunk_idx, cell_width, cell_height);
|
||||||
for (int x = 0; x < grid_x; ++x) {
|
if (!chunk_texture_initialized[chunk_idx]) return;
|
||||||
for (int y = 0; y < grid_y; ++y) {
|
|
||||||
|
// Get chunk bounds
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(chunk_x, chunk_y, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
chunk_textures[chunk_idx]->clear(sf::Color::Transparent);
|
||||||
|
|
||||||
|
// Render only tiles within this chunk (local coordinates in texture)
|
||||||
|
for (int x = start_x; x < end_x; ++x) {
|
||||||
|
for (int y = start_y; y < end_y; ++y) {
|
||||||
int tile_index = at(x, y);
|
int tile_index = at(x, y);
|
||||||
if (tile_index < 0) continue; // No tile
|
if (tile_index < 0) continue; // No tile
|
||||||
|
|
||||||
auto pixel_pos = sf::Vector2f(x * cell_width, y * cell_height);
|
// Position relative to chunk origin
|
||||||
|
auto pixel_pos = sf::Vector2f((x - start_x) * cell_width, (y - start_y) * cell_height);
|
||||||
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f));
|
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f));
|
||||||
cached_texture.draw(sprite);
|
chunk_textures[chunk_idx]->draw(sprite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cached_texture.display();
|
chunk_textures[chunk_idx]->display();
|
||||||
dirty = false;
|
chunk_dirty[chunk_idx] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy: render all chunks (used by fill, resize, etc.)
|
||||||
|
void TileLayer::renderToTexture(int cell_width, int cell_height) {
|
||||||
|
for (int cy = 0; cy < chunks_y; ++cy) {
|
||||||
|
for (int cx = 0; cx < chunks_x; ++cx) {
|
||||||
|
renderChunkToTexture(cx, cy, cell_width, cell_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TileLayer::render(sf::RenderTarget& target,
|
void TileLayer::render(sf::RenderTarget& target,
|
||||||
|
|
@ -261,56 +452,62 @@ void TileLayer::render(sf::RenderTarget& target,
|
||||||
float zoom, int cell_width, int cell_height) {
|
float zoom, int cell_width, int cell_height) {
|
||||||
if (!visible || !texture) return;
|
if (!visible || !texture) return;
|
||||||
|
|
||||||
// #148 - Use cached texture rendering
|
// Calculate visible chunk range
|
||||||
// Re-render to texture only if dirty
|
int chunk_left = std::max(0, left_edge / CHUNK_SIZE);
|
||||||
if (dirty || !texture_initialized) {
|
int chunk_top = std::max(0, top_edge / CHUNK_SIZE);
|
||||||
renderToTexture(cell_width, cell_height);
|
int chunk_right = std::min(chunks_x - 1, (x_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);
|
||||||
}
|
int chunk_bottom = std::min(chunks_y - 1, (y_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);
|
||||||
|
|
||||||
if (!texture_initialized) {
|
// Iterate only over visible chunks
|
||||||
// Fallback to direct rendering if texture creation failed
|
for (int cy = chunk_top; cy <= chunk_bottom; ++cy) {
|
||||||
for (int x = (left_edge - 1 >= 0 ? left_edge - 1 : 0); x < x_limit; ++x) {
|
for (int cx = chunk_left; cx <= chunk_right; ++cx) {
|
||||||
for (int y = (top_edge - 1 >= 0 ? top_edge - 1 : 0); y < y_limit; ++y) {
|
int chunk_idx = cy * chunks_x + cx;
|
||||||
if (x < 0 || x >= grid_x || y < 0 || y >= grid_y) continue;
|
|
||||||
|
|
||||||
int tile_index = at(x, y);
|
// Re-render chunk only if dirty AND visible
|
||||||
if (tile_index < 0) continue;
|
if (chunk_dirty[chunk_idx] || !chunk_texture_initialized[chunk_idx]) {
|
||||||
|
renderChunkToTexture(cx, cy, cell_width, cell_height);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!chunk_texture_initialized[chunk_idx]) {
|
||||||
|
// Fallback: direct rendering for this chunk
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
for (int x = start_x; x < end_x; ++x) {
|
||||||
|
for (int y = start_y; y < end_y; ++y) {
|
||||||
|
int tile_index = at(x, y);
|
||||||
|
if (tile_index < 0) continue;
|
||||||
|
|
||||||
|
auto pixel_pos = sf::Vector2f(
|
||||||
|
(x * cell_width - left_spritepixels) * zoom,
|
||||||
|
(y * cell_height - top_spritepixels) * zoom
|
||||||
|
);
|
||||||
|
sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(zoom, zoom));
|
||||||
|
target.draw(sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit this chunk's texture to target
|
||||||
|
int start_x, start_y, end_x, end_y;
|
||||||
|
getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);
|
||||||
|
|
||||||
|
// Chunk position in world pixel coordinates
|
||||||
|
float chunk_world_x = start_x * cell_width;
|
||||||
|
float chunk_world_y = start_y * cell_height;
|
||||||
|
|
||||||
|
// Position in target (accounting for viewport offset and zoom)
|
||||||
|
float dest_x = (chunk_world_x - left_spritepixels) * zoom;
|
||||||
|
float dest_y = (chunk_world_y - top_spritepixels) * zoom;
|
||||||
|
|
||||||
|
sf::Sprite chunk_sprite(chunk_textures[chunk_idx]->getTexture());
|
||||||
|
chunk_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
|
||||||
|
chunk_sprite.setScale(sf::Vector2f(zoom, zoom));
|
||||||
|
|
||||||
|
target.draw(chunk_sprite);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -324,6 +521,24 @@ PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
|
||||||
"set(x, y, color)\n\nSet the color at cell position (x, y)."},
|
"set(x, y, color)\n\nSet the color at cell position (x, y)."},
|
||||||
{"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
|
{"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
|
||||||
"fill(color)\n\nFill the entire layer with the specified color."},
|
"fill(color)\n\nFill the entire layer with the specified color."},
|
||||||
|
{"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"fill_rect(pos, size, color)\n\n"
|
||||||
|
"Fill a rectangular region with a color.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos (tuple): Top-left corner as (x, y)\n"
|
||||||
|
" size (tuple): Dimensions as (width, height)\n"
|
||||||
|
" color: Color object or (r, g, b[, a]) tuple"},
|
||||||
|
{"draw_fov", (PyCFunction)PyGridLayerAPI::ColorLayer_draw_fov, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"draw_fov(source, radius=None, fov=None, visible=None, discovered=None, unknown=None)\n\n"
|
||||||
|
"Paint cells based on field-of-view visibility from source position.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" source (tuple): FOV origin as (x, y)\n"
|
||||||
|
" radius (int): FOV radius. Default: grid's fov_radius\n"
|
||||||
|
" fov (FOV): FOV algorithm. Default: grid's fov setting\n"
|
||||||
|
" visible (Color): Color for currently visible cells\n"
|
||||||
|
" discovered (Color): Color for previously seen cells\n"
|
||||||
|
" unknown (Color): Color for never-seen cells\n\n"
|
||||||
|
"Note: Layer must be attached to a grid for FOV calculation."},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -442,7 +657,7 @@ PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* arg
|
||||||
Py_DECREF(color_type);
|
Py_DECREF(color_type);
|
||||||
|
|
||||||
self->data->at(x, y) = color;
|
self->data->at(x, y) = color;
|
||||||
self->data->markDirty(); // #148 - Mark for re-render
|
self->data->markDirty(x, y); // Mark only the affected chunk
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -486,6 +701,170 @@ PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* ar
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"pos", "size", "color", NULL};
|
||||||
|
PyObject* pos_obj;
|
||||||
|
PyObject* size_obj;
|
||||||
|
PyObject* color_obj;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO", const_cast<char**>(kwlist),
|
||||||
|
&pos_obj, &size_obj, &color_obj)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pos
|
||||||
|
int x, y;
|
||||||
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||||
|
x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
|
||||||
|
y = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "pos must be a (x, y) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse size
|
||||||
|
int width, height;
|
||||||
|
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||||
|
width = PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
||||||
|
height = PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse color
|
||||||
|
sf::Color color;
|
||||||
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
|
if (!mcrfpy_module) return NULL;
|
||||||
|
|
||||||
|
auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
|
||||||
|
Py_DECREF(mcrfpy_module);
|
||||||
|
if (!color_type) return NULL;
|
||||||
|
|
||||||
|
if (PyObject_IsInstance(color_obj, color_type)) {
|
||||||
|
color = ((PyColorObject*)color_obj)->data;
|
||||||
|
} else if (PyTuple_Check(color_obj)) {
|
||||||
|
int r, g, b, a = 255;
|
||||||
|
if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
color = sf::Color(r, g, b, a);
|
||||||
|
} else {
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
|
||||||
|
self->data->fillRect(x, y, width, height, color);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::ColorLayer_draw_fov(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"source", "radius", "fov", "visible", "discovered", "unknown", NULL};
|
||||||
|
PyObject* source_obj;
|
||||||
|
int radius = -1; // -1 means use grid's default
|
||||||
|
PyObject* fov_obj = Py_None;
|
||||||
|
PyObject* visible_obj = nullptr;
|
||||||
|
PyObject* discovered_obj = nullptr;
|
||||||
|
PyObject* unknown_obj = nullptr;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|iOOOO", const_cast<char**>(kwlist),
|
||||||
|
&source_obj, &radius, &fov_obj, &visible_obj, &discovered_obj, &unknown_obj)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->grid) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer is not attached to a grid");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse source position
|
||||||
|
int source_x, source_y;
|
||||||
|
if (PyTuple_Check(source_obj) && PyTuple_Size(source_obj) == 2) {
|
||||||
|
source_x = PyLong_AsLong(PyTuple_GetItem(source_obj, 0));
|
||||||
|
source_y = PyLong_AsLong(PyTuple_GetItem(source_obj, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "source must be a (x, y) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get radius from grid if not specified
|
||||||
|
if (radius < 0) {
|
||||||
|
radius = self->grid->fov_radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get FOV algorithm
|
||||||
|
TCOD_fov_algorithm_t algorithm;
|
||||||
|
bool was_none = false;
|
||||||
|
if (!PyFOV::from_arg(fov_obj, &algorithm, &was_none)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (was_none) {
|
||||||
|
algorithm = self->grid->fov_algorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper lambda to parse color
|
||||||
|
auto parse_color = [](PyObject* obj, sf::Color& out, const sf::Color& default_val, const char* name) -> bool {
|
||||||
|
if (!obj || obj == Py_None) {
|
||||||
|
out = default_val;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
|
||||||
|
if (!mcrfpy_module) return false;
|
||||||
|
|
||||||
|
auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
|
||||||
|
Py_DECREF(mcrfpy_module);
|
||||||
|
if (!color_type) return false;
|
||||||
|
|
||||||
|
if (PyObject_IsInstance(obj, color_type)) {
|
||||||
|
out = ((PyColorObject*)obj)->data;
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
return true;
|
||||||
|
} else if (PyTuple_Check(obj)) {
|
||||||
|
int r, g, b, a = 255;
|
||||||
|
if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) {
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out = sf::Color(r, g, b, a);
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(color_type);
|
||||||
|
PyErr_Format(PyExc_TypeError, "%s must be a Color object or (r, g, b[, a]) tuple", name);
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Default colors for FOV visualization
|
||||||
|
sf::Color visible_color(255, 255, 200, 64); // Light yellow tint
|
||||||
|
sf::Color discovered_color(128, 128, 128, 128); // Gray
|
||||||
|
sf::Color unknown_color(0, 0, 0, 255); // Black
|
||||||
|
|
||||||
|
if (!parse_color(visible_obj, visible_color, visible_color, "visible")) return NULL;
|
||||||
|
if (!parse_color(discovered_obj, discovered_color, discovered_color, "discovered")) return NULL;
|
||||||
|
if (!parse_color(unknown_obj, unknown_color, unknown_color, "unknown")) return NULL;
|
||||||
|
|
||||||
|
self->data->drawFOV(source_x, source_y, radius, algorithm, visible_color, discovered_color, unknown_color);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
PyObject* PyGridLayerAPI::ColorLayer_get_z_index(PyColorLayerObject* self, void* closure) {
|
PyObject* PyGridLayerAPI::ColorLayer_get_z_index(PyColorLayerObject* self, void* closure) {
|
||||||
if (!self->data) {
|
if (!self->data) {
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
|
@ -556,6 +935,13 @@ PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
|
||||||
"set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."},
|
"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", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS,
|
||||||
"fill(index)\n\nFill the entire layer with the specified tile index."},
|
"fill(index)\n\nFill the entire layer with the specified tile index."},
|
||||||
|
{"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
"fill_rect(pos, size, index)\n\n"
|
||||||
|
"Fill a rectangular region with a tile index.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos (tuple): Top-left corner as (x, y)\n"
|
||||||
|
" size (tuple): Dimensions as (width, height)\n"
|
||||||
|
" index (int): Tile index to fill with (-1 for no tile)"},
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -661,7 +1047,7 @@ PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args)
|
||||||
}
|
}
|
||||||
|
|
||||||
self->data->at(x, y) = index;
|
self->data->at(x, y) = index;
|
||||||
self->data->markDirty(); // #148 - Mark for re-render
|
self->data->markDirty(x, y); // Mark only the affected chunk
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -680,6 +1066,48 @@ PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args
|
||||||
Py_RETURN_NONE;
|
Py_RETURN_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PyObject* PyGridLayerAPI::TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {"pos", "size", "index", NULL};
|
||||||
|
PyObject* pos_obj;
|
||||||
|
PyObject* size_obj;
|
||||||
|
int tile_index;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi", const_cast<char**>(kwlist),
|
||||||
|
&pos_obj, &size_obj, &tile_index)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->data) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pos
|
||||||
|
int x, y;
|
||||||
|
if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
|
||||||
|
x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
|
||||||
|
y = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "pos must be a (x, y) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse size
|
||||||
|
int width, height;
|
||||||
|
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||||
|
width = PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
|
||||||
|
height = PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
|
||||||
|
if (PyErr_Occurred()) return NULL;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
self->data->fillRect(x, y, width, height, tile_index);
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
PyObject* PyGridLayerAPI::TileLayer_get_z_index(PyTileLayerObject* self, void* closure) {
|
PyObject* PyGridLayerAPI::TileLayer_get_z_index(PyTileLayerObject* self, void* closure) {
|
||||||
if (!self->data) {
|
if (!self->data) {
|
||||||
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
|
||||||
|
|
@ -749,7 +1177,7 @@ int PyGridLayerAPI::TileLayer_set_texture(PyTileLayerObject* self, PyObject* val
|
||||||
|
|
||||||
if (value == Py_None) {
|
if (value == Py_None) {
|
||||||
self->data->texture.reset();
|
self->data->texture.reset();
|
||||||
self->data->markDirty(); // #148 - Mark for re-render
|
self->data->markDirty(); // Mark ALL chunks for re-render (texture change affects all)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -768,7 +1196,7 @@ int PyGridLayerAPI::TileLayer_set_texture(PyTileLayerObject* self, PyObject* val
|
||||||
Py_DECREF(texture_type);
|
Py_DECREF(texture_type);
|
||||||
|
|
||||||
self->data->texture = ((PyTextureObject*)value)->data;
|
self->data->texture = ((PyTextureObject*)value)->data;
|
||||||
self->data->markDirty(); // #148 - Mark for re-render
|
self->data->markDirty(); // Mark ALL chunks for re-render (texture change affects all)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "Python.h"
|
#include "Python.h"
|
||||||
#include "structmember.h"
|
#include "structmember.h"
|
||||||
#include <SFML/Graphics.hpp>
|
#include <SFML/Graphics.hpp>
|
||||||
|
#include <libtcod.h>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
@ -23,6 +24,9 @@ enum class GridLayerType {
|
||||||
// Abstract base class for grid layers
|
// Abstract base class for grid layers
|
||||||
class GridLayer {
|
class GridLayer {
|
||||||
public:
|
public:
|
||||||
|
// Chunk size for per-chunk dirty tracking (matches GridChunk::CHUNK_SIZE)
|
||||||
|
static constexpr int CHUNK_SIZE = 64;
|
||||||
|
|
||||||
GridLayerType type;
|
GridLayerType type;
|
||||||
std::string name; // #150 - Layer name for GridPoint property access
|
std::string name; // #150 - Layer name for GridPoint property access
|
||||||
int z_index; // Negative = below entities, >= 0 = above entities
|
int z_index; // Negative = below entities, >= 0 = above entities
|
||||||
|
|
@ -30,33 +34,53 @@ public:
|
||||||
UIGrid* parent_grid; // Parent grid reference
|
UIGrid* parent_grid; // Parent grid reference
|
||||||
bool visible; // Visibility flag
|
bool visible; // Visibility flag
|
||||||
|
|
||||||
// #148 - Dirty flag and RenderTexture caching
|
// Chunk dimensions
|
||||||
bool dirty; // True if layer needs re-render
|
int chunks_x, chunks_y;
|
||||||
sf::RenderTexture cached_texture; // Cached layer content
|
|
||||||
sf::Sprite cached_sprite; // Sprite for blitting cached texture
|
// Per-chunk dirty flags and RenderTextures
|
||||||
bool texture_initialized; // True if RenderTexture has been created
|
std::vector<bool> chunk_dirty; // One flag per chunk
|
||||||
int cached_cell_width, cached_cell_height; // Cell size used for cached texture
|
std::vector<std::unique_ptr<sf::RenderTexture>> chunk_textures; // One texture per chunk
|
||||||
|
std::vector<bool> chunk_texture_initialized; // Track which textures are created
|
||||||
|
int cached_cell_width, cached_cell_height; // Cell size used for cached textures
|
||||||
|
|
||||||
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
|
GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent);
|
||||||
virtual ~GridLayer() = default;
|
virtual ~GridLayer() = default;
|
||||||
|
|
||||||
// Mark layer as needing re-render
|
// Mark entire layer as needing re-render
|
||||||
void markDirty();
|
void markDirty();
|
||||||
|
|
||||||
// Ensure cached texture is properly sized for current grid dimensions
|
// Mark specific cell's chunk as dirty
|
||||||
void ensureTextureSize(int cell_width, int cell_height);
|
void markDirty(int cell_x, int cell_y);
|
||||||
|
|
||||||
// Render the layer content to the cached texture (called when dirty)
|
// Get chunk index for a cell
|
||||||
|
int getChunkIndex(int cell_x, int cell_y) const;
|
||||||
|
|
||||||
|
// Get chunk coordinates for a cell
|
||||||
|
void getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const;
|
||||||
|
|
||||||
|
// Get cell bounds for a chunk
|
||||||
|
void getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const;
|
||||||
|
|
||||||
|
// Ensure a specific chunk's texture is properly sized
|
||||||
|
void ensureChunkTexture(int chunk_idx, int cell_width, int cell_height);
|
||||||
|
|
||||||
|
// Initialize chunk tracking arrays
|
||||||
|
void initChunks();
|
||||||
|
|
||||||
|
// Render a specific chunk to its cached texture (called when chunk is dirty)
|
||||||
|
virtual void renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) = 0;
|
||||||
|
|
||||||
|
// Render the layer content to the cached texture (legacy - marks all dirty)
|
||||||
virtual void renderToTexture(int cell_width, int cell_height) = 0;
|
virtual void renderToTexture(int cell_width, int cell_height) = 0;
|
||||||
|
|
||||||
// Render the layer to a RenderTarget with the given transformation parameters
|
// Render the layer to a RenderTarget with the given transformation parameters
|
||||||
// Uses cached texture if available, only re-renders when dirty
|
// Uses cached chunk textures, only re-renders visible dirty chunks
|
||||||
virtual void render(sf::RenderTarget& target,
|
virtual void render(sf::RenderTarget& target,
|
||||||
float left_spritepixels, float top_spritepixels,
|
float left_spritepixels, float top_spritepixels,
|
||||||
int left_edge, int top_edge, int x_limit, int y_limit,
|
int left_edge, int top_edge, int x_limit, int y_limit,
|
||||||
float zoom, int cell_width, int cell_height) = 0;
|
float zoom, int cell_width, int cell_height) = 0;
|
||||||
|
|
||||||
// Resize the layer (reallocates storage)
|
// Resize the layer (reallocates storage and reinitializes chunks)
|
||||||
virtual void resize(int new_grid_x, int new_grid_y) = 0;
|
virtual void resize(int new_grid_x, int new_grid_y) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -74,7 +98,21 @@ public:
|
||||||
// Fill entire layer with a color
|
// Fill entire layer with a color
|
||||||
void fill(const sf::Color& color);
|
void fill(const sf::Color& color);
|
||||||
|
|
||||||
// #148 - Render all content to cached texture
|
// Fill a rectangular region with a color (#113)
|
||||||
|
void fillRect(int x, int y, int width, int height, const sf::Color& color);
|
||||||
|
|
||||||
|
// Draw FOV-based visibility (#113)
|
||||||
|
// Paints cells based on current FOV state from parent grid
|
||||||
|
void drawFOV(int source_x, int source_y, int radius,
|
||||||
|
TCOD_fov_algorithm_t algorithm,
|
||||||
|
const sf::Color& visible,
|
||||||
|
const sf::Color& discovered,
|
||||||
|
const sf::Color& unknown);
|
||||||
|
|
||||||
|
// Render a specific chunk to its texture (called when chunk is dirty AND visible)
|
||||||
|
void renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) override;
|
||||||
|
|
||||||
|
// #148 - Render all content to cached texture (legacy - calls renderChunkToTexture for all)
|
||||||
void renderToTexture(int cell_width, int cell_height) override;
|
void renderToTexture(int cell_width, int cell_height) override;
|
||||||
|
|
||||||
void render(sf::RenderTarget& target,
|
void render(sf::RenderTarget& target,
|
||||||
|
|
@ -101,7 +139,13 @@ public:
|
||||||
// Fill entire layer with a tile index
|
// Fill entire layer with a tile index
|
||||||
void fill(int tile_index);
|
void fill(int tile_index);
|
||||||
|
|
||||||
// #148 - Render all content to cached texture
|
// Fill a rectangular region with a tile index (#113)
|
||||||
|
void fillRect(int x, int y, int width, int height, int tile_index);
|
||||||
|
|
||||||
|
// Render a specific chunk to its texture (called when chunk is dirty AND visible)
|
||||||
|
void renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) override;
|
||||||
|
|
||||||
|
// #148 - Render all content to cached texture (legacy - calls renderChunkToTexture for all)
|
||||||
void renderToTexture(int cell_width, int cell_height) override;
|
void renderToTexture(int cell_width, int cell_height) override;
|
||||||
|
|
||||||
void render(sf::RenderTarget& target,
|
void render(sf::RenderTarget& target,
|
||||||
|
|
@ -139,6 +183,8 @@ public:
|
||||||
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args);
|
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args);
|
||||||
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
|
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
|
||||||
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
|
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
|
||||||
|
static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* ColorLayer_draw_fov(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* ColorLayer_get_z_index(PyColorLayerObject* self, void* closure);
|
static PyObject* ColorLayer_get_z_index(PyColorLayerObject* self, void* closure);
|
||||||
static int ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure);
|
static int ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
|
static PyObject* ColorLayer_get_visible(PyColorLayerObject* self, void* closure);
|
||||||
|
|
@ -151,6 +197,7 @@ public:
|
||||||
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args);
|
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args);
|
||||||
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
|
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
|
||||||
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
|
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
|
||||||
|
static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* TileLayer_get_z_index(PyTileLayerObject* self, void* closure);
|
static PyObject* TileLayer_get_z_index(PyTileLayerObject* self, void* closure);
|
||||||
static int TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure);
|
static int TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* TileLayer_get_visible(PyTileLayerObject* self, void* closure);
|
static PyObject* TileLayer_get_visible(PyTileLayerObject* self, void* closure);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
#include "PyTimer.h"
|
#include "PyTimer.h"
|
||||||
#include "PyWindow.h"
|
#include "PyWindow.h"
|
||||||
#include "PySceneObject.h"
|
#include "PySceneObject.h"
|
||||||
|
#include "PyFOV.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "ImGuiConsole.h"
|
#include "ImGuiConsole.h"
|
||||||
#include "BenchmarkLogger.h"
|
#include "BenchmarkLogger.h"
|
||||||
|
|
@ -366,20 +367,28 @@ PyObject* PyInit_mcrfpy()
|
||||||
PyModule_AddObject(m, "default_font", Py_None);
|
PyModule_AddObject(m, "default_font", Py_None);
|
||||||
PyModule_AddObject(m, "default_texture", Py_None);
|
PyModule_AddObject(m, "default_texture", Py_None);
|
||||||
|
|
||||||
// Add TCOD FOV algorithm constants
|
// Add FOV enum class (uses Python's IntEnum) (#114)
|
||||||
PyModule_AddIntConstant(m, "FOV_BASIC", FOV_BASIC);
|
PyObject* fov_class = PyFOV::create_enum_class(m);
|
||||||
PyModule_AddIntConstant(m, "FOV_DIAMOND", FOV_DIAMOND);
|
if (!fov_class) {
|
||||||
PyModule_AddIntConstant(m, "FOV_SHADOW", FOV_SHADOW);
|
// If enum creation fails, continue without it (non-fatal)
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
|
PyErr_Clear();
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
|
}
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
|
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
|
// Add default_fov module property - defaults to FOV.BASIC
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
|
// New grids copy this value at creation time
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
|
if (fov_class) {
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
|
PyObject* default_fov = PyObject_GetAttrString(fov_class, "BASIC");
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
|
if (default_fov) {
|
||||||
PyModule_AddIntConstant(m, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
|
PyModule_AddObject(m, "default_fov", default_fov);
|
||||||
PyModule_AddIntConstant(m, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
|
} else {
|
||||||
|
PyErr_Clear();
|
||||||
|
// Fallback to integer
|
||||||
|
PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to integer if enum failed
|
||||||
|
PyModule_AddIntConstant(m, "default_fov", FOV_BASIC);
|
||||||
|
}
|
||||||
|
|
||||||
// Add automation submodule
|
// Add automation submodule
|
||||||
PyObject* automation_module = McRFPy_Automation::init_automation_module();
|
PyObject* automation_module = McRFPy_Automation::init_automation_module();
|
||||||
|
|
|
||||||
|
|
@ -185,38 +185,19 @@ static PyObject* McRFPy_Libtcod::dijkstra_path_to(PyObject* self, PyObject* args
|
||||||
return path_list;
|
return path_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add FOV algorithm constants to module
|
// FOV algorithm constants removed - use mcrfpy.FOV enum instead (#114)
|
||||||
static PyObject* McRFPy_Libtcod::add_fov_constants(PyObject* module) {
|
|
||||||
// FOV algorithms
|
|
||||||
PyModule_AddIntConstant(module, "FOV_BASIC", FOV_BASIC);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_DIAMOND", FOV_DIAMOND);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_SHADOW", FOV_SHADOW);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_0", FOV_PERMISSIVE_0);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_1", FOV_PERMISSIVE_1);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_2", FOV_PERMISSIVE_2);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_3", FOV_PERMISSIVE_3);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_4", FOV_PERMISSIVE_4);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_5", FOV_PERMISSIVE_5);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_6", FOV_PERMISSIVE_6);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_7", FOV_PERMISSIVE_7);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_PERMISSIVE_8", FOV_PERMISSIVE_8);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_RESTRICTIVE", FOV_RESTRICTIVE);
|
|
||||||
PyModule_AddIntConstant(module, "FOV_SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST);
|
|
||||||
|
|
||||||
return module;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method definitions
|
// Method definitions
|
||||||
static PyMethodDef libtcodMethods[] = {
|
static PyMethodDef libtcodMethods[] = {
|
||||||
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
|
{"compute_fov", McRFPy_Libtcod::compute_fov, METH_VARARGS,
|
||||||
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=FOV_BASIC)\n\n"
|
"compute_fov(grid, x, y, radius, light_walls=True, algorithm=mcrfpy.FOV.BASIC)\n\n"
|
||||||
"Compute field of view from a position.\n\n"
|
"Compute field of view from a position.\n\n"
|
||||||
"Args:\n"
|
"Args:\n"
|
||||||
" grid: Grid object to compute FOV on\n"
|
" grid: Grid object to compute FOV on\n"
|
||||||
" x, y: Origin position\n"
|
" x, y: Origin position\n"
|
||||||
" radius: Maximum sight radius\n"
|
" radius: Maximum sight radius\n"
|
||||||
" light_walls: Whether walls are lit when in FOV\n"
|
" light_walls: Whether walls are lit when in FOV\n"
|
||||||
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_SHADOW, etc.)\n\n"
|
" algorithm: FOV algorithm (mcrfpy.FOV.BASIC, mcrfpy.FOV.SHADOW, etc.)\n\n"
|
||||||
"Returns:\n"
|
"Returns:\n"
|
||||||
" List of (x, y) tuples for visible cells"},
|
" List of (x, y) tuples for visible cells"},
|
||||||
|
|
||||||
|
|
@ -293,13 +274,13 @@ static PyModuleDef libtcodModule = {
|
||||||
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
|
"TCOD-compatible algorithms for field of view, pathfinding, and line drawing.\n\n"
|
||||||
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
|
"This module provides access to TCOD's algorithms integrated with McRogueFace grids.\n"
|
||||||
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
|
"Unlike the original TCOD, these functions work directly with Grid objects.\n\n"
|
||||||
"FOV Algorithms:\n"
|
"FOV Algorithms (use mcrfpy.FOV enum):\n"
|
||||||
" FOV_BASIC - Basic circular FOV\n"
|
" mcrfpy.FOV.BASIC - Basic circular FOV\n"
|
||||||
" FOV_SHADOW - Shadow casting (recommended)\n"
|
" mcrfpy.FOV.SHADOW - Shadow casting (recommended)\n"
|
||||||
" FOV_DIAMOND - Diamond-shaped FOV\n"
|
" mcrfpy.FOV.DIAMOND - Diamond-shaped FOV\n"
|
||||||
" FOV_PERMISSIVE_0 through FOV_PERMISSIVE_8 - Permissive variants\n"
|
" mcrfpy.FOV.PERMISSIVE_0 through PERMISSIVE_8 - Permissive variants\n"
|
||||||
" FOV_RESTRICTIVE - Most restrictive FOV\n"
|
" mcrfpy.FOV.RESTRICTIVE - Most restrictive FOV\n"
|
||||||
" FOV_SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
|
" mcrfpy.FOV.SYMMETRIC_SHADOWCAST - Symmetric shadow casting\n\n"
|
||||||
"Example:\n"
|
"Example:\n"
|
||||||
" import mcrfpy\n"
|
" import mcrfpy\n"
|
||||||
" from mcrfpy import libtcod\n\n"
|
" from mcrfpy import libtcod\n\n"
|
||||||
|
|
@ -317,8 +298,7 @@ PyObject* McRFPy_Libtcod::init_libtcod_module() {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add FOV algorithm constants
|
// FOV algorithm constants now provided by mcrfpy.FOV enum (#114)
|
||||||
add_fov_constants(m);
|
|
||||||
|
|
||||||
return m;
|
return m;
|
||||||
}
|
}
|
||||||
|
|
@ -19,9 +19,6 @@ namespace McRFPy_Libtcod
|
||||||
static PyObject* line(PyObject* self, PyObject* args);
|
static PyObject* line(PyObject* self, PyObject* args);
|
||||||
static PyObject* line_iter(PyObject* self, PyObject* args);
|
static PyObject* line_iter(PyObject* self, PyObject* args);
|
||||||
|
|
||||||
// FOV algorithm constants
|
|
||||||
static PyObject* add_fov_constants(PyObject* module);
|
|
||||||
|
|
||||||
// Module initialization
|
// Module initialization
|
||||||
PyObject* init_libtcod_module();
|
PyObject* init_libtcod_module();
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
#include "PyFOV.h"
|
||||||
|
#include "McRFPy_API.h"
|
||||||
|
|
||||||
|
// Static storage for cached enum class reference
|
||||||
|
PyObject* PyFOV::fov_enum_class = nullptr;
|
||||||
|
|
||||||
|
PyObject* PyFOV::create_enum_class(PyObject* module) {
|
||||||
|
// Import IntEnum from enum module
|
||||||
|
PyObject* enum_module = PyImport_ImportModule("enum");
|
||||||
|
if (!enum_module) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
|
||||||
|
Py_DECREF(enum_module);
|
||||||
|
if (!int_enum) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create dict of enum members
|
||||||
|
PyObject* members = PyDict_New();
|
||||||
|
if (!members) {
|
||||||
|
Py_DECREF(int_enum);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all FOV algorithm members
|
||||||
|
struct {
|
||||||
|
const char* name;
|
||||||
|
int value;
|
||||||
|
} fov_members[] = {
|
||||||
|
{"BASIC", FOV_BASIC},
|
||||||
|
{"DIAMOND", FOV_DIAMOND},
|
||||||
|
{"SHADOW", FOV_SHADOW},
|
||||||
|
{"PERMISSIVE_0", FOV_PERMISSIVE_0},
|
||||||
|
{"PERMISSIVE_1", FOV_PERMISSIVE_1},
|
||||||
|
{"PERMISSIVE_2", FOV_PERMISSIVE_2},
|
||||||
|
{"PERMISSIVE_3", FOV_PERMISSIVE_3},
|
||||||
|
{"PERMISSIVE_4", FOV_PERMISSIVE_4},
|
||||||
|
{"PERMISSIVE_5", FOV_PERMISSIVE_5},
|
||||||
|
{"PERMISSIVE_6", FOV_PERMISSIVE_6},
|
||||||
|
{"PERMISSIVE_7", FOV_PERMISSIVE_7},
|
||||||
|
{"PERMISSIVE_8", FOV_PERMISSIVE_8},
|
||||||
|
{"RESTRICTIVE", FOV_RESTRICTIVE},
|
||||||
|
{"SYMMETRIC_SHADOWCAST", FOV_SYMMETRIC_SHADOWCAST},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& m : fov_members) {
|
||||||
|
PyObject* value = PyLong_FromLong(m.value);
|
||||||
|
if (!value) {
|
||||||
|
Py_DECREF(members);
|
||||||
|
Py_DECREF(int_enum);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (PyDict_SetItemString(members, m.name, value) < 0) {
|
||||||
|
Py_DECREF(value);
|
||||||
|
Py_DECREF(members);
|
||||||
|
Py_DECREF(int_enum);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
Py_DECREF(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call IntEnum("FOV", members) to create the enum class
|
||||||
|
PyObject* name = PyUnicode_FromString("FOV");
|
||||||
|
if (!name) {
|
||||||
|
Py_DECREF(members);
|
||||||
|
Py_DECREF(int_enum);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntEnum(name, members) using functional API
|
||||||
|
PyObject* args = PyTuple_Pack(2, name, members);
|
||||||
|
Py_DECREF(name);
|
||||||
|
Py_DECREF(members);
|
||||||
|
if (!args) {
|
||||||
|
Py_DECREF(int_enum);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObject* fov_class = PyObject_Call(int_enum, args, NULL);
|
||||||
|
Py_DECREF(args);
|
||||||
|
Py_DECREF(int_enum);
|
||||||
|
|
||||||
|
if (!fov_class) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the reference for fast type checking
|
||||||
|
fov_enum_class = fov_class;
|
||||||
|
Py_INCREF(fov_enum_class);
|
||||||
|
|
||||||
|
// Add to module
|
||||||
|
if (PyModule_AddObject(module, "FOV", fov_class) < 0) {
|
||||||
|
Py_DECREF(fov_class);
|
||||||
|
fov_enum_class = nullptr;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fov_class;
|
||||||
|
}
|
||||||
|
|
||||||
|
int PyFOV::from_arg(PyObject* arg, TCOD_fov_algorithm_t* out_algo, bool* was_none) {
|
||||||
|
if (was_none) *was_none = false;
|
||||||
|
|
||||||
|
// Accept None -> caller should use default
|
||||||
|
if (arg == Py_None) {
|
||||||
|
if (was_none) *was_none = true;
|
||||||
|
*out_algo = FOV_BASIC;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept FOV enum member (check if it's an instance of our enum)
|
||||||
|
if (fov_enum_class && PyObject_IsInstance(arg, fov_enum_class)) {
|
||||||
|
// IntEnum members have a 'value' attribute
|
||||||
|
PyObject* value = PyObject_GetAttrString(arg, "value");
|
||||||
|
if (!value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
long val = PyLong_AsLong(value);
|
||||||
|
Py_DECREF(value);
|
||||||
|
if (val == -1 && PyErr_Occurred()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*out_algo = (TCOD_fov_algorithm_t)val;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept int (for backwards compatibility)
|
||||||
|
if (PyLong_Check(arg)) {
|
||||||
|
long val = PyLong_AsLong(arg);
|
||||||
|
if (val == -1 && PyErr_Occurred()) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (val < 0 || val >= NB_FOV_ALGORITHMS) {
|
||||||
|
PyErr_Format(PyExc_ValueError,
|
||||||
|
"Invalid FOV algorithm value: %ld. Must be 0-%d or use mcrfpy.FOV enum.",
|
||||||
|
val, NB_FOV_ALGORITHMS - 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
*out_algo = (TCOD_fov_algorithm_t)val;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyErr_SetString(PyExc_TypeError,
|
||||||
|
"FOV algorithm must be mcrfpy.FOV enum member, int, or None");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#pragma once
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include <libtcod.h>
|
||||||
|
|
||||||
|
// Module-level FOV enum class (created at runtime using Python's IntEnum)
|
||||||
|
// Stored as a module attribute: mcrfpy.FOV
|
||||||
|
|
||||||
|
class PyFOV {
|
||||||
|
public:
|
||||||
|
// Create the FOV enum class and add to module
|
||||||
|
// Returns the enum class (new reference), or NULL on error
|
||||||
|
static PyObject* create_enum_class(PyObject* module);
|
||||||
|
|
||||||
|
// Helper to extract algorithm from Python arg (accepts FOV enum, int, or None)
|
||||||
|
// Returns 1 on success, 0 on error (with exception set)
|
||||||
|
// If arg is None, sets *out_algo to the default (FOV_BASIC) and sets *was_none to true
|
||||||
|
static int from_arg(PyObject* arg, TCOD_fov_algorithm_t* out_algo, bool* was_none = nullptr);
|
||||||
|
|
||||||
|
// Cached reference to the FOV enum class for fast type checking
|
||||||
|
static PyObject* fov_enum_class;
|
||||||
|
};
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "Profiler.h"
|
#include "Profiler.h"
|
||||||
|
#include "PyFOV.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath> // #142 - for std::floor
|
#include <cmath> // #142 - for std::floor
|
||||||
#include <cstring> // #150 - for strcmp
|
#include <cstring> // #150 - for strcmp
|
||||||
|
|
@ -12,7 +13,8 @@
|
||||||
UIGrid::UIGrid()
|
UIGrid::UIGrid()
|
||||||
: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr),
|
: 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),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||||
perspective_enabled(false), use_chunks(false) // Default to omniscient view
|
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
||||||
|
use_chunks(false) // Default to omniscient view
|
||||||
{
|
{
|
||||||
// Initialize entities list
|
// Initialize entities list
|
||||||
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
entities = std::make_shared<std::list<std::shared_ptr<UIEntity>>>();
|
||||||
|
|
@ -43,7 +45,7 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _x
|
||||||
zoom(1.0f),
|
zoom(1.0f),
|
||||||
ptex(_ptex),
|
ptex(_ptex),
|
||||||
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
fill_color(8, 8, 8, 255), tcod_map(nullptr), tcod_dijkstra(nullptr), tcod_path(nullptr),
|
||||||
perspective_enabled(false),
|
perspective_enabled(false), fov_algorithm(FOV_BASIC), fov_radius(10),
|
||||||
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
|
use_chunks(gx > CHUNK_THRESHOLD || gy > CHUNK_THRESHOLD) // #123 - Use chunks for large grids
|
||||||
{
|
{
|
||||||
// Use texture dimensions if available, otherwise use defaults
|
// Use texture dimensions if available, otherwise use defaults
|
||||||
|
|
@ -1275,6 +1277,62 @@ int UIGrid::set_perspective_enabled(PyUIGridObject* self, PyObject* value, void*
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #114 - FOV algorithm property
|
||||||
|
PyObject* UIGrid::get_fov(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
// Return the FOV enum member for the current algorithm
|
||||||
|
if (PyFOV::fov_enum_class) {
|
||||||
|
// Get the enum member by value
|
||||||
|
PyObject* value = PyLong_FromLong(self->data->fov_algorithm);
|
||||||
|
if (!value) return NULL;
|
||||||
|
|
||||||
|
// Call FOV(value) to get the enum member
|
||||||
|
PyObject* args = PyTuple_Pack(1, value);
|
||||||
|
Py_DECREF(value);
|
||||||
|
if (!args) return NULL;
|
||||||
|
|
||||||
|
PyObject* result = PyObject_Call(PyFOV::fov_enum_class, args, NULL);
|
||||||
|
Py_DECREF(args);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
// Fallback to integer
|
||||||
|
return PyLong_FromLong(self->data->fov_algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_fov(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
TCOD_fov_algorithm_t algo;
|
||||||
|
if (!PyFOV::from_arg(value, &algo, nullptr)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->fov_algorithm = algo;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #114 - FOV radius property
|
||||||
|
PyObject* UIGrid::get_fov_radius(PyUIGridObject* self, void* closure)
|
||||||
|
{
|
||||||
|
return PyLong_FromLong(self->data->fov_radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
|
||||||
|
{
|
||||||
|
if (!PyLong_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fov_radius must be an integer");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
long radius = PyLong_AsLong(value);
|
||||||
|
if (radius == -1 && PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (radius < 0) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "fov_radius must be non-negative");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->fov_radius = (int)radius;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Python API implementations for TCOD functionality
|
// Python API implementations for TCOD functionality
|
||||||
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
|
|
@ -1836,6 +1894,11 @@ PyGetSetDef UIGrid::getsetters[] = {
|
||||||
{"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled,
|
{"perspective_enabled", (getter)UIGrid::get_perspective_enabled, (setter)UIGrid::set_perspective_enabled,
|
||||||
"Whether to use perspective-based FOV rendering. When True with no valid entity, "
|
"Whether to use perspective-based FOV rendering. When True with no valid entity, "
|
||||||
"all cells appear undiscovered.", NULL},
|
"all cells appear undiscovered.", NULL},
|
||||||
|
{"fov", (getter)UIGrid::get_fov, (setter)UIGrid::set_fov,
|
||||||
|
"FOV algorithm for this grid (mcrfpy.FOV enum). "
|
||||||
|
"Used by entity.updateVisibility() and layer methods when fov=None.", NULL},
|
||||||
|
{"fov_radius", (getter)UIGrid::get_fov_radius, (setter)UIGrid::set_fov_radius,
|
||||||
|
"Default FOV radius for this grid. Used when radius not specified.", NULL},
|
||||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
|
||||||
MCRF_PROPERTY(z_index,
|
MCRF_PROPERTY(z_index,
|
||||||
"Z-order for rendering (lower values rendered first). "
|
"Z-order for rendering (lower values rendered first). "
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,10 @@ public:
|
||||||
std::weak_ptr<UIEntity> perspective_entity; // Weak reference to perspective entity
|
std::weak_ptr<UIEntity> perspective_entity; // Weak reference to perspective entity
|
||||||
bool perspective_enabled; // Whether to use perspective rendering
|
bool perspective_enabled; // Whether to use perspective rendering
|
||||||
|
|
||||||
|
// #114 - FOV algorithm and radius for this grid
|
||||||
|
TCOD_fov_algorithm_t fov_algorithm; // Default FOV algorithm (from mcrfpy.default_fov)
|
||||||
|
int fov_radius; // Default FOV radius
|
||||||
|
|
||||||
// #142 - Grid cell mouse events
|
// #142 - Grid cell mouse events
|
||||||
std::unique_ptr<PyClickCallable> on_cell_enter_callable;
|
std::unique_ptr<PyClickCallable> on_cell_enter_callable;
|
||||||
std::unique_ptr<PyClickCallable> on_cell_exit_callable;
|
std::unique_ptr<PyClickCallable> on_cell_exit_callable;
|
||||||
|
|
@ -149,6 +153,10 @@ public:
|
||||||
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
|
static int set_perspective(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_perspective_enabled(PyUIGridObject* self, void* closure);
|
static PyObject* get_perspective_enabled(PyUIGridObject* self, void* closure);
|
||||||
static int set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure);
|
static int set_perspective_enabled(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_fov(PyUIGridObject* self, void* closure);
|
||||||
|
static int set_fov(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
|
static PyObject* get_fov_radius(PyUIGridObject* self, void* closure);
|
||||||
|
static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
|
static PyObject* py_is_in_fov(PyUIGridObject* self, PyObject* args);
|
||||||
|
|
|
||||||
|
|
@ -37,13 +37,13 @@ def run_tests():
|
||||||
# Test 3: Field of View
|
# Test 3: Field of View
|
||||||
print("Test 3: Field of View Algorithms")
|
print("Test 3: Field of View Algorithms")
|
||||||
|
|
||||||
# Test different algorithms
|
# Test different algorithms (using new mcrfpy.FOV enum)
|
||||||
algorithms = [
|
algorithms = [
|
||||||
("Basic", mcrfpy.FOV_BASIC),
|
("Basic", mcrfpy.FOV.BASIC),
|
||||||
("Diamond", mcrfpy.FOV_DIAMOND),
|
("Diamond", mcrfpy.FOV.DIAMOND),
|
||||||
("Shadow", mcrfpy.FOV_SHADOW),
|
("Shadow", mcrfpy.FOV.SHADOW),
|
||||||
("Permissive", mcrfpy.FOV_PERMISSIVE_2),
|
("Permissive", mcrfpy.FOV.PERMISSIVE_2),
|
||||||
("Restrictive", mcrfpy.FOV_RESTRICTIVE)
|
("Restrictive", mcrfpy.FOV.RESTRICTIVE)
|
||||||
]
|
]
|
||||||
|
|
||||||
for name, algo in algorithms:
|
for name, algo in algorithms:
|
||||||
|
|
|
||||||
|
|
@ -7,190 +7,142 @@ Demonstrates:
|
||||||
1. Grid with obstacles (walls)
|
1. Grid with obstacles (walls)
|
||||||
2. Two entities at different positions
|
2. Two entities at different positions
|
||||||
3. Entity-specific FOV calculation
|
3. Entity-specific FOV calculation
|
||||||
4. Visual representation of visible/discovered areas
|
4. Color layer for FOV visualization (new API)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import mcrfpy
|
import mcrfpy
|
||||||
from mcrfpy import libtcod
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Constants
|
def run_tests():
|
||||||
WALL_SPRITE = 219 # Full block character
|
"""Run FOV entity tests"""
|
||||||
PLAYER_SPRITE = 64 # @ symbol
|
print("=== TCOD FOV Entity Tests ===\n")
|
||||||
ENEMY_SPRITE = 69 # E character
|
|
||||||
FLOOR_SPRITE = 46 # . period
|
|
||||||
|
|
||||||
def setup_scene():
|
# Test 1: FOV enum availability
|
||||||
"""Create the demo scene with grid and entities"""
|
print("Test 1: FOV Enum")
|
||||||
mcrfpy.createScene("fov_demo")
|
try:
|
||||||
|
print(f" FOV.BASIC = {mcrfpy.FOV.BASIC}")
|
||||||
|
print(f" FOV.SHADOW = {mcrfpy.FOV.SHADOW}")
|
||||||
|
print("✓ FOV enum available\n")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ FOV enum not available: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
# Create grid
|
# Test 2: Create grid with walls
|
||||||
grid = mcrfpy.Grid(0, 0, grid_size=(40, 25))
|
print("Test 2: Grid Creation with Walls")
|
||||||
grid.background_color = mcrfpy.Color(20, 20, 20)
|
grid = mcrfpy.Grid(pos=(0, 0), size=(640, 400), grid_size=(40, 25))
|
||||||
|
|
||||||
# Initialize all cells as floor
|
# Set up walls
|
||||||
for y in range(grid.grid_y):
|
for y in range(25):
|
||||||
for x in range(grid.grid_x):
|
for x in range(40):
|
||||||
cell = grid.at(x, y)
|
point = grid.at(x, y)
|
||||||
cell.walkable = True
|
# Border walls
|
||||||
cell.transparent = True
|
if x == 0 or x == 39 or y == 0 or y == 24:
|
||||||
cell.tilesprite = FLOOR_SPRITE
|
point.walkable = False
|
||||||
cell.color = mcrfpy.Color(50, 50, 50)
|
point.transparent = False
|
||||||
|
# Central wall
|
||||||
|
elif x == 20 and y != 12: # Wall with door at y=12
|
||||||
|
point.walkable = False
|
||||||
|
point.transparent = False
|
||||||
|
else:
|
||||||
|
point.walkable = True
|
||||||
|
point.transparent = True
|
||||||
|
|
||||||
# Create walls (horizontal wall)
|
print("✓ Grid with walls created\n")
|
||||||
for x in range(10, 30):
|
|
||||||
cell = grid.at(x, 10)
|
|
||||||
cell.walkable = False
|
|
||||||
cell.transparent = False
|
|
||||||
cell.tilesprite = WALL_SPRITE
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Create walls (vertical wall)
|
# Test 3: Create entities
|
||||||
for y in range(5, 20):
|
print("Test 3: Entity Creation")
|
||||||
cell = grid.at(20, y)
|
player = mcrfpy.Entity((5, 12))
|
||||||
cell.walkable = False
|
enemy = mcrfpy.Entity((35, 12))
|
||||||
cell.transparent = False
|
|
||||||
cell.tilesprite = WALL_SPRITE
|
|
||||||
cell.color = mcrfpy.Color(100, 100, 100)
|
|
||||||
|
|
||||||
# Add door gaps
|
|
||||||
grid.at(15, 10).walkable = True
|
|
||||||
grid.at(15, 10).transparent = True
|
|
||||||
grid.at(15, 10).tilesprite = FLOOR_SPRITE
|
|
||||||
|
|
||||||
grid.at(20, 15).walkable = True
|
|
||||||
grid.at(20, 15).transparent = True
|
|
||||||
grid.at(20, 15).tilesprite = FLOOR_SPRITE
|
|
||||||
|
|
||||||
# Create two entities
|
|
||||||
player = mcrfpy.Entity(5, 5)
|
|
||||||
player.sprite = PLAYER_SPRITE
|
|
||||||
grid.entities.append(player)
|
grid.entities.append(player)
|
||||||
|
|
||||||
enemy = mcrfpy.Entity(35, 20)
|
|
||||||
enemy.sprite = ENEMY_SPRITE
|
|
||||||
grid.entities.append(enemy)
|
grid.entities.append(enemy)
|
||||||
|
print(f" Player at ({player.x}, {player.y})")
|
||||||
|
print(f" Enemy at ({enemy.x}, {enemy.y})")
|
||||||
|
print("✓ Entities created\n")
|
||||||
|
|
||||||
# Add grid to scene
|
# Test 4: FOV calculation for player
|
||||||
ui = mcrfpy.sceneUI("fov_demo")
|
print("Test 4: Player FOV Calculation")
|
||||||
ui.append(grid)
|
grid.compute_fov(int(player.x), int(player.y), radius=15, algorithm=mcrfpy.FOV.SHADOW)
|
||||||
|
|
||||||
# Add info text
|
# Player should see themselves
|
||||||
info = mcrfpy.Caption("TCOD FOV Demo - Blue: Player FOV, Red: Enemy FOV", 10, 430)
|
assert grid.is_in_fov(int(player.x), int(player.y)), "Player should see themselves"
|
||||||
info.fill_color = mcrfpy.Color(255, 255, 255)
|
print(" Player can see their own position")
|
||||||
ui.append(info)
|
|
||||||
|
|
||||||
controls = mcrfpy.Caption("Arrow keys: Move player | Q: Quit", 10, 450)
|
# Player should see nearby cells
|
||||||
controls.fill_color = mcrfpy.Color(200, 200, 200)
|
assert grid.is_in_fov(6, 12), "Player should see adjacent cells"
|
||||||
ui.append(controls)
|
print(" Player can see adjacent cells")
|
||||||
|
|
||||||
return grid, player, enemy
|
# Player should NOT see behind the wall (outside door line)
|
||||||
|
assert not grid.is_in_fov(21, 5), "Player should not see behind wall"
|
||||||
|
print(" Player cannot see behind wall at (21, 5)")
|
||||||
|
|
||||||
def update_fov(grid, player, enemy):
|
# Player should NOT see enemy (behind wall even with door)
|
||||||
"""Update field of view for both entities"""
|
assert not grid.is_in_fov(int(enemy.x), int(enemy.y)), "Player should not see enemy"
|
||||||
# Clear all overlays first
|
print(" Player cannot see enemy")
|
||||||
for y in range(grid.grid_y):
|
|
||||||
for x in range(grid.grid_x):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
cell.color_overlay = mcrfpy.Color(0, 0, 0, 200) # Dark by default
|
|
||||||
|
|
||||||
# Compute and display player FOV (blue tint)
|
print("✓ Player FOV working correctly\n")
|
||||||
grid.compute_fov(player.x, player.y, radius=10, algorithm=libtcod.FOV_SHADOW)
|
|
||||||
for y in range(grid.grid_y):
|
|
||||||
for x in range(grid.grid_x):
|
|
||||||
if grid.is_in_fov(x, y):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
cell.color_overlay = mcrfpy.Color(100, 100, 255, 50) # Light blue
|
|
||||||
|
|
||||||
# Compute and display enemy FOV (red tint)
|
# Test 5: FOV calculation for enemy
|
||||||
grid.compute_fov(enemy.x, enemy.y, radius=8, algorithm=libtcod.FOV_SHADOW)
|
print("Test 5: Enemy FOV Calculation")
|
||||||
for y in range(grid.grid_y):
|
grid.compute_fov(int(enemy.x), int(enemy.y), radius=15, algorithm=mcrfpy.FOV.SHADOW)
|
||||||
for x in range(grid.grid_x):
|
|
||||||
if grid.is_in_fov(x, y):
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
# Mix colors if both can see
|
|
||||||
if cell.color_overlay.r > 0 or cell.color_overlay.g > 0 or cell.color_overlay.b > 200:
|
|
||||||
# Already blue, make purple
|
|
||||||
cell.color_overlay = mcrfpy.Color(255, 100, 255, 50)
|
|
||||||
else:
|
|
||||||
# Just red
|
|
||||||
cell.color_overlay = mcrfpy.Color(255, 100, 100, 50)
|
|
||||||
|
|
||||||
def test_pathfinding(grid, player, enemy):
|
# Enemy should see themselves
|
||||||
"""Test pathfinding between entities"""
|
assert grid.is_in_fov(int(enemy.x), int(enemy.y)), "Enemy should see themselves"
|
||||||
path = grid.find_path(player.x, player.y, enemy.x, enemy.y)
|
print(" Enemy can see their own position")
|
||||||
|
|
||||||
if path:
|
# Enemy should NOT see player (behind wall)
|
||||||
print(f"Path found from player to enemy: {len(path)} steps")
|
assert not grid.is_in_fov(int(player.x), int(player.y)), "Enemy should not see player"
|
||||||
# Highlight path
|
print(" Enemy cannot see player")
|
||||||
for x, y in path[1:-1]: # Skip start and end
|
|
||||||
cell = grid.at(x, y)
|
|
||||||
if cell.walkable:
|
|
||||||
cell.tile_overlay = 43 # + symbol
|
|
||||||
else:
|
|
||||||
print("No path found between player and enemy")
|
|
||||||
|
|
||||||
def handle_keypress(scene_name, keycode):
|
print("✓ Enemy FOV working correctly\n")
|
||||||
"""Handle keyboard input"""
|
|
||||||
if keycode == 81 or keycode == 256: # Q or ESC
|
|
||||||
print("\nExiting FOV demo...")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# Get entities (assumes global access for demo)
|
# Test 6: FOV with color layer
|
||||||
if keycode == 265: # UP
|
print("Test 6: FOV Color Layer Visualization")
|
||||||
if player.y > 0 and grid.at(player.x, player.y - 1).walkable:
|
fov_layer = grid.add_layer('color', z_index=-1)
|
||||||
player.y -= 1
|
fov_layer.fill((0, 0, 0, 255)) # Start with black (unknown)
|
||||||
elif keycode == 264: # DOWN
|
|
||||||
if player.y < grid.grid_y - 1 and grid.at(player.x, player.y + 1).walkable:
|
|
||||||
player.y += 1
|
|
||||||
elif keycode == 263: # LEFT
|
|
||||||
if player.x > 0 and grid.at(player.x - 1, player.y).walkable:
|
|
||||||
player.x -= 1
|
|
||||||
elif keycode == 262: # RIGHT
|
|
||||||
if player.x < grid.grid_x - 1 and grid.at(player.x + 1, player.y).walkable:
|
|
||||||
player.x += 1
|
|
||||||
|
|
||||||
# Update FOV after movement
|
# Draw player FOV
|
||||||
update_fov(grid, player, enemy)
|
fov_layer.draw_fov(
|
||||||
test_pathfinding(grid, player, enemy)
|
source=(int(player.x), int(player.y)),
|
||||||
|
radius=10,
|
||||||
|
fov=mcrfpy.FOV.SHADOW,
|
||||||
|
visible=(255, 255, 200, 64),
|
||||||
|
discovered=(100, 100, 100, 128),
|
||||||
|
unknown=(0, 0, 0, 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check visible cell
|
||||||
|
visible_cell = fov_layer.at(int(player.x), int(player.y))
|
||||||
|
assert visible_cell.r == 255, "Player position should be visible"
|
||||||
|
print(" Player position has visible color")
|
||||||
|
|
||||||
|
# Check hidden cell (behind wall)
|
||||||
|
hidden_cell = fov_layer.at(int(enemy.x), int(enemy.y))
|
||||||
|
assert hidden_cell.r == 0, "Enemy position should be unknown"
|
||||||
|
print(" Enemy position has unknown color")
|
||||||
|
|
||||||
|
print("✓ FOV color layer working correctly\n")
|
||||||
|
|
||||||
|
# Test 7: Line of sight via libtcod
|
||||||
|
print("Test 7: Line Drawing")
|
||||||
|
line = mcrfpy.libtcod.line(int(player.x), int(player.y), int(enemy.x), int(enemy.y))
|
||||||
|
print(f" Line from player to enemy: {len(line)} cells")
|
||||||
|
assert len(line) > 0, "Line should have cells"
|
||||||
|
print("✓ Line drawing working\n")
|
||||||
|
|
||||||
|
print("=== All FOV Entity Tests Passed! ===")
|
||||||
|
return True
|
||||||
|
|
||||||
# Main execution
|
# Main execution
|
||||||
print("McRogueFace TCOD FOV Demo")
|
if __name__ == "__main__":
|
||||||
print("=========================")
|
try:
|
||||||
print("Testing mcrfpy.libtcod module...")
|
if run_tests():
|
||||||
|
print("\nPASS")
|
||||||
# Test that libtcod module exists
|
sys.exit(0)
|
||||||
try:
|
else:
|
||||||
print(f"libtcod module: {libtcod}")
|
print("\nFAIL")
|
||||||
print(f"FOV constants: FOV_BASIC={libtcod.FOV_BASIC}, FOV_SHADOW={libtcod.FOV_SHADOW}")
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"ERROR: Could not access libtcod module: {e}")
|
print(f"\nFAIL: {e}")
|
||||||
sys.exit(1)
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
# Create scene
|
sys.exit(1)
|
||||||
grid, player, enemy = setup_scene()
|
|
||||||
|
|
||||||
# Make these global for keypress handler (demo only)
|
|
||||||
globals()['grid'] = grid
|
|
||||||
globals()['player'] = player
|
|
||||||
globals()['enemy'] = enemy
|
|
||||||
|
|
||||||
# Initial FOV calculation
|
|
||||||
update_fov(grid, player, enemy)
|
|
||||||
|
|
||||||
# Test pathfinding
|
|
||||||
test_pathfinding(grid, player, enemy)
|
|
||||||
|
|
||||||
# Test line drawing
|
|
||||||
line = libtcod.line(player.x, player.y, enemy.x, enemy.y)
|
|
||||||
print(f"Line from player to enemy: {len(line)} cells")
|
|
||||||
|
|
||||||
# Set up input handling
|
|
||||||
mcrfpy.keypressScene(handle_keypress)
|
|
||||||
|
|
||||||
# Show the scene
|
|
||||||
mcrfpy.setScene("fov_demo")
|
|
||||||
|
|
||||||
print("\nFOV demo running. Use arrow keys to move player (@)")
|
|
||||||
print("Blue areas are visible to player, red to enemy, purple to both")
|
|
||||||
print("Press Q to quit")
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue