feat: Add entity.grid property and fix auto-removal bug

UIEntity now has a `.grid` property with getter/setter:
- entity.grid          # Get current grid (or None)
- entity.grid = grid   # Move to new grid (auto-removes from old)
- entity.grid = None   # Remove from current grid

Also fixes UIEntityCollection.append() to properly implement the
documented "single grid only" behavior - entities are now correctly
removed from their old grid when appended to a new one.

This matches the parent property pattern used for UIDrawables.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-11-27 21:08:31 -05:00
commit e9b5a8301d
3 changed files with 113 additions and 8 deletions

View file

@ -413,6 +413,92 @@ int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* cl
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_x * new_grid->grid_y);
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
@ -557,6 +643,10 @@ PyGetSetDef UIEntity::getsetters[] = {
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
{"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1},
{"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},
{"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0},

View file

@ -98,6 +98,8 @@ public:
static int set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_float_member(PyUIEntityObject* self, void* closure);
static int set_float_member(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_grid(PyUIEntityObject* self, void* closure);
static int set_grid(PyUIEntityObject* self, PyObject* value, void* closure);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIEntityObject* self);

View file

@ -1846,19 +1846,32 @@ PySequenceMethods UIEntityCollection::sqmethods = {
PyObject* UIEntityCollection::append(PyUIEntityCollectionObject* self, PyObject* o)
{
// if not UIDrawable subclass, reject it
// self->data->push_back( c++ object inside o );
// this would be a great use case for .tp_base
//if (!PyObject_IsInstance(o, (PyObject*)&PyUIEntityType))
// Type check - must be Entity
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity")))
{
PyErr_SetString(PyExc_TypeError, "Only Entity objects can be added to EntityCollection");
return NULL;
}
PyUIEntityObject* entity = (PyUIEntityObject*)o;
// Remove from old grid first (if different from target grid)
// This implements the documented "single grid only" behavior
if (entity->data->grid && entity->data->grid != self->grid) {
auto& old_entities = entity->data->grid->entities;
auto it = std::find_if(old_entities->begin(), old_entities->end(),
[entity](const std::shared_ptr<UIEntity>& e) {
return e.get() == entity->data.get();
});
if (it != old_entities->end()) {
old_entities->erase(it);
}
}
// Add to this grid (if not already in it)
if (entity->data->grid != self->grid) {
self->data->push_back(entity->data);
entity->data->grid = self->grid;
}
// Initialize gridstate if not already done
if (entity->data->gridstate.size() == 0 && self->grid) {