McRogueFace/src/UIEntity.h
John McCardle c1a9523ac2 Add collision label support for pathfinding (closes #302)
Add `collide` kwarg to Grid.find_path() and Grid.get_dijkstra_map() that
treats entities bearing a given label as impassable obstacles via
mark-and-restore on the TCOD walkability map. Dijkstra cache key now
includes collide label for separate caching. Add Entity.find_path()
convenience method that delegates to the grid.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 01:34:19 -04:00

261 lines
14 KiB
C++

#pragma once
#include "Common.h"
#include "Python.h"
#include "structmember.h"
#include "IndexTexture.h"
#include "Resources.h"
#include <list>
#include <unordered_set>
#include <string>
#include "PyCallable.h"
#include "PyTexture.h"
#include "PyDrawable.h"
#include "PyColor.h"
#include "PyVector.h"
#include "PyFont.h"
#include "UIGridPoint.h"
#include "UIBase.h"
#include "UISprite.h"
#include "EntityBehavior.h"
class UIGrid;
// UIEntity
/*
****************************************
* say it with me: *
* ✨ UIEntity is not a UIDrawable ✨ *
****************************************
**Why Not, John?**
Doesn't it say "UI" on the front of it?
It sure does. Probably should have called it "GridEntity", but it's a bit late now.
UIDrawables have positions in **screen pixel coordinates**. Their position is an offset from their parent's position, and they are deeply nestable (Scene -> Frame -> Frame -> ...)
However:
UIEntity has a position in **Grid tile coordinates**.
UIEntity is not nestable at all. Grid -> Entity.
UIEntity has a strict one/none relationship with a Grid: if you add it to another grid, it will have itself removed from the losing grid's collection.
UIEntity originally only allowed a single-tile sprite, but around mid-July 2025, I'm working to change that to allow any UIDrawable to go there, or multi-tile sprites.
UIEntity is, at its core, the container for *a perspective of map data*.
The Grid should contain the true, complete contents of the game's space, and the Entity should use pathfinding, field of view, and game logic to interact with the Grid's layer data.
In Conclusion, because UIEntity cannot be drawn on a Frame or Scene, and has the unique role of serving as a filter of the data contained in a Grid, UIEntity will not become a UIDrawable.
*/
//class UIEntity;
//typedef struct {
// PyObject_HEAD
// std::shared_ptr<UIEntity> data;
//} PyUIEntityObject;
// helper methods with no namespace requirement
PyObject* sfVector2f_to_PyObject(sf::Vector2f vector);
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj);
PyObject* UIGridPointState_to_PyObject(const UIGridPointState& state);
PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointState>& vec);
class UIEntity
{
public:
uint64_t serial_number = 0; // For Python object cache
PyObject* pyobject = nullptr; // Strong ref: preserves Python subclass identity while in grid
std::shared_ptr<UIGrid> grid;
std::vector<UIGridPointState> gridstate;
UISprite sprite;
sf::Vector2f position; //(x,y) in grid coordinates; float for animation
sf::Vector2i cell_position{0, 0}; // #295: integer logical position (decoupled from float position)
sf::Vector2f sprite_offset; // pixel offset for oversized sprites (applied pre-zoom)
std::unordered_set<std::string> labels; // #296: entity label system for collision/targeting
PyObject* step_callback = nullptr; // #299: callback for grid.step() turn management
int default_behavior = 0; // #299: BehaviorType::IDLE - behavior to revert to after DONE
EntityBehavior behavior; // #300: behavior state for grid.step()
int turn_order = 1; // #300: 0 = skip, higher = later in turn order
float move_speed = 0.15f; // #300: animation duration for movement (0 = instant)
std::string target_label; // #300: label to search for with TARGET trigger
int sight_radius = 10; // #300: FOV radius for TARGET trigger
// #303 - Per-entity FOV result cache for TARGET trigger optimization
// Caches the visibility bitmap from the last FOV computation so that
// entities that haven't moved skip recomputation entirely.
struct TargetFOVCache {
sf::Vector2i origin{-1, -1};
int radius = -1;
uint32_t transparency_gen = 0;
std::vector<bool> visibility; // (2*radius+1)^2 bitmap
int vis_side = 0; // 2*radius+1
bool isValid(sf::Vector2i pos, int r, uint32_t gen) const {
return vis_side > 0 && pos == origin && r == radius && gen == transparency_gen;
}
bool isVisible(int x, int y) const {
int dx = x - origin.x + radius;
int dy = y - origin.y + radius;
if (dx < 0 || dx >= vis_side || dy < 0 || dy >= vis_side) return false;
return visibility[dy * vis_side + dx];
}
} target_fov_cache;
//void render(sf::Vector2f); //override final;
UIEntity();
~UIEntity();
// Release the strong reference that preserves Python subclass identity.
// Called when entity leaves a grid (die, set_grid, collection removal).
void releasePyIdentity() {
if (pyobject) {
PyObject* tmp = pyobject;
pyobject = nullptr;
Py_DECREF(tmp);
}
// #299: Clean up step callback
Py_XDECREF(step_callback);
step_callback = nullptr;
}
// Visibility methods
void ensureGridstate(); // Resize gridstate to match current grid dimensions
void updateVisibility(); // Update gridstate from current FOV
// Property system for animations
bool setProperty(const std::string& name, float value);
bool setProperty(const std::string& name, int value);
bool getProperty(const std::string& name, float& value) const;
bool hasProperty(const std::string& name) const;
// Animation shorthand helper - creates and starts an animation on this entity
// Returns a PyAnimation object. Used by the .animate() method.
static PyObject* animate(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
// Methods that delegate to sprite
sf::FloatRect get_bounds() const { return sprite.get_bounds(); }
void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; }
void resize(float w, float h) { /* Entities don't support direct resizing */ }
static PyObject* at(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* find_path(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* update_visibility(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* visible_entities(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* get_position(PyUIEntityObject* self, void* closure);
static int set_position(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_gridstate(PyUIEntityObject* self, void* closure);
static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure);
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);
// #176 - Pixel position (pos, x, y) computed from draw_pos * tile_size
static PyObject* get_pixel_pos(PyUIEntityObject* self, void* closure);
static int set_pixel_pos(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_pixel_member(PyUIEntityObject* self, void* closure);
static int set_pixel_member(PyUIEntityObject* self, PyObject* value, void* closure);
// #176 - Integer grid position (grid_x, grid_y)
static PyObject* get_grid_int_member(PyUIEntityObject* self, void* closure);
static int set_grid_int_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 PyObject* get_sprite_offset(PyUIEntityObject* self, void* closure);
static int set_sprite_offset(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_sprite_offset_member(PyUIEntityObject* self, void* closure);
static int set_sprite_offset_member(PyUIEntityObject* self, PyObject* value, void* closure);
// #295 - cell_pos (integer logical position)
static PyObject* get_cell_pos(PyUIEntityObject* self, void* closure);
static int set_cell_pos(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_cell_member(PyUIEntityObject* self, void* closure);
static int set_cell_member(PyUIEntityObject* self, PyObject* value, void* closure);
// #296 - Label system
static PyObject* get_labels(PyUIEntityObject* self, void* closure);
static int set_labels(PyUIEntityObject* self, PyObject* value, void* closure);
// #299 - Step callback and default behavior
static PyObject* get_step(PyUIEntityObject* self, void* closure);
static int set_step(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_default_behavior(PyUIEntityObject* self, void* closure);
static int set_default_behavior(PyUIEntityObject* self, PyObject* value, void* closure);
// #300 - Behavior system properties
static PyObject* get_behavior_type(PyUIEntityObject* self, void* closure);
static PyObject* get_turn_order(PyUIEntityObject* self, void* closure);
static int set_turn_order(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_move_speed(PyUIEntityObject* self, void* closure);
static int set_move_speed(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_target_label(PyUIEntityObject* self, void* closure);
static int set_target_label(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* get_sight_radius(PyUIEntityObject* self, void* closure);
static int set_sight_radius(PyUIEntityObject* self, PyObject* value, void* closure);
static PyObject* py_set_behavior(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_add_label(PyUIEntityObject* self, PyObject* arg);
static PyObject* py_remove_label(PyUIEntityObject* self, PyObject* arg);
static PyObject* py_has_label(PyUIEntityObject* self, PyObject* arg);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
static PyObject* repr(PyUIEntityObject* self);
};
// Forward declaration of methods array
extern PyMethodDef UIEntity_all_methods[];
namespace mcrfpydef {
inline PyTypeObject PyUIEntityType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.Entity",
.tp_basicsize = sizeof(PyUIEntityObject),
.tp_itemsize = 0,
.tp_dealloc = [](PyObject* obj) {
auto* self = (PyUIEntityObject*)obj;
// Clear the identity ref without DECREF - we ARE this object
if (self->data) self->data->pyobject = nullptr;
if (self->weakreflist) PyObject_ClearWeakRefs(obj);
self->data.reset();
Py_TYPE(obj)->tp_free(obj);
},
.tp_repr = (reprfunc)UIEntity::repr,
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
.tp_doc = PyDoc_STR("Entity(grid_pos=None, texture=None, sprite_index=0, **kwargs)\n\n"
"A game entity that exists on a grid with sprite rendering.\n\n"
"Args:\n"
" grid_pos (tuple, optional): Grid position as (x, y) tuple. Default: (0, 0)\n"
" texture (Texture, optional): Texture object for sprite. Default: default texture\n"
" sprite_index (int, optional): Index into texture atlas. Default: 0\n\n"
"Keyword Args:\n"
" grid (Grid): Grid to attach entity to. Default: None\n"
" visible (bool): Visibility state. Default: True\n"
" opacity (float): Opacity (0.0-1.0). Default: 1.0\n"
" name (str): Element name for finding. Default: None\n"
" x (float): X grid position override (tile coords). Default: 0\n"
" y (float): Y grid position override (tile coords). Default: 0\n"
" sprite_offset (tuple): Pixel offset for oversized sprites. Default: (0, 0)\n\n"
"Attributes:\n"
" pos (Vector): Pixel position relative to grid (requires grid attachment)\n"
" x, y (float): Pixel position components (requires grid attachment)\n"
" grid_pos (Vector): Integer tile coordinates (logical game position)\n"
" grid_x, grid_y (int): Integer tile coordinate components\n"
" draw_pos (Vector): Fractional tile position for smooth animation\n"
" gridstate (GridPointState): Visibility state for grid points\n"
" sprite_index (int): Current sprite index\n"
" visible (bool): Visibility state\n"
" opacity (float): Opacity value\n"
" name (str): Element name\n"
" sprite_offset (Vector): Pixel offset for oversized sprites\n"
" sprite_offset_x (float): X component of sprite offset\n"
" sprite_offset_y (float): Y component of sprite offset"),
.tp_methods = UIEntity_all_methods,
.tp_getset = UIEntity::getsetters,
.tp_base = NULL,
.tp_init = (initproc)UIEntity::init,
.tp_new = PyType_GenericNew,
};
}