1220 lines
44 KiB
C++
1220 lines
44 KiB
C++
#include "UIEntity.h"
|
|
#include "UIGrid.h"
|
|
#include "McRFPy_API.h"
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <libtcod.h>
|
|
#include "PyObjectUtils.h"
|
|
#include "PyVector.h"
|
|
#include "PythonObjectCache.h"
|
|
#include "PyFOV.h"
|
|
#include "Animation.h"
|
|
#include "PyAnimation.h"
|
|
#include "PyEasing.h"
|
|
#include "PyPositionHelper.h"
|
|
// UIDrawable methods now in UIBase.h
|
|
#include "UIEntityPyMethods.h"
|
|
|
|
|
|
|
|
UIEntity::UIEntity()
|
|
: self(nullptr), grid(nullptr), position(0.0f, 0.0f)
|
|
{
|
|
// Initialize sprite with safe defaults (sprite has its own safe constructor now)
|
|
// gridstate vector starts empty - will be lazily initialized when needed
|
|
}
|
|
|
|
UIEntity::~UIEntity() {
|
|
if (serial_number != 0) {
|
|
PythonObjectCache::getInstance().remove(serial_number);
|
|
}
|
|
}
|
|
|
|
// Removed UIEntity(UIGrid&) constructor - using lazy initialization instead
|
|
|
|
void UIEntity::updateVisibility()
|
|
{
|
|
if (!grid) return;
|
|
|
|
// Lazy initialize gridstate if needed
|
|
if (gridstate.size() == 0) {
|
|
gridstate.resize(grid->grid_w * grid->grid_h);
|
|
// Initialize all cells as not visible/discovered
|
|
for (auto& state : gridstate) {
|
|
state.visible = false;
|
|
state.discovered = false;
|
|
}
|
|
}
|
|
|
|
// First, mark all cells as not visible
|
|
for (auto& state : gridstate) {
|
|
state.visible = false;
|
|
}
|
|
|
|
// Compute FOV from entity's position using grid's FOV settings (#114)
|
|
int x = static_cast<int>(position.x);
|
|
int y = static_cast<int>(position.y);
|
|
|
|
// Use grid's configured FOV algorithm and radius
|
|
grid->computeFOV(x, y, grid->fov_radius, true, grid->fov_algorithm);
|
|
|
|
// Update visible cells based on FOV computation
|
|
for (int gy = 0; gy < grid->grid_h; gy++) {
|
|
for (int gx = 0; gx < grid->grid_w; gx++) {
|
|
int idx = gy * grid->grid_w + gx;
|
|
if (grid->isInFOV(gx, gy)) {
|
|
gridstate[idx].visible = true;
|
|
gridstate[idx].discovered = true; // Once seen, always discovered
|
|
}
|
|
}
|
|
}
|
|
|
|
// #113 - Update any ColorLayers bound to this entity via perspective
|
|
// Get shared_ptr to self for comparison
|
|
std::shared_ptr<UIEntity> self_ptr = nullptr;
|
|
if (grid->entities) {
|
|
for (auto& entity : *grid->entities) {
|
|
if (entity.get() == this) {
|
|
self_ptr = entity;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (self_ptr) {
|
|
for (auto& layer : grid->layers) {
|
|
if (layer->type == GridLayerType::Color) {
|
|
auto color_layer = std::static_pointer_cast<ColorLayer>(layer);
|
|
if (color_layer->has_perspective) {
|
|
auto bound_entity = color_layer->perspective_entity.lock();
|
|
if (bound_entity && bound_entity.get() == this) {
|
|
color_layer->updatePerspective();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|
int x, y;
|
|
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
|
|
return NULL; // Error already set by PyPosition_ParseInt
|
|
}
|
|
|
|
if (self->data->grid == NULL) {
|
|
PyErr_SetString(PyExc_ValueError, "Entity cannot access surroundings because it is not associated with a grid");
|
|
return NULL;
|
|
}
|
|
|
|
// Lazy initialize gridstate if needed
|
|
if (self->data->gridstate.size() == 0) {
|
|
self->data->gridstate.resize(self->data->grid->grid_w * self->data->grid->grid_h);
|
|
// Initialize all cells as not visible/discovered
|
|
for (auto& state : self->data->gridstate) {
|
|
state.visible = false;
|
|
state.discovered = false;
|
|
}
|
|
}
|
|
|
|
// Bounds check
|
|
if (x < 0 || x >= self->data->grid->grid_w || y < 0 || y >= self->data->grid->grid_h) {
|
|
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y);
|
|
return NULL;
|
|
}
|
|
|
|
// Use type directly since GridPointState is internal-only (not exported to module)
|
|
auto type = &mcrfpydef::PyUIGridPointStateType;
|
|
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
|
|
obj->data = &(self->data->gridstate[y * self->data->grid->grid_w + x]);
|
|
obj->grid = self->data->grid;
|
|
obj->entity = self->data;
|
|
obj->x = x; // #16 - Store position for .point property
|
|
obj->y = y;
|
|
return (PyObject*)obj;
|
|
}
|
|
|
|
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
|
|
// Check if entity has an associated grid
|
|
if (!self->data || !self->data->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Entity is not associated with a grid");
|
|
return NULL;
|
|
}
|
|
|
|
// Get the grid's entity collection
|
|
auto entities = self->data->grid->entities;
|
|
if (!entities) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Grid has no entity collection");
|
|
return NULL;
|
|
}
|
|
|
|
// Find this entity in the collection
|
|
int index = 0;
|
|
for (auto it = entities->begin(); it != entities->end(); ++it, ++index) {
|
|
if (it->get() == self->data.get()) {
|
|
return PyLong_FromLong(index);
|
|
}
|
|
}
|
|
|
|
// Entity not found in its grid's collection
|
|
PyErr_SetString(PyExc_ValueError, "Entity not found in its grid's entity collection");
|
|
return NULL;
|
|
}
|
|
|
|
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|
// Define all parameters with defaults
|
|
PyObject* grid_pos_obj = nullptr;
|
|
PyObject* texture = nullptr;
|
|
int sprite_index = 0;
|
|
PyObject* grid_obj = nullptr;
|
|
int visible = 1;
|
|
float opacity = 1.0f;
|
|
const char* name = nullptr;
|
|
float x = 0.0f, y = 0.0f;
|
|
|
|
// Keywords list matches the new spec: positional args first, then all keyword args
|
|
static const char* kwlist[] = {
|
|
"grid_pos", "texture", "sprite_index", // Positional args (as per spec)
|
|
// Keyword-only args
|
|
"grid", "visible", "opacity", "name", "x", "y",
|
|
nullptr
|
|
};
|
|
|
|
// Parse arguments with | for optional positional args
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiOifzff", const_cast<char**>(kwlist),
|
|
&grid_pos_obj, &texture, &sprite_index, // Positional
|
|
&grid_obj, &visible, &opacity, &name, &x, &y)) {
|
|
return -1;
|
|
}
|
|
|
|
// Handle grid position argument (can be tuple or use x/y keywords)
|
|
if (grid_pos_obj) {
|
|
if (PyTuple_Check(grid_pos_obj) && PyTuple_Size(grid_pos_obj) == 2) {
|
|
PyObject* x_val = PyTuple_GetItem(grid_pos_obj, 0);
|
|
PyObject* y_val = PyTuple_GetItem(grid_pos_obj, 1);
|
|
if ((PyFloat_Check(x_val) || PyLong_Check(x_val)) &&
|
|
(PyFloat_Check(y_val) || PyLong_Check(y_val))) {
|
|
x = PyFloat_Check(x_val) ? PyFloat_AsDouble(x_val) : PyLong_AsLong(x_val);
|
|
y = PyFloat_Check(y_val) ? PyFloat_AsDouble(y_val) : PyLong_AsLong(y_val);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos tuple must contain numbers");
|
|
return -1;
|
|
}
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "grid_pos must be a tuple (x, y)");
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// Handle texture argument
|
|
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
|
if (texture && texture != Py_None) {
|
|
if (!PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
|
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
|
return -1;
|
|
}
|
|
auto pytexture = (PyTextureObject*)texture;
|
|
texture_ptr = pytexture->data;
|
|
} else {
|
|
// Use default texture when None or not provided
|
|
texture_ptr = McRFPy_API::default_texture;
|
|
}
|
|
|
|
// Handle grid argument
|
|
if (grid_obj && !PyObject_IsInstance(grid_obj, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
|
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
|
return -1;
|
|
}
|
|
|
|
// Create the entity
|
|
self->data = std::make_shared<UIEntity>();
|
|
|
|
// Initialize weak reference list
|
|
self->weakreflist = NULL;
|
|
|
|
// Register in Python object cache
|
|
if (self->data->serial_number == 0) {
|
|
self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
|
|
PyObject* weakref = PyWeakref_NewRef((PyObject*)self, NULL);
|
|
if (weakref) {
|
|
PythonObjectCache::getInstance().registerObject(self->data->serial_number, weakref);
|
|
Py_DECREF(weakref); // Cache owns the reference now
|
|
}
|
|
}
|
|
|
|
// Store reference to Python object (legacy - to be removed)
|
|
self->data->self = (PyObject*)self;
|
|
Py_INCREF(self);
|
|
|
|
// Set texture and sprite index
|
|
if (texture_ptr) {
|
|
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
|
|
} else {
|
|
// Create an empty sprite for testing
|
|
self->data->sprite = UISprite();
|
|
}
|
|
|
|
// Set position using grid coordinates
|
|
self->data->position = sf::Vector2f(x, y);
|
|
|
|
// Set other properties (delegate to sprite)
|
|
self->data->sprite.visible = visible;
|
|
self->data->sprite.opacity = opacity;
|
|
if (name) {
|
|
self->data->sprite.name = std::string(name);
|
|
}
|
|
|
|
// Handle grid attachment
|
|
if (grid_obj) {
|
|
PyUIGridObject* pygrid = (PyUIGridObject*)grid_obj;
|
|
self->data->grid = pygrid->data;
|
|
// Append entity to grid's entity list
|
|
pygrid->data->entities->push_back(self->data);
|
|
|
|
// Don't initialize gridstate here - lazy initialization to support large numbers of entities
|
|
// gridstate will be initialized when visibility is updated or accessed
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
PyObject* UIEntity::get_spritenumber(PyUIEntityObject* self, void* closure) {
|
|
return PyLong_FromDouble(self->data->sprite.getSpriteIndex());
|
|
}
|
|
|
|
PyObject* sfVector2f_to_PyObject(sf::Vector2f vec) {
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
|
if (obj) {
|
|
obj->data = vec;
|
|
}
|
|
return (PyObject*)obj;
|
|
}
|
|
|
|
PyObject* sfVector2i_to_PyObject(sf::Vector2i vec) {
|
|
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
|
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
|
if (obj) {
|
|
obj->data = sf::Vector2f(static_cast<float>(vec.x), static_cast<float>(vec.y));
|
|
}
|
|
return (PyObject*)obj;
|
|
}
|
|
|
|
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj) {
|
|
PyVectorObject* vec = PyVector::from_arg(obj);
|
|
if (!vec) {
|
|
// PyVector::from_arg already set the error
|
|
return sf::Vector2f(0, 0);
|
|
}
|
|
return vec->data;
|
|
}
|
|
|
|
sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
|
|
PyVectorObject* vec = PyVector::from_arg(obj);
|
|
if (!vec) {
|
|
// PyVector::from_arg already set the error
|
|
return sf::Vector2i(0, 0);
|
|
}
|
|
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
|
|
}
|
|
|
|
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state) {
|
|
// Create a new GridPointState Python object (detached - no grid/entity context)
|
|
// Use type directly since GridPointState is internal-only (not exported to module)
|
|
auto type = &mcrfpydef::PyUIGridPointStateType;
|
|
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
|
|
if (!obj) {
|
|
return NULL;
|
|
}
|
|
|
|
// Allocate new data and copy values
|
|
obj->data = new UIGridPointState();
|
|
obj->data->visible = state.visible;
|
|
obj->data->discovered = state.discovered;
|
|
|
|
// Initialize context fields (detached state has no grid/entity context)
|
|
obj->grid = nullptr;
|
|
obj->entity = nullptr;
|
|
obj->x = -1;
|
|
obj->y = -1;
|
|
|
|
return (PyObject*)obj;
|
|
}
|
|
|
|
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec) {
|
|
PyObject* list = PyList_New(vec.size());
|
|
if (!list) return PyErr_NoMemory();
|
|
|
|
for (size_t i = 0; i < vec.size(); ++i) {
|
|
PyObject* obj = UIGridPointState_to_PyObject(vec[i]);
|
|
if (!obj) { // Cleanup on failure
|
|
Py_DECREF(list);
|
|
return NULL;
|
|
}
|
|
PyList_SET_ITEM(list, i, obj); // This steals a reference to obj
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) {
|
|
if (reinterpret_cast<intptr_t>(closure) == 0) {
|
|
return sfVector2f_to_PyObject(self->data->position);
|
|
} else {
|
|
// Return integer-cast position for grid coordinates
|
|
sf::Vector2i int_pos(static_cast<int>(self->data->position.x),
|
|
static_cast<int>(self->data->position.y));
|
|
return sfVector2i_to_PyObject(int_pos);
|
|
}
|
|
}
|
|
|
|
int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closure) {
|
|
// Save old position for spatial hash update (#115)
|
|
float old_x = self->data->position.x;
|
|
float old_y = self->data->position.y;
|
|
|
|
if (reinterpret_cast<intptr_t>(closure) == 0) {
|
|
sf::Vector2f vec = PyObject_to_sfVector2f(value);
|
|
if (PyErr_Occurred()) {
|
|
return -1; // Error already set by PyObject_to_sfVector2f
|
|
}
|
|
self->data->position = vec;
|
|
} else {
|
|
// For integer position, convert to float and set position
|
|
sf::Vector2i vec = PyObject_to_sfVector2i(value);
|
|
if (PyErr_Occurred()) {
|
|
return -1; // Error already set by PyObject_to_sfVector2i
|
|
}
|
|
self->data->position = sf::Vector2f(static_cast<float>(vec.x),
|
|
static_cast<float>(vec.y));
|
|
}
|
|
|
|
// Update spatial hash if grid exists (#115)
|
|
if (self->data->grid) {
|
|
self->data->grid->spatial_hash.update(self->data, old_x, old_y);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIEntity::get_gridstate(PyUIEntityObject* self, void* closure) {
|
|
// Assuming a function to convert std::vector<UIGridPointState> to PyObject* list
|
|
return UIGridPointStateVector_to_PyList(self->data->gridstate);
|
|
}
|
|
|
|
int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure) {
|
|
int val;
|
|
if (PyLong_Check(value))
|
|
val = PyLong_AsLong(value);
|
|
else
|
|
{
|
|
PyErr_SetString(PyExc_TypeError, "sprite_index must be an integer");
|
|
return -1;
|
|
}
|
|
//self->data->sprite.sprite_index = val;
|
|
self->data->sprite.setSpriteIndex(val); // todone - I don't like ".sprite.sprite" in this stack of UIEntity.UISprite.sf::Sprite
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
|
|
{
|
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
|
if (member_ptr == 0) // x
|
|
return PyFloat_FromDouble(self->data->position.x);
|
|
else if (member_ptr == 1) // y
|
|
return PyFloat_FromDouble(self->data->position.y);
|
|
else
|
|
{
|
|
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure)
|
|
{
|
|
float val;
|
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
|
if (PyFloat_Check(value))
|
|
{
|
|
val = PyFloat_AsDouble(value);
|
|
}
|
|
else if (PyLong_Check(value))
|
|
{
|
|
val = PyLong_AsLong(value);
|
|
}
|
|
else
|
|
{
|
|
PyErr_SetString(PyExc_TypeError, "Position must be a number (int or float)");
|
|
return -1;
|
|
}
|
|
|
|
// Save old position for spatial hash update (#115)
|
|
float old_x = self->data->position.x;
|
|
float old_y = self->data->position.y;
|
|
|
|
if (member_ptr == 0) // x
|
|
{
|
|
self->data->position.x = val;
|
|
}
|
|
else if (member_ptr == 1) // y
|
|
{
|
|
self->data->position.y = val;
|
|
}
|
|
|
|
// Update spatial hash if grid exists (#115)
|
|
if (self->data->grid) {
|
|
self->data->grid->spatial_hash.update(self->data, old_x, old_y);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
// #176 - Helper to get cell dimensions from grid
|
|
static void get_cell_dimensions(UIEntity* entity, float& cell_width, float& cell_height) {
|
|
// Default cell dimensions when no texture
|
|
constexpr float DEFAULT_CELL_WIDTH = 16.0f;
|
|
constexpr float DEFAULT_CELL_HEIGHT = 16.0f;
|
|
|
|
if (entity->grid) {
|
|
auto ptex = entity->grid->getTexture();
|
|
cell_width = ptex ? static_cast<float>(ptex->sprite_width) : DEFAULT_CELL_WIDTH;
|
|
cell_height = ptex ? static_cast<float>(ptex->sprite_height) : DEFAULT_CELL_HEIGHT;
|
|
} else {
|
|
cell_width = DEFAULT_CELL_WIDTH;
|
|
cell_height = DEFAULT_CELL_HEIGHT;
|
|
}
|
|
}
|
|
|
|
// #176 - Pixel position: pos = draw_pos * tile_size
|
|
PyObject* UIEntity::get_pixel_pos(PyUIEntityObject* self, void* closure) {
|
|
if (!self->data->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "entity is not attached to a Grid");
|
|
return NULL;
|
|
}
|
|
|
|
float cell_width, cell_height;
|
|
get_cell_dimensions(self->data.get(), cell_width, cell_height);
|
|
|
|
sf::Vector2f pixel_pos(
|
|
self->data->position.x * cell_width,
|
|
self->data->position.y * cell_height
|
|
);
|
|
return sfVector2f_to_PyObject(pixel_pos);
|
|
}
|
|
|
|
int UIEntity::set_pixel_pos(PyUIEntityObject* self, PyObject* value, void* closure) {
|
|
if (!self->data->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "entity is not attached to a Grid");
|
|
return -1;
|
|
}
|
|
|
|
sf::Vector2f pixel_vec = PyObject_to_sfVector2f(value);
|
|
if (PyErr_Occurred()) {
|
|
return -1;
|
|
}
|
|
|
|
float cell_width, cell_height;
|
|
get_cell_dimensions(self->data.get(), cell_width, cell_height);
|
|
|
|
// Save old position for spatial hash update
|
|
float old_x = self->data->position.x;
|
|
float old_y = self->data->position.y;
|
|
|
|
// Convert pixels to tile coordinates
|
|
self->data->position.x = pixel_vec.x / cell_width;
|
|
self->data->position.y = pixel_vec.y / cell_height;
|
|
|
|
// Update spatial hash
|
|
self->data->grid->spatial_hash.update(self->data, old_x, old_y);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// #176 - Individual pixel coordinates (x, y)
|
|
PyObject* UIEntity::get_pixel_member(PyUIEntityObject* self, void* closure) {
|
|
if (!self->data->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "entity is not attached to a Grid");
|
|
return NULL;
|
|
}
|
|
|
|
float cell_width, cell_height;
|
|
get_cell_dimensions(self->data.get(), cell_width, cell_height);
|
|
|
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
|
if (member_ptr == 0) // x
|
|
return PyFloat_FromDouble(self->data->position.x * cell_width);
|
|
else // y
|
|
return PyFloat_FromDouble(self->data->position.y * cell_height);
|
|
}
|
|
|
|
int UIEntity::set_pixel_member(PyUIEntityObject* self, PyObject* value, void* closure) {
|
|
if (!self->data->grid) {
|
|
PyErr_SetString(PyExc_RuntimeError, "entity is not attached to a Grid");
|
|
return -1;
|
|
}
|
|
|
|
float val;
|
|
if (PyFloat_Check(value)) {
|
|
val = PyFloat_AsDouble(value);
|
|
} else if (PyLong_Check(value)) {
|
|
val = PyLong_AsLong(value);
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Position must be a number (int or float)");
|
|
return -1;
|
|
}
|
|
|
|
float cell_width, cell_height;
|
|
get_cell_dimensions(self->data.get(), cell_width, cell_height);
|
|
|
|
// Save old position for spatial hash update
|
|
float old_x = self->data->position.x;
|
|
float old_y = self->data->position.y;
|
|
|
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
|
if (member_ptr == 0) // x
|
|
self->data->position.x = val / cell_width;
|
|
else // y
|
|
self->data->position.y = val / cell_height;
|
|
|
|
// Update spatial hash
|
|
self->data->grid->spatial_hash.update(self->data, old_x, old_y);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// #176 - Integer grid position (grid_x, grid_y)
|
|
PyObject* UIEntity::get_grid_int_member(PyUIEntityObject* self, void* closure) {
|
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
|
if (member_ptr == 0) // grid_x
|
|
return PyLong_FromLong(static_cast<int>(self->data->position.x));
|
|
else // grid_y
|
|
return PyLong_FromLong(static_cast<int>(self->data->position.y));
|
|
}
|
|
|
|
int UIEntity::set_grid_int_member(PyUIEntityObject* self, PyObject* value, void* closure) {
|
|
int val;
|
|
if (PyLong_Check(value)) {
|
|
val = PyLong_AsLong(value);
|
|
} else if (PyFloat_Check(value)) {
|
|
val = static_cast<int>(PyFloat_AsDouble(value));
|
|
} else {
|
|
PyErr_SetString(PyExc_TypeError, "Grid position must be an integer");
|
|
return -1;
|
|
}
|
|
|
|
// Save old position for spatial hash update
|
|
float old_x = self->data->position.x;
|
|
float old_y = self->data->position.y;
|
|
|
|
auto member_ptr = reinterpret_cast<intptr_t>(closure);
|
|
if (member_ptr == 0) // grid_x
|
|
self->data->position.x = static_cast<float>(val);
|
|
else // grid_y
|
|
self->data->position.y = static_cast<float>(val);
|
|
|
|
// Update spatial hash if grid exists
|
|
if (self->data->grid) {
|
|
self->data->grid->spatial_hash.update(self->data, old_x, old_y);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIEntity::get_grid(PyUIEntityObject* self, void* closure)
|
|
{
|
|
if (!self->data || !self->data->grid) {
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// Return a Python Grid object wrapping the C++ grid
|
|
PyTypeObject* grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
|
if (!grid_type) return nullptr;
|
|
|
|
auto pyGrid = (PyUIGridObject*)grid_type->tp_alloc(grid_type, 0);
|
|
Py_DECREF(grid_type);
|
|
|
|
if (pyGrid) {
|
|
pyGrid->data = self->data->grid;
|
|
pyGrid->weakreflist = NULL;
|
|
}
|
|
return (PyObject*)pyGrid;
|
|
}
|
|
|
|
int UIEntity::set_grid(PyUIEntityObject* self, PyObject* value, void* closure)
|
|
{
|
|
if (!self->data) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object");
|
|
return -1;
|
|
}
|
|
|
|
// Handle None - remove from current grid
|
|
if (value == Py_None) {
|
|
if (self->data->grid) {
|
|
// Remove from current grid's entity list
|
|
auto& entities = self->data->grid->entities;
|
|
auto it = std::find_if(entities->begin(), entities->end(),
|
|
[self](const std::shared_ptr<UIEntity>& e) {
|
|
return e.get() == self->data.get();
|
|
});
|
|
if (it != entities->end()) {
|
|
entities->erase(it);
|
|
}
|
|
self->data->grid.reset();
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Value must be a Grid
|
|
PyTypeObject* grid_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid");
|
|
bool is_grid = grid_type && PyObject_IsInstance(value, (PyObject*)grid_type);
|
|
Py_XDECREF(grid_type);
|
|
|
|
if (!is_grid) {
|
|
PyErr_SetString(PyExc_TypeError, "grid must be a Grid or None");
|
|
return -1;
|
|
}
|
|
|
|
auto new_grid = ((PyUIGridObject*)value)->data;
|
|
|
|
// Remove from old grid first (if any)
|
|
if (self->data->grid && self->data->grid != new_grid) {
|
|
auto& old_entities = self->data->grid->entities;
|
|
auto it = std::find_if(old_entities->begin(), old_entities->end(),
|
|
[self](const std::shared_ptr<UIEntity>& e) {
|
|
return e.get() == self->data.get();
|
|
});
|
|
if (it != old_entities->end()) {
|
|
old_entities->erase(it);
|
|
}
|
|
}
|
|
|
|
// Add to new grid
|
|
if (self->data->grid != new_grid) {
|
|
new_grid->entities->push_back(self->data);
|
|
self->data->grid = new_grid;
|
|
|
|
// Initialize gridstate if needed
|
|
if (self->data->gridstate.size() == 0) {
|
|
self->data->gridstate.resize(new_grid->grid_w * new_grid->grid_h);
|
|
for (auto& state : self->data->gridstate) {
|
|
state.visible = false;
|
|
state.discovered = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
|
{
|
|
// Check if entity has a grid
|
|
if (!self->data || !self->data->grid) {
|
|
Py_RETURN_NONE; // Entity not on a grid, nothing to do
|
|
}
|
|
|
|
// Remove entity from grid's entity list
|
|
auto grid = self->data->grid;
|
|
auto& entities = grid->entities;
|
|
|
|
// Find and remove this entity from the list
|
|
auto it = std::find_if(entities->begin(), entities->end(),
|
|
[self](const std::shared_ptr<UIEntity>& e) {
|
|
return e.get() == self->data.get();
|
|
});
|
|
|
|
if (it != entities->end()) {
|
|
// Remove from spatial hash before erasing (#115)
|
|
grid->spatial_hash.remove(self->data);
|
|
|
|
entities->erase(it);
|
|
// Clear the grid reference
|
|
self->data->grid.reset();
|
|
}
|
|
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|
int target_x, target_y;
|
|
|
|
// Parse position using flexible position helper
|
|
// Supports: path_to(x, y), path_to((x, y)), path_to(pos=(x, y)), path_to(Vector(x, y))
|
|
if (!PyPosition_ParseInt(args, kwds, &target_x, &target_y)) {
|
|
return NULL; // Error already set by PyPosition_ParseInt
|
|
}
|
|
|
|
// Check if entity has a grid
|
|
if (!self->data || !self->data->grid) {
|
|
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to compute paths");
|
|
return NULL;
|
|
}
|
|
|
|
// Get current position
|
|
int current_x = static_cast<int>(self->data->position.x);
|
|
int current_y = static_cast<int>(self->data->position.y);
|
|
|
|
// Validate target position
|
|
auto grid = self->data->grid;
|
|
if (target_x < 0 || target_x >= grid->grid_w || target_y < 0 || target_y >= grid->grid_h) {
|
|
PyErr_Format(PyExc_ValueError, "Target position (%d, %d) is out of grid bounds (0-%d, 0-%d)",
|
|
target_x, target_y, grid->grid_w - 1, grid->grid_h - 1);
|
|
return NULL;
|
|
}
|
|
|
|
// Use A* pathfinding via temporary TCODPath
|
|
TCODPath tcod_path(grid->getTCODMap(), 1.41f);
|
|
if (!tcod_path.compute(current_x, current_y, target_x, target_y)) {
|
|
// No path found - return empty list
|
|
return PyList_New(0);
|
|
}
|
|
|
|
// Convert path to Python list of tuples
|
|
PyObject* path_list = PyList_New(tcod_path.size());
|
|
if (!path_list) return PyErr_NoMemory();
|
|
|
|
for (int i = 0; i < tcod_path.size(); ++i) {
|
|
int px, py;
|
|
tcod_path.get(i, &px, &py);
|
|
|
|
PyObject* coord_tuple = PyTuple_New(2);
|
|
if (!coord_tuple) {
|
|
Py_DECREF(path_list);
|
|
return PyErr_NoMemory();
|
|
}
|
|
|
|
PyTuple_SetItem(coord_tuple, 0, PyLong_FromLong(px));
|
|
PyTuple_SetItem(coord_tuple, 1, PyLong_FromLong(py));
|
|
PyList_SetItem(path_list, i, coord_tuple);
|
|
}
|
|
|
|
return path_list;
|
|
}
|
|
|
|
PyObject* UIEntity::update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
|
|
{
|
|
self->data->updateVisibility();
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyObject* kwds)
|
|
{
|
|
static const char* keywords[] = {"fov", "radius", nullptr};
|
|
PyObject* fov_arg = nullptr;
|
|
int radius = -1; // -1 means use grid default
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|Oi", const_cast<char**>(keywords),
|
|
&fov_arg, &radius)) {
|
|
return NULL;
|
|
}
|
|
|
|
// Check if entity has a grid
|
|
if (!self->data || !self->data->grid) {
|
|
PyErr_SetString(PyExc_ValueError, "Entity must be associated with a grid to find visible entities");
|
|
return NULL;
|
|
}
|
|
|
|
auto grid = self->data->grid;
|
|
|
|
// Parse FOV algorithm - use grid default if not specified
|
|
TCOD_fov_algorithm_t algorithm = grid->fov_algorithm;
|
|
bool fov_was_none = false;
|
|
if (fov_arg && fov_arg != Py_None) {
|
|
if (PyFOV::from_arg(fov_arg, &algorithm, &fov_was_none) < 0) {
|
|
return NULL; // Error already set
|
|
}
|
|
}
|
|
|
|
// Use grid radius if not specified
|
|
if (radius < 0) {
|
|
radius = grid->fov_radius;
|
|
}
|
|
|
|
// Get current position
|
|
int x = static_cast<int>(self->data->position.x);
|
|
int y = static_cast<int>(self->data->position.y);
|
|
|
|
// Compute FOV from this entity's position
|
|
grid->computeFOV(x, y, radius, true, algorithm);
|
|
|
|
// Create result list
|
|
PyObject* result = PyList_New(0);
|
|
if (!result) return PyErr_NoMemory();
|
|
|
|
// Get Entity type for creating Python objects
|
|
PyTypeObject* entity_type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity");
|
|
if (!entity_type) {
|
|
Py_DECREF(result);
|
|
return NULL;
|
|
}
|
|
|
|
// Iterate through all entities in the grid
|
|
if (grid->entities) {
|
|
for (auto& entity : *grid->entities) {
|
|
// Skip self
|
|
if (entity.get() == self->data.get()) {
|
|
continue;
|
|
}
|
|
|
|
// Check if entity is in FOV
|
|
int ex = static_cast<int>(entity->position.x);
|
|
int ey = static_cast<int>(entity->position.y);
|
|
|
|
if (grid->isInFOV(ex, ey)) {
|
|
// Create Python Entity object for this entity
|
|
auto pyEntity = (PyUIEntityObject*)entity_type->tp_alloc(entity_type, 0);
|
|
if (!pyEntity) {
|
|
Py_DECREF(result);
|
|
Py_DECREF(entity_type);
|
|
return PyErr_NoMemory();
|
|
}
|
|
|
|
pyEntity->data = entity;
|
|
pyEntity->weakreflist = NULL;
|
|
|
|
if (PyList_Append(result, (PyObject*)pyEntity) < 0) {
|
|
Py_DECREF(pyEntity);
|
|
Py_DECREF(result);
|
|
Py_DECREF(entity_type);
|
|
return NULL;
|
|
}
|
|
Py_DECREF(pyEntity); // List now owns the reference
|
|
}
|
|
}
|
|
}
|
|
|
|
Py_DECREF(entity_type);
|
|
return result;
|
|
}
|
|
|
|
PyMethodDef UIEntity::methods[] = {
|
|
{"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
|
|
"at(x, y) or at(pos) -> GridPointState\n\n"
|
|
"Get the grid point state at the specified position.\n\n"
|
|
"Args:\n"
|
|
" x, y: Grid coordinates as two integers, OR\n"
|
|
" pos: Grid coordinates as tuple, list, or Vector\n\n"
|
|
"Returns:\n"
|
|
" GridPointState for the entity's view of that grid cell.\n\n"
|
|
"Example:\n"
|
|
" state = entity.at(5, 3)\n"
|
|
" state = entity.at((5, 3))\n"
|
|
" state = entity.at(pos=(5, 3))"},
|
|
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
|
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
|
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
|
"path_to(x, y) or path_to(target) -> list\n\n"
|
|
"Find a path to the target position using Dijkstra pathfinding.\n\n"
|
|
"Args:\n"
|
|
" x, y: Target coordinates as two integers, OR\n"
|
|
" target: Target coordinates as tuple, list, or Vector\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing the path.\n\n"
|
|
"Example:\n"
|
|
" path = entity.path_to(10, 5)\n"
|
|
" path = entity.path_to((10, 5))\n"
|
|
" path = entity.path_to(pos=(10, 5))"},
|
|
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
|
|
"update_visibility() -> None\n\n"
|
|
"Update entity's visibility state based on current FOV.\n\n"
|
|
"Recomputes which cells are visible from the entity's position and updates\n"
|
|
"the entity's gridstate to track explored areas. This is called automatically\n"
|
|
"when the entity moves if it has a grid with perspective set."},
|
|
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
|
|
"visible_entities(fov=None, radius=None) -> list[Entity]\n\n"
|
|
"Get list of other entities visible from this entity's position.\n\n"
|
|
"Args:\n"
|
|
" fov (FOV, optional): FOV algorithm to use. Default: grid.fov\n"
|
|
" radius (int, optional): FOV radius. Default: grid.fov_radius\n\n"
|
|
"Returns:\n"
|
|
" List of Entity objects that are within field of view.\n\n"
|
|
"Computes FOV from this entity's position and returns all other entities\n"
|
|
"whose positions fall within the visible area."},
|
|
{NULL, NULL, 0, NULL}
|
|
};
|
|
|
|
// Define the PyObjectType alias for the macros
|
|
typedef PyUIEntityObject PyObjectType;
|
|
|
|
// Combine base methods with entity-specific methods
|
|
// Note: Use UIDRAWABLE_METHODS_BASE (not UIDRAWABLE_METHODS) because UIEntity is NOT a UIDrawable
|
|
// and the template-based animate helper won't work. Entity has its own animate() method.
|
|
PyMethodDef UIEntity_all_methods[] = {
|
|
UIDRAWABLE_METHODS_BASE,
|
|
{"animate", (PyCFunction)UIEntity::animate, METH_VARARGS | METH_KEYWORDS,
|
|
MCRF_METHOD(Entity, animate,
|
|
MCRF_SIG("(property: str, target: Any, duration: float, easing=None, delta=False, callback=None, conflict_mode='replace')", "Animation"),
|
|
MCRF_DESC("Create and start an animation on this entity's property."),
|
|
MCRF_ARGS_START
|
|
MCRF_ARG("property", "Name of the property to animate: 'draw_x', 'draw_y' (tile coords), 'sprite_scale', 'sprite_index'")
|
|
MCRF_ARG("target", "Target value - float or int depending on property")
|
|
MCRF_ARG("duration", "Animation duration in seconds")
|
|
MCRF_ARG("easing", "Easing function: Easing enum value, string name, or None for linear")
|
|
MCRF_ARG("delta", "If True, target is relative to current value; if False, target is absolute")
|
|
MCRF_ARG("callback", "Optional callable invoked when animation completes")
|
|
MCRF_ARG("conflict_mode", "'replace' (default), 'queue', or 'error' if property already animating")
|
|
MCRF_RETURNS("Animation object for monitoring progress")
|
|
MCRF_RAISES("ValueError", "If property name is not valid for Entity (draw_x, draw_y, sprite_scale, sprite_index)")
|
|
MCRF_NOTE("Use 'draw_x'/'draw_y' to animate tile coordinates for smooth movement between grid cells.")
|
|
)},
|
|
{"at", (PyCFunction)UIEntity::at, METH_VARARGS | METH_KEYWORDS,
|
|
"at(x, y) or at(pos) -> GridPointState\n\n"
|
|
"Get the grid point state at the specified position.\n\n"
|
|
"Args:\n"
|
|
" x, y: Grid coordinates as two integers, OR\n"
|
|
" pos: Grid coordinates as tuple, list, or Vector\n\n"
|
|
"Returns:\n"
|
|
" GridPointState for the entity's view of that grid cell.\n\n"
|
|
"Example:\n"
|
|
" state = entity.at(5, 3)\n"
|
|
" state = entity.at((5, 3))\n"
|
|
" state = entity.at(pos=(5, 3))"},
|
|
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
|
{"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"},
|
|
{"path_to", (PyCFunction)UIEntity::path_to, METH_VARARGS | METH_KEYWORDS,
|
|
"path_to(x, y) or path_to(target) -> list\n\n"
|
|
"Find a path to the target position using Dijkstra pathfinding.\n\n"
|
|
"Args:\n"
|
|
" x, y: Target coordinates as two integers, OR\n"
|
|
" target: Target coordinates as tuple, list, or Vector\n\n"
|
|
"Returns:\n"
|
|
" List of (x, y) tuples representing the path.\n\n"
|
|
"Example:\n"
|
|
" path = entity.path_to(10, 5)\n"
|
|
" path = entity.path_to((10, 5))\n"
|
|
" path = entity.path_to(pos=(10, 5))"},
|
|
{"update_visibility", (PyCFunction)UIEntity::update_visibility, METH_NOARGS,
|
|
"update_visibility() -> None\n\n"
|
|
"Update entity's visibility state based on current FOV.\n\n"
|
|
"Recomputes which cells are visible from the entity's position and updates\n"
|
|
"the entity's gridstate to track explored areas. This is called automatically\n"
|
|
"when the entity moves if it has a grid with perspective set."},
|
|
{"visible_entities", (PyCFunction)UIEntity::visible_entities, METH_VARARGS | METH_KEYWORDS,
|
|
"visible_entities(fov=None, radius=None) -> list[Entity]\n\n"
|
|
"Get list of other entities visible from this entity's position.\n\n"
|
|
"Args:\n"
|
|
" fov (FOV, optional): FOV algorithm to use. Default: grid.fov\n"
|
|
" radius (int, optional): FOV radius. Default: grid.fov_radius\n\n"
|
|
"Returns:\n"
|
|
" List of Entity objects that are within field of view.\n\n"
|
|
"Computes FOV from this entity's position and returns all other entities\n"
|
|
"whose positions fall within the visible area."},
|
|
{NULL} // Sentinel
|
|
};
|
|
|
|
PyGetSetDef UIEntity::getsetters[] = {
|
|
// #176 - Pixel coordinates (relative to grid, like UIDrawable.pos)
|
|
{"pos", (getter)UIEntity::get_pixel_pos, (setter)UIEntity::set_pixel_pos,
|
|
"Pixel position relative to grid (Vector). Computed as draw_pos * tile_size. "
|
|
"Requires entity to be attached to a grid.", NULL},
|
|
{"x", (getter)UIEntity::get_pixel_member, (setter)UIEntity::set_pixel_member,
|
|
"Pixel X position relative to grid. Requires entity to be attached to a grid.", (void*)0},
|
|
{"y", (getter)UIEntity::get_pixel_member, (setter)UIEntity::set_pixel_member,
|
|
"Pixel Y position relative to grid. Requires entity to be attached to a grid.", (void*)1},
|
|
|
|
// #176 - Integer tile coordinates (logical game position)
|
|
{"grid_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position,
|
|
"Grid position as integer tile coordinates (Vector). The logical cell this entity occupies.", (void*)1},
|
|
{"grid_x", (getter)UIEntity::get_grid_int_member, (setter)UIEntity::set_grid_int_member,
|
|
"Grid X position as integer tile coordinate.", (void*)0},
|
|
{"grid_y", (getter)UIEntity::get_grid_int_member, (setter)UIEntity::set_grid_int_member,
|
|
"Grid Y position as integer tile coordinate.", (void*)1},
|
|
|
|
// Float tile coordinates (for smooth animation between tiles)
|
|
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position,
|
|
"Fractional tile position for rendering (Vector). Use for smooth animation between grid cells.", (void*)0},
|
|
|
|
{"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL},
|
|
{"grid", (getter)UIEntity::get_grid, (setter)UIEntity::set_grid,
|
|
"Grid this entity belongs to. "
|
|
"Get: Returns the Grid or None. "
|
|
"Set: Assign a Grid to move entity, or None to remove from grid.", NULL},
|
|
{"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL},
|
|
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index (DEPRECATED: use sprite_index instead)", NULL},
|
|
{"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL},
|
|
{"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL},
|
|
{"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL},
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
PyObject* UIEntity::repr(PyUIEntityObject* self) {
|
|
std::ostringstream ss;
|
|
if (!self->data) ss << "<Entity (invalid internal object)>";
|
|
else {
|
|
// #176 - Use grid_x/grid_y naming to reflect tile coordinates
|
|
ss << "<Entity (grid_x=" << static_cast<int>(self->data->position.x)
|
|
<< ", grid_y=" << static_cast<int>(self->data->position.y)
|
|
<< ", sprite_index=" << self->data->sprite.getSpriteIndex() << ")>";
|
|
}
|
|
std::string repr_str = ss.str();
|
|
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
|
}
|
|
|
|
// Property system implementation for animations
|
|
// #176 - Animation properties use tile coordinates (draw_x, draw_y)
|
|
// "x" and "y" are kept as aliases for backwards compatibility
|
|
bool UIEntity::setProperty(const std::string& name, float value) {
|
|
if (name == "draw_x" || name == "x") { // #176 - draw_x is preferred, x is alias
|
|
position.x = value;
|
|
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
|
if (grid) grid->markDirty(); // #144 - Propagate to parent grid for texture caching
|
|
return true;
|
|
}
|
|
else if (name == "draw_y" || name == "y") { // #176 - draw_y is preferred, y is alias
|
|
position.y = value;
|
|
// Don't update sprite position here - UIGrid::render() handles the pixel positioning
|
|
if (grid) grid->markDirty(); // #144 - Propagate to parent grid for texture caching
|
|
return true;
|
|
}
|
|
else if (name == "sprite_scale") {
|
|
sprite.setScale(sf::Vector2f(value, value));
|
|
if (grid) grid->markDirty(); // #144 - Content change
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UIEntity::setProperty(const std::string& name, int value) {
|
|
if (name == "sprite_index" || name == "sprite_number") {
|
|
sprite.setSpriteIndex(value);
|
|
if (grid) grid->markDirty(); // #144 - Content change
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UIEntity::getProperty(const std::string& name, float& value) const {
|
|
if (name == "draw_x" || name == "x") { // #176
|
|
value = position.x;
|
|
return true;
|
|
}
|
|
else if (name == "draw_y" || name == "y") { // #176
|
|
value = position.y;
|
|
return true;
|
|
}
|
|
else if (name == "sprite_scale") {
|
|
value = sprite.getScale().x; // Assuming uniform scale
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UIEntity::hasProperty(const std::string& name) const {
|
|
// #176 - Float properties (draw_x/draw_y preferred, x/y are aliases)
|
|
if (name == "draw_x" || name == "draw_y" || name == "x" || name == "y" || name == "sprite_scale") {
|
|
return true;
|
|
}
|
|
// Int properties
|
|
if (name == "sprite_index" || name == "sprite_number") {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Animation shorthand for Entity - creates and starts an animation
|
|
PyObject* UIEntity::animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr};
|
|
|
|
const char* property_name;
|
|
PyObject* target_value;
|
|
float duration;
|
|
PyObject* easing_arg = Py_None;
|
|
int delta = 0;
|
|
PyObject* callback = nullptr;
|
|
const char* conflict_mode_str = nullptr;
|
|
|
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast<char**>(keywords),
|
|
&property_name, &target_value, &duration,
|
|
&easing_arg, &delta, &callback, &conflict_mode_str)) {
|
|
return NULL;
|
|
}
|
|
|
|
// Validate property exists on this entity
|
|
if (!self->data->hasProperty(property_name)) {
|
|
PyErr_Format(PyExc_ValueError,
|
|
"Property '%s' is not valid for animation on Entity. "
|
|
"Valid properties: draw_x, draw_y (tile coords), sprite_scale, sprite_index",
|
|
property_name);
|
|
return NULL;
|
|
}
|
|
|
|
// Validate callback is callable if provided
|
|
if (callback && callback != Py_None && !PyCallable_Check(callback)) {
|
|
PyErr_SetString(PyExc_TypeError, "callback must be callable");
|
|
return NULL;
|
|
}
|
|
|
|
// Convert None to nullptr for C++
|
|
if (callback == Py_None) {
|
|
callback = nullptr;
|
|
}
|
|
|
|
// Convert Python target value to AnimationValue
|
|
// Entity only supports float and int properties
|
|
AnimationValue animValue;
|
|
|
|
if (PyFloat_Check(target_value)) {
|
|
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
|
|
}
|
|
else if (PyLong_Check(target_value)) {
|
|
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
|
}
|
|
else {
|
|
PyErr_SetString(PyExc_TypeError, "Entity animations only support float or int target values");
|
|
return NULL;
|
|
}
|
|
|
|
// Get easing function from argument
|
|
EasingFunction easingFunc;
|
|
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
|
|
return NULL; // Error already set by from_arg
|
|
}
|
|
|
|
// Parse conflict mode
|
|
AnimationConflictMode conflict_mode = AnimationConflictMode::REPLACE;
|
|
if (conflict_mode_str) {
|
|
if (strcmp(conflict_mode_str, "replace") == 0) {
|
|
conflict_mode = AnimationConflictMode::REPLACE;
|
|
} else if (strcmp(conflict_mode_str, "queue") == 0) {
|
|
conflict_mode = AnimationConflictMode::QUEUE;
|
|
} else if (strcmp(conflict_mode_str, "error") == 0) {
|
|
conflict_mode = AnimationConflictMode::RAISE_ERROR;
|
|
} else {
|
|
PyErr_Format(PyExc_ValueError,
|
|
"Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
// Create the Animation
|
|
auto animation = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
|
|
|
|
// Start on this entity (uses startEntity, not start)
|
|
animation->startEntity(self->data);
|
|
|
|
// Add to AnimationManager
|
|
AnimationManager::getInstance().addAnimation(animation, conflict_mode);
|
|
|
|
// Check if ERROR mode raised an exception
|
|
if (PyErr_Occurred()) {
|
|
return NULL;
|
|
}
|
|
|
|
// Create and return a PyAnimation wrapper
|
|
PyTypeObject* animType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Animation");
|
|
if (!animType) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Could not find Animation type");
|
|
return NULL;
|
|
}
|
|
|
|
PyAnimationObject* pyAnim = (PyAnimationObject*)animType->tp_alloc(animType, 0);
|
|
Py_DECREF(animType);
|
|
|
|
if (!pyAnim) {
|
|
return NULL;
|
|
}
|
|
|
|
pyAnim->data = animation;
|
|
return (PyObject*)pyAnim;
|
|
}
|