Positions are always mcrfpy.Vector, Vector/tuple/iterables expected as inputs, and for position-only inputs we permit x,y args to prevent requiring double-parens

This commit is contained in:
John McCardle 2026-01-05 10:16:16 -05:00
commit d2e4791f5a
25 changed files with 2109 additions and 636 deletions

View file

@ -4,6 +4,7 @@
#include "PyColor.h"
#include "PyTexture.h"
#include "PyFOV.h"
#include "PyPositionHelper.h"
#include <sstream>
// =============================================================================
@ -562,10 +563,18 @@ void TileLayer::render(sf::RenderTarget& target,
// =============================================================================
PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS,
"at(x, y) -> Color\n\nGet the color at cell position (x, y)."},
{"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS | METH_KEYWORDS,
"at(pos) -> Color\nat(x, y) -> Color\n\n"
"Get the color at cell position.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" x, y: Position as separate integer arguments"},
{"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS,
"set(x, y, color)\n\nSet the color at cell position (x, y)."},
"set(pos, color)\n\n"
"Set the color at cell position.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" color: Color object or (r, g, b[, a]) tuple"},
{"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
"fill(color)\n\nFill the entire layer with the specified color."},
{"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
@ -646,9 +655,9 @@ int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, Py
return 0;
}
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) {
PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
@ -678,9 +687,14 @@ PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args
}
PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) {
int x, y;
PyObject* pos_obj;
PyObject* color_obj;
if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) {
if (!PyArg_ParseTuple(args, "OO", &pos_obj, &color_obj)) {
return NULL;
}
int x, y;
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
return NULL;
}
@ -1108,10 +1122,18 @@ PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
// =============================================================================
PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS,
"at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."},
{"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS | METH_KEYWORDS,
"at(pos) -> int\nat(x, y) -> int\n\n"
"Get the tile index at cell position. Returns -1 if no tile.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" x, y: Position as separate integer arguments"},
{"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS,
"set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."},
"set(pos, index)\n\n"
"Set the tile index at cell position. Use -1 for no tile.\n\n"
"Args:\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" index: Tile index (-1 for no tile)"},
{"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS,
"fill(index)\n\nFill the entire layer with the specified tile index."},
{"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
@ -1190,9 +1212,9 @@ int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyOb
return 0;
}
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) {
PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
@ -1210,8 +1232,14 @@ PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args)
}
PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) {
int x, y, index;
if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) {
PyObject* pos_obj;
int index;
if (!PyArg_ParseTuple(args, "Oi", &pos_obj, &index)) {
return NULL;
}
int x, y;
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
return NULL;
}

View file

@ -200,7 +200,7 @@ class PyGridLayerAPI {
public:
// ColorLayer methods
static int ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_at(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* ColorLayer_set(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_fill(PyColorLayerObject* self, PyObject* args);
static PyObject* ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds);
@ -217,7 +217,7 @@ public:
// TileLayer methods
static int TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_at(PyTileLayerObject* self, PyObject* args, PyObject* kwds);
static PyObject* TileLayer_set(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_fill(PyTileLayerObject* self, PyObject* args);
static PyObject* TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds);

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ public:
// Mouse position and screen info
static PyObject* _position(PyObject* self, PyObject* args);
static PyObject* _size(PyObject* self, PyObject* args);
static PyObject* _onScreen(PyObject* self, PyObject* args);
static PyObject* _onScreen(PyObject* self, PyObject* args, PyObject* kwargs);
// Mouse movement
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);

View file

@ -1,6 +1,7 @@
#include "PyCallable.h"
#include "McRFPy_API.h"
#include "GameEngine.h"
#include "PyVector.h"
PyCallable::PyCallable(PyObject* _target)
{
@ -49,7 +50,16 @@ PyClickCallable::PyClickCallable()
void PyClickCallable::call(sf::Vector2f mousepos, std::string button, std::string action)
{
PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), action.c_str());
// Create a Vector object for the position
PyObject* pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", mousepos.x, mousepos.y);
if (!pos) {
std::cerr << "Failed to create Vector object for click callback" << std::endl;
PyErr_Print();
PyErr_Clear();
return;
}
PyObject* args = Py_BuildValue("(Oss)", pos, button.c_str(), action.c_str());
Py_DECREF(pos); // Py_BuildValue increments the refcount
PyObject* retval = PyCallable::call(args, NULL);
if (!retval)
{

View file

@ -1,13 +1,14 @@
#include "PyDrawable.h"
#include "McRFPy_API.h"
#include "McRFPy_Doc.h"
#include "PyPositionHelper.h"
// Click property getter
static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure)
{
if (!self->data->click_callable)
if (!self->data->click_callable)
Py_RETURN_NONE;
PyObject* ptr = self->data->click_callable->borrow();
if (ptr && ptr != Py_None)
return ptr;
@ -35,20 +36,20 @@ static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure)
return PyLong_FromLong(self->data->z_index);
}
// Z-index property setter
// Z-index property setter
static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure)
{
if (!PyLong_Check(value)) {
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
return -1;
}
int val = PyLong_AsLong(value);
self->data->z_index = val;
// Mark scene as needing resort
self->data->notifyZIndexChanged();
return 0;
}
@ -65,7 +66,7 @@ static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void*
PyErr_SetString(PyExc_TypeError, "visible must be a boolean");
return -1;
}
self->data->visible = (value == Py_True);
return 0;
}
@ -88,11 +89,11 @@ static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void*
PyErr_SetString(PyExc_TypeError, "opacity must be a number");
return -1;
}
// Clamp to valid range
if (val < 0.0f) val = 0.0f;
if (val > 1.0f) val = 1.0f;
self->data->opacity = val;
return 0;
}
@ -102,7 +103,7 @@ static PyGetSetDef PyDrawable_getsetters[] = {
{"on_click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click,
MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
"Function receives (pos: Vector, button: str, action: str)."
), NULL},
{"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index,
MCRF_PROPERTY(z_index,
@ -130,25 +131,25 @@ static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUS
}
// move method implementation (#98)
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args)
static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args)
static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args, PyObject* kwds)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
if (!PyPosition_ParseFloat(args, kwds, &w, &h)) {
return NULL;
}
self->data->resize(w, h);
Py_RETURN_NONE;
}
@ -162,23 +163,27 @@ static PyMethodDef PyDrawable_methods[] = {
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds")
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.")
)},
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS,
{"move", (PyCFunction)PyDrawable_move, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Drawable, move,
MCRF_SIG("(dx: float, dy: float)", "None"),
MCRF_SIG("(dx, dy) or (delta)", "None"),
MCRF_DESC("Move the element by a relative offset."),
MCRF_ARGS_START
MCRF_ARG("dx", "Horizontal offset in pixels")
MCRF_ARG("dy", "Vertical offset in pixels")
MCRF_NOTE("This modifies the x and y position properties by the given amounts.")
MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)")
MCRF_ARG("dy", "Vertical offset in pixels (or use delta)")
MCRF_ARG("delta", "Offset as tuple, list, or Vector: (dx, dy)")
MCRF_NOTE("This modifies the x and y position properties by the given amounts. "
"Accepts move(dx, dy), move((dx, dy)), move(Vector), or move(pos=(dx, dy)).")
)},
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS,
{"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(Drawable, resize,
MCRF_SIG("(width: float, height: float)", "None"),
MCRF_SIG("(width, height) or (size)", "None"),
MCRF_DESC("Resize the element to new dimensions."),
MCRF_ARGS_START
MCRF_ARG("width", "New width in pixels")
MCRF_ARG("height", "New height in pixels")
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.")
MCRF_ARG("width", "New width in pixels (or use size)")
MCRF_ARG("height", "New height in pixels (or use size)")
MCRF_ARG("size", "Size as tuple, list, or Vector: (width, height)")
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content. "
"Accepts resize(w, h), resize((w, h)), resize(Vector), or resize(pos=(w, h)).")
)},
{NULL} // Sentinel
};
@ -208,4 +213,4 @@ namespace mcrfpydef {
.tp_init = (initproc)PyDrawable_init,
.tp_new = PyType_GenericNew,
};
}
}

View file

@ -3,7 +3,32 @@
#include "PyVector.h"
#include "McRFPy_API.h"
// Helper class for standardized position argument parsing across UI classes
// ============================================================================
// PyPositionHelper - Reusable position argument parsing for McRogueFace API
// ============================================================================
//
// This helper provides standardized parsing for position arguments that can be
// specified in multiple formats:
// - Two separate args: func(x, y)
// - A tuple: func((x, y))
// - A Vector object: func(Vector(x, y))
// - Any iterable with len() == 2: func([x, y])
// - Keyword args: func(x=x, y=y) or func(pos=(x,y))
//
// Usage patterns:
// // For methods with only position args (like Grid.at()):
// int x, y;
// if (!PyPosition_ParseInt(args, kwds, &x, &y)) return NULL;
//
// // For extracting position from a single PyObject:
// float x, y;
// if (!PyPosition_FromObject(obj, &x, &y)) return NULL;
//
// // For more complex parsing with additional args:
// auto result = PyPositionHelper::parse_position(args, kwds);
// if (!result.has_position) { ... }
// ============================================================================
class PyPositionHelper {
public:
// Template structure for parsing results
@ -12,33 +37,315 @@ public:
float y = 0.0f;
bool has_position = false;
};
struct ParseResultInt {
int x = 0;
int y = 0;
bool has_position = false;
};
private:
// Internal helper: extract two numeric values from a 2-element iterable
// Returns true on success, false on failure (does NOT set Python error)
static bool extract_from_iterable(PyObject* obj, float* out_x, float* out_y) {
// First check if it's a Vector (most specific)
PyTypeObject* vector_type = (PyTypeObject*)PyObject_GetAttrString(
McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
if (PyObject_IsInstance(obj, (PyObject*)vector_type)) {
PyVectorObject* vec = (PyVectorObject*)obj;
*out_x = vec->data.x;
*out_y = vec->data.y;
Py_DECREF(vector_type);
return true;
}
Py_DECREF(vector_type);
} else {
PyErr_Clear(); // Clear any error from GetAttrString
}
// Check for tuple (common case, optimized)
if (PyTuple_Check(obj)) {
if (PyTuple_Size(obj) != 2) return false;
PyObject* x_obj = PyTuple_GetItem(obj, 0);
PyObject* y_obj = PyTuple_GetItem(obj, 1);
if (!extract_number(x_obj, out_x) || !extract_number(y_obj, out_y)) {
return false;
}
return true;
}
// Check for list (also common)
if (PyList_Check(obj)) {
if (PyList_Size(obj) != 2) return false;
PyObject* x_obj = PyList_GetItem(obj, 0);
PyObject* y_obj = PyList_GetItem(obj, 1);
if (!extract_number(x_obj, out_x) || !extract_number(y_obj, out_y)) {
return false;
}
return true;
}
// Generic iterable fallback: check __len__ and index
// This handles any object that implements sequence protocol
if (PySequence_Check(obj)) {
Py_ssize_t len = PySequence_Size(obj);
if (len != 2) {
PyErr_Clear(); // Clear size error
return false;
}
PyObject* x_obj = PySequence_GetItem(obj, 0);
if (!x_obj) { PyErr_Clear(); return false; }
PyObject* y_obj = PySequence_GetItem(obj, 1);
if (!y_obj) { Py_DECREF(x_obj); PyErr_Clear(); return false; }
bool success = extract_number(x_obj, out_x) && extract_number(y_obj, out_y);
Py_DECREF(x_obj);
Py_DECREF(y_obj);
return success;
}
return false;
}
// Internal helper: extract integer values from a 2-element iterable
static bool extract_from_iterable_int(PyObject* obj, int* out_x, int* out_y) {
// First check if it's a Vector
PyTypeObject* vector_type = (PyTypeObject*)PyObject_GetAttrString(
McRFPy_API::mcrf_module, "Vector");
if (vector_type) {
if (PyObject_IsInstance(obj, (PyObject*)vector_type)) {
PyVectorObject* vec = (PyVectorObject*)obj;
*out_x = static_cast<int>(vec->data.x);
*out_y = static_cast<int>(vec->data.y);
Py_DECREF(vector_type);
return true;
}
Py_DECREF(vector_type);
} else {
PyErr_Clear();
}
// Check for tuple
if (PyTuple_Check(obj)) {
if (PyTuple_Size(obj) != 2) return false;
PyObject* x_obj = PyTuple_GetItem(obj, 0);
PyObject* y_obj = PyTuple_GetItem(obj, 1);
if (!extract_int(x_obj, out_x) || !extract_int(y_obj, out_y)) {
return false;
}
return true;
}
// Check for list
if (PyList_Check(obj)) {
if (PyList_Size(obj) != 2) return false;
PyObject* x_obj = PyList_GetItem(obj, 0);
PyObject* y_obj = PyList_GetItem(obj, 1);
if (!extract_int(x_obj, out_x) || !extract_int(y_obj, out_y)) {
return false;
}
return true;
}
// Generic sequence fallback
if (PySequence_Check(obj)) {
Py_ssize_t len = PySequence_Size(obj);
if (len != 2) {
PyErr_Clear();
return false;
}
PyObject* x_obj = PySequence_GetItem(obj, 0);
if (!x_obj) { PyErr_Clear(); return false; }
PyObject* y_obj = PySequence_GetItem(obj, 1);
if (!y_obj) { Py_DECREF(x_obj); PyErr_Clear(); return false; }
bool success = extract_int(x_obj, out_x) && extract_int(y_obj, out_y);
Py_DECREF(x_obj);
Py_DECREF(y_obj);
return success;
}
return false;
}
// Extract a float from a numeric Python object
static bool extract_number(PyObject* obj, float* out) {
if (PyFloat_Check(obj)) {
*out = static_cast<float>(PyFloat_AsDouble(obj));
return true;
}
if (PyLong_Check(obj)) {
*out = static_cast<float>(PyLong_AsLong(obj));
return true;
}
return false;
}
// Extract an int from a numeric Python object (integers only)
static bool extract_int(PyObject* obj, int* out) {
if (PyLong_Check(obj)) {
*out = static_cast<int>(PyLong_AsLong(obj));
return true;
}
// Also accept float but only if it's a whole number
if (PyFloat_Check(obj)) {
double val = PyFloat_AsDouble(obj);
if (val == static_cast<double>(static_cast<int>(val))) {
*out = static_cast<int>(val);
return true;
}
}
return false;
}
public:
// ========================================================================
// Simple API: Parse position from a single PyObject
// ========================================================================
// Extract float position from any supported format
// Sets Python error and returns false on failure
static bool FromObject(PyObject* obj, float* out_x, float* out_y) {
if (extract_from_iterable(obj, out_x, out_y)) {
return true;
}
PyErr_SetString(PyExc_TypeError,
"Expected a position as (x, y) tuple, [x, y] list, Vector, or other 2-element sequence");
return false;
}
// Extract integer position from any supported format
// Sets Python error and returns false on failure
static bool FromObjectInt(PyObject* obj, int* out_x, int* out_y) {
if (extract_from_iterable_int(obj, out_x, out_y)) {
return true;
}
PyErr_SetString(PyExc_TypeError,
"Expected integer position as (x, y) tuple, [x, y] list, Vector, or other 2-element sequence");
return false;
}
// ========================================================================
// Method argument API: Parse position from args tuple
// ========================================================================
// Parse float position from method arguments
// Supports: func(x, y) or func((x, y)) or func(Vector) or func(iterable)
// Sets Python error and returns false on failure
static bool ParseFloat(PyObject* args, PyObject* kwds, float* out_x, float* out_y) {
// First try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if (extract_number(x_obj, out_x) && extract_number(y_obj, out_y)) {
return true;
}
}
if (pos_obj) {
if (extract_from_iterable(pos_obj, out_x, out_y)) {
return true;
}
}
}
Py_ssize_t nargs = PyTuple_Size(args);
// Try two separate numeric arguments: func(x, y)
if (nargs >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (extract_number(first, out_x) && extract_number(second, out_y)) {
return true;
}
}
// Try single iterable argument: func((x, y)) or func(Vector) or func([x, y])
if (nargs == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (extract_from_iterable(first, out_x, out_y)) {
return true;
}
}
PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), ((x,y)), pos=(x,y), Vector, or 2-element sequence");
return false;
}
// Parse integer position from method arguments
// Supports: func(x, y) or func((x, y)) or func(Vector) or func(iterable)
// Sets Python error and returns false on failure
static bool ParseInt(PyObject* args, PyObject* kwds, int* out_x, int* out_y) {
// First try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if (extract_int(x_obj, out_x) && extract_int(y_obj, out_y)) {
return true;
}
}
if (pos_obj) {
if (extract_from_iterable_int(pos_obj, out_x, out_y)) {
return true;
}
}
}
Py_ssize_t nargs = PyTuple_Size(args);
// Try two separate integer arguments: func(x, y)
if (nargs >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (extract_int(first, out_x) && extract_int(second, out_y)) {
return true;
}
}
// Try single iterable argument: func((x, y)) or func(Vector) or func([x, y])
if (nargs == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (extract_from_iterable_int(first, out_x, out_y)) {
return true;
}
}
PyErr_SetString(PyExc_TypeError,
"Position must be integers specified as: (x, y), ((x,y)), pos=(x,y), Vector, or 2-element sequence");
return false;
}
// ========================================================================
// Legacy struct-based API (for compatibility with existing code)
// ========================================================================
// Parse position from multiple formats for UI class constructors
// Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector
static ParseResult parse_position(PyObject* args, PyObject* kwds,
int* arg_index = nullptr)
static ParseResult parse_position(PyObject* args, PyObject* kwds,
int* arg_index = nullptr)
{
ParseResult result;
float x = 0.0f, y = 0.0f;
PyObject* pos_obj = nullptr;
int start_index = arg_index ? *arg_index : 0;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) > start_index + 1) {
if (PyTuple_Size(args) > start_index + 1) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyObject* second = PyTuple_GetItem(args, start_index + 1);
// Check if both are numbers
if ((PyFloat_Check(first) || PyLong_Check(first)) &&
(PyFloat_Check(second) || PyLong_Check(second))) {
x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first);
y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second);
if (extract_number(first, &x) && extract_number(second, &y)) {
result.x = x;
result.y = y;
result.has_position = true;
@ -46,119 +353,100 @@ public:
return result;
}
}
// Check for single positional argument that might be tuple or Vector
if (!kwds && PyTuple_Size(args) > start_index) {
// Check for single positional argument that might be tuple, list, or Vector
if (PyTuple_Size(args) > start_index) {
PyObject* first = PyTuple_GetItem(args, start_index);
PyVectorObject* vec = PyVector::from_arg(first);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
if (extract_from_iterable(first, &x, &y)) {
result.x = x;
result.y = y;
result.has_position = true;
if (arg_index) *arg_index += 1;
return result;
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_kw = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj) {
if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) &&
(PyFloat_Check(y_obj) || PyLong_Check(y_obj))) {
result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj);
result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj);
if (extract_number(x_obj, &x) && extract_number(y_obj, &y)) {
result.x = x;
result.y = y;
result.has_position = true;
return result;
}
}
if (pos_kw) {
PyVectorObject* vec = PyVector::from_arg(pos_kw);
if (vec) {
result.x = vec->data.x;
result.y = vec->data.y;
if (extract_from_iterable(pos_kw, &x, &y)) {
result.x = x;
result.y = y;
result.has_position = true;
return result;
}
}
}
return result;
}
// Parse integer position for Grid.at() and similar
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds)
{
ParseResultInt result;
// Check for positional tuple (x, y) first
if (!kwds && PyTuple_Size(args) >= 2) {
PyObject* first = PyTuple_GetItem(args, 0);
PyObject* second = PyTuple_GetItem(args, 1);
if (PyLong_Check(first) && PyLong_Check(second)) {
result.x = PyLong_AsLong(first);
result.y = PyLong_AsLong(second);
result.has_position = true;
return result;
}
int x = 0, y = 0;
// Try the new simplified parser first
if (ParseInt(args, kwds, &x, &y)) {
result.x = x;
result.y = y;
result.has_position = true;
PyErr_Clear(); // Clear any error set by ParseInt
}
// Check for single tuple argument
if (!kwds && PyTuple_Size(args) == 1) {
PyObject* first = PyTuple_GetItem(args, 0);
if (PyTuple_Check(first) && PyTuple_Size(first) == 2) {
PyObject* x_obj = PyTuple_GetItem(first, 0);
PyObject* y_obj = PyTuple_GetItem(first, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
}
}
// Try keyword arguments
if (kwds) {
PyObject* x_obj = PyDict_GetItemString(kwds, "x");
PyObject* y_obj = PyDict_GetItemString(kwds, "y");
PyObject* pos_obj = PyDict_GetItemString(kwds, "pos");
if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
result.x = PyLong_AsLong(x_obj);
result.y = PyLong_AsLong(y_obj);
result.has_position = true;
return result;
}
if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
PyObject* x_val = PyTuple_GetItem(pos_obj, 0);
PyObject* y_val = PyTuple_GetItem(pos_obj, 1);
if (PyLong_Check(x_val) && PyLong_Check(y_val)) {
result.x = PyLong_AsLong(x_val);
result.y = PyLong_AsLong(y_val);
result.has_position = true;
return result;
}
}
}
return result;
}
// Error message helper
static void set_position_error() {
PyErr_SetString(PyExc_TypeError,
"Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector");
"Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence");
}
static void set_position_int_error() {
PyErr_SetString(PyExc_TypeError,
"Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values");
"Position must be integers specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), Vector, or 2-element sequence");
}
};
};
// ============================================================================
// Convenience macros/functions for common use patterns
// ============================================================================
// Parse integer position from method args - simplest API
// Usage: if (!PyPosition_ParseInt(args, kwds, &x, &y)) return NULL;
inline bool PyPosition_ParseInt(PyObject* args, PyObject* kwds, int* x, int* y) {
return PyPositionHelper::ParseInt(args, kwds, x, y);
}
// Parse float position from method args
// Usage: if (!PyPosition_ParseFloat(args, kwds, &x, &y)) return NULL;
inline bool PyPosition_ParseFloat(PyObject* args, PyObject* kwds, float* x, float* y) {
return PyPositionHelper::ParseFloat(args, kwds, x, y);
}
// Extract integer position from a single Python object
// Usage: if (!PyPosition_FromObjectInt(obj, &x, &y)) return NULL;
inline bool PyPosition_FromObjectInt(PyObject* obj, int* x, int* y) {
return PyPositionHelper::FromObjectInt(obj, x, y);
}
// Extract float position from a single Python object
// Usage: if (!PyPosition_FromObject(obj, &x, &y)) return NULL;
inline bool PyPosition_FromObject(PyObject* obj, float* x, float* y) {
return PyPositionHelper::FromObject(obj, x, y);
}

View file

@ -430,7 +430,7 @@ PyGetSetDef UIArc::getsetters[] = {
{"thickness", (getter)UIArc::get_thickness, (setter)UIArc::set_thickness,
"Line thickness", NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
"Callable executed when arc is clicked.", (void*)PyObjectsEnum::UIARC},
"Callable executed when arc is clicked. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UIARC},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UIARC},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,

View file

@ -1,6 +1,7 @@
#pragma once
#include "Python.h"
#include "McRFPy_Doc.h"
#include "PyPositionHelper.h"
#include <memory>
class UIEntity;
@ -52,23 +53,23 @@ static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args))
// move method implementation (#98)
template<typename T>
static PyObject* UIDrawable_move(T* self, PyObject* args)
static PyObject* UIDrawable_move(T* self, PyObject* args, PyObject* kwds)
{
float dx, dy;
if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) {
if (!PyPosition_ParseFloat(args, kwds, &dx, &dy)) {
return NULL;
}
self->data->move(dx, dy);
Py_RETURN_NONE;
}
// resize method implementation (#98)
template<typename T>
static PyObject* UIDrawable_resize(T* self, PyObject* args)
static PyObject* UIDrawable_resize(T* self, PyObject* args, PyObject* kwds)
{
float w, h;
if (!PyArg_ParseTuple(args, "ff", &w, &h)) {
if (!PyPosition_ParseFloat(args, kwds, &w, &h)) {
return NULL;
}
@ -97,23 +98,25 @@ static PyObject* UIDrawable_animate(T* self, PyObject* args, PyObject* kwds)
MCRF_RETURNS("tuple: (x, y, width, height) representing the element's bounds") \
MCRF_NOTE("The bounds are in screen coordinates and account for current position and size.") \
)}, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
MCRF_METHOD(Drawable, move, \
MCRF_SIG("(dx: float, dy: float)", "None"), \
MCRF_SIG("(dx, dy) or (delta)", "None"), \
MCRF_DESC("Move the element by a relative offset."), \
MCRF_ARGS_START \
MCRF_ARG("dx", "Horizontal offset in pixels") \
MCRF_ARG("dy", "Vertical offset in pixels") \
MCRF_NOTE("This modifies the x and y position properties by the given amounts.") \
MCRF_ARG("dx", "Horizontal offset in pixels (or use delta)") \
MCRF_ARG("dy", "Vertical offset in pixels (or use delta)") \
MCRF_ARG("delta", "Offset as tuple, list, or Vector: (dx, dy)") \
MCRF_NOTE("Accepts move(dx, dy), move((dx, dy)), move(Vector), or move(pos=(dx, dy)).") \
)}, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS | METH_KEYWORDS, \
MCRF_METHOD(Drawable, resize, \
MCRF_SIG("(width: float, height: float)", "None"), \
MCRF_SIG("(width, height) or (size)", "None"), \
MCRF_DESC("Resize the element to new dimensions."), \
MCRF_ARGS_START \
MCRF_ARG("width", "New width in pixels") \
MCRF_ARG("height", "New height in pixels") \
MCRF_NOTE("For Caption and Sprite, this may not change actual size if determined by content.") \
MCRF_ARG("width", "New width in pixels (or use size)") \
MCRF_ARG("height", "New height in pixels (or use size)") \
MCRF_ARG("size", "Size as tuple, list, or Vector: (width, height)") \
MCRF_NOTE("Accepts resize(w, h), resize((w, h)), resize(Vector), or resize(pos=(w, h)).") \
)}
// Macro to add common UIDrawable methods to a method array (includes animate for UIDrawable derivatives)
@ -222,12 +225,12 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
{"on_enter", (getter)UIDrawable::get_on_enter, (setter)UIDrawable::set_on_enter, \
MCRF_PROPERTY(on_enter, \
"Callback for mouse enter events. " \
"Called with (x, y, button, action) when mouse enters this element's bounds." \
"Called with (pos: Vector, button: str, action: str) when mouse enters this element's bounds." \
), (void*)type_enum}, \
{"on_exit", (getter)UIDrawable::get_on_exit, (setter)UIDrawable::set_on_exit, \
MCRF_PROPERTY(on_exit, \
"Callback for mouse exit events. " \
"Called with (x, y, button, action) when mouse leaves this element's bounds." \
"Called with (pos: Vector, button: str, action: str) when mouse leaves this element's bounds." \
), (void*)type_enum}, \
{"hovered", (getter)UIDrawable::get_hovered, NULL, \
MCRF_PROPERTY(hovered, \
@ -237,7 +240,7 @@ static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure)
{"on_move", (getter)UIDrawable::get_on_move, (setter)UIDrawable::set_on_move, \
MCRF_PROPERTY(on_move, \
"Callback for mouse movement within bounds. " \
"Called with (x, y, button, action) for each mouse movement while inside. " \
"Called with (pos: Vector, button: str, action: str) for each mouse movement while inside. " \
"Performance note: Called frequently during movement - keep handlers fast." \
), (void*)type_enum}

View file

@ -280,7 +280,7 @@ PyGetSetDef UICaption::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
"Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UICAPTION},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index,

View file

@ -385,7 +385,7 @@ PyGetSetDef UICircle::getsetters[] = {
{"outline", (getter)UICircle::get_outline, (setter)UICircle::set_outline,
"Outline thickness (0 for no outline)", NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
"Callable executed when circle is clicked.", (void*)PyObjectsEnum::UICIRCLE},
"Callable executed when circle is clicked. Function receives (pos: Vector, button: str, action: str).", (void*)PyObjectsEnum::UICIRCLE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
"Z-order for rendering (lower values rendered first).", (void*)PyObjectsEnum::UICIRCLE},
{"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name,

View file

@ -10,6 +10,7 @@
#include "Animation.h"
#include "PyAnimation.h"
#include "PyEasing.h"
#include "PyPositionHelper.h"
// UIDrawable methods now in UIBase.h
#include "UIEntityPyMethods.h"
@ -94,18 +95,17 @@ void UIEntity::updateVisibility()
}
}
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
int x, y;
if (!PyArg_ParseTuple(o, "ii", &x, &y)) {
PyErr_SetString(PyExc_TypeError, "UIEntity.at requires two integer arguments: (x, y)");
return NULL;
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_x * self->data->grid->grid_y);
@ -115,13 +115,13 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
state.discovered = false;
}
}
// Bounds check
if (x < 0 || x >= self->data->grid->grid_x || y < 0 || y >= self->data->grid->grid_y) {
PyErr_Format(PyExc_IndexError, "Grid coordinates (%d, %d) out of bounds", x, y);
return NULL;
}
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "GridPointState");
auto obj = (PyUIGridPointStateObject*)type->tp_alloc(type, 0);
Py_DECREF(type);
@ -590,21 +590,14 @@ PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored))
}
PyObject* UIEntity::path_to(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"target_x", "target_y", "x", "y", nullptr};
int target_x = -1, target_y = -1;
// Parse arguments - support both target_x/target_y and x/y parameter names
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords),
&target_x, &target_y)) {
PyErr_Clear();
// Try alternative parameter names
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iiii", const_cast<char**>(keywords),
&target_x, &target_y, &target_x, &target_y)) {
PyErr_SetString(PyExc_TypeError, "path_to() requires target_x and target_y integer arguments");
return NULL;
}
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");
@ -743,19 +736,32 @@ PyObject* UIEntity::visible_entities(PyUIEntityObject* self, PyObject* args, PyO
}
PyMethodDef UIEntity::methods[] = {
{"at", (PyCFunction)UIEntity::at, METH_O},
{"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: int, y: int) -> bool\n\n"
"Find and follow path to target position using A* pathfinding.\n\n"
"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: Target X coordinate\n"
" y: Target Y coordinate\n\n"
" x, y: Target coordinates as two integers, OR\n"
" target: Target coordinates as tuple, list, or Vector\n\n"
"Returns:\n"
" True if a path was found and the entity started moving, False otherwise\n\n"
"The entity will automatically move along the path over multiple frames.\n"
"Call this again to change the target or repath."},
" 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"
@ -799,19 +805,32 @@ PyMethodDef UIEntity_all_methods[] = {
MCRF_RAISES("ValueError", "If property name is not valid for Entity (x, y, sprite_scale, sprite_index)")
MCRF_NOTE("Entity animations use grid coordinates for x/y, not pixel coordinates.")
)},
{"at", (PyCFunction)UIEntity::at, METH_O},
{"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: int, y: int) -> bool\n\n"
"Find and follow path to target position using A* pathfinding.\n\n"
"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: Target X coordinate\n"
" y: Target Y coordinate\n\n"
" x, y: Target coordinates as two integers, OR\n"
" target: Target coordinates as tuple, list, or Vector\n\n"
"Returns:\n"
" True if a path was found and the entity started moving, False otherwise\n\n"
"The entity will automatically move along the path over multiple frames.\n"
"Call this again to change the target or repath."},
" 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"

View file

@ -89,7 +89,7 @@ public:
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* o);
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);

View file

@ -444,7 +444,7 @@ PyGetSetDef UIFrame::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
"Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UIFRAME},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index,

View file

@ -5,6 +5,7 @@
#include "UIEntity.h"
#include "Profiler.h"
#include "PyFOV.h"
#include "PyPositionHelper.h" // For standardized position argument parsing
#include <algorithm>
#include <cmath> // #142 - for std::floor, std::isnan
#include <cstring> // #150 - for strcmp
@ -685,15 +686,20 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
// Only fire if within valid grid bounds
if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) {
PyObject* args = Py_BuildValue("(ii)", cell_x, cell_y);
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
// Create Vector object for cell position
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)cell_x, (float)cell_y);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
}
}
}
@ -709,15 +715,20 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point)
// Only fire if within valid grid bounds
if (cell_x >= 0 && cell_x < this->grid_x && cell_y >= 0 && cell_y < this->grid_y) {
PyObject* args = Py_BuildValue("(ii)", cell_x, cell_y);
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
// Create Vector object for cell position
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)cell_x, (float)cell_y);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_click_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell click callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
}
// Don't return this - no click_callable to call
}
@ -1141,36 +1152,14 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* keywords[] = {"x", "y", nullptr};
int x = 0, y = 0;
// First try to parse as two integers
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii", const_cast<char**>(keywords), &x, &y)) {
PyErr_Clear();
// Try to parse as a single tuple argument
PyObject* pos_tuple = nullptr;
if (PyArg_ParseTuple(args, "O", &pos_tuple)) {
if (PyTuple_Check(pos_tuple) && PyTuple_Size(pos_tuple) == 2) {
PyObject* x_obj = PyTuple_GetItem(pos_tuple, 0);
PyObject* y_obj = PyTuple_GetItem(pos_tuple, 1);
if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) {
x = PyLong_AsLong(x_obj);
y = PyLong_AsLong(y_obj);
} else {
PyErr_SetString(PyExc_TypeError, "Grid indices must be integers");
return NULL;
}
} else {
PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers");
return NULL;
}
} else {
PyErr_SetString(PyExc_TypeError, "at() takes two integers or a tuple of two integers");
return NULL;
}
int x, y;
// Use the flexible position parsing helper - accepts:
// at(x, y), at((x, y)), at([x, y]), at(Vector(x, y)), at(pos=(x, y)), etc.
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL; // Error already set by PyPosition_ParseInt
}
// Range validation
if (x < 0 || x >= self->data->grid_x) {
PyErr_Format(PyExc_IndexError, "x index %d is out of range [0, %d)", x, self->data->grid_x);
@ -1349,16 +1338,22 @@ int UIGrid::set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure)
// Python API implementations for TCOD functionality
PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"x", "y", "radius", "light_walls", "algorithm", NULL};
int x, y, radius = 0;
static const char* kwlist[] = {"pos", "radius", "light_walls", "algorithm", NULL};
PyObject* pos_obj = NULL;
int radius = 0;
int light_walls = 1;
int algorithm = FOV_BASIC;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|ipi", const_cast<char**>(kwlist),
&x, &y, &radius, &light_walls, &algorithm)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ipi", const_cast<char**>(kwlist),
&pos_obj, &radius, &light_walls, &algorithm)) {
return NULL;
}
int x, y;
if (!PyPosition_FromObjectInt(pos_obj, &x, &y)) {
return NULL;
}
// Compute FOV
self->data->computeFOV(x, y, radius, light_walls, (TCOD_fov_algorithm_t)algorithm);
@ -1367,33 +1362,42 @@ PyObject* UIGrid::py_compute_fov(PyUIGridObject* self, PyObject* args, PyObject*
Py_RETURN_NONE;
}
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args)
PyObject* UIGrid::py_is_in_fov(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
bool in_fov = self->data->isInFOV(x, y);
return PyBool_FromLong(in_fov);
}
PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
int x1, y1, x2, y2;
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
&x1, &y1, &x2, &y2, &diagonal_cost)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&start_obj, &end_obj, &diagonal_cost)) {
return NULL;
}
int x1, y1, x2, y2;
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
return NULL;
}
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
return NULL;
}
std::vector<std::pair<int, int>> path = self->data->findPath(x1, y1, x2, y2, diagonal_cost);
PyObject* path_list = PyList_New(path.size());
if (!path_list) return NULL;
for (size_t i = 0; i < path.size(); i++) {
PyObject* coord = Py_BuildValue("(ii)", path[i].first, path[i].second);
if (!coord) {
@ -1402,80 +1406,93 @@ PyObject* UIGrid::py_find_path(PyUIGridObject* self, PyObject* args, PyObject* k
}
PyList_SET_ITEM(path_list, i, coord);
}
return path_list;
}
PyObject* UIGrid::py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
static const char* kwlist[] = {"root_x", "root_y", "diagonal_cost", NULL};
int root_x, root_y;
static const char* kwlist[] = {"root", "diagonal_cost", NULL};
PyObject* root_obj = NULL;
float diagonal_cost = 1.41f;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ii|f", const_cast<char**>(kwlist),
&root_x, &root_y, &diagonal_cost)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|f", const_cast<char**>(kwlist),
&root_obj, &diagonal_cost)) {
return NULL;
}
int root_x, root_y;
if (!PyPosition_FromObjectInt(root_obj, &root_x, &root_y)) {
return NULL;
}
self->data->computeDijkstra(root_x, root_y, diagonal_cost);
Py_RETURN_NONE;
}
PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args)
PyObject* UIGrid::py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
float distance = self->data->getDijkstraDistance(x, y);
if (distance < 0) {
Py_RETURN_NONE; // Invalid position
}
return PyFloat_FromDouble(distance);
}
PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args)
PyObject* UIGrid::py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
int x, y;
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
if (!PyPosition_ParseInt(args, kwds, &x, &y)) {
return NULL;
}
std::vector<std::pair<int, int>> path = self->data->getDijkstraPath(x, y);
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // Steals reference
}
return path_list;
}
PyObject* UIGrid::py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds)
{
int x1, y1, x2, y2;
static const char* kwlist[] = {"start", "end", "diagonal_cost", NULL};
PyObject* start_obj = NULL;
PyObject* end_obj = NULL;
float diagonal_cost = 1.41f;
static const char* kwlist[] = {"x1", "y1", "x2", "y2", "diagonal_cost", NULL};
if (!PyArg_ParseTupleAndKeywords(args, kwds, "iiii|f", const_cast<char**>(kwlist),
&x1, &y1, &x2, &y2, &diagonal_cost)) {
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|f", const_cast<char**>(kwlist),
&start_obj, &end_obj, &diagonal_cost)) {
return NULL;
}
int x1, y1, x2, y2;
if (!PyPosition_FromObjectInt(start_obj, &x1, &y1)) {
return NULL;
}
if (!PyPosition_FromObjectInt(end_obj, &x2, &y2)) {
return NULL;
}
// Compute A* path
std::vector<std::pair<int, int>> path = self->data->computeAStarPath(x1, y1, x2, y2, diagonal_cost);
// Convert to Python list
PyObject* path_list = PyList_New(path.size());
for (size_t i = 0; i < path.size(); i++) {
PyObject* pos = Py_BuildValue("(ii)", path[i].first, path[i].second);
PyList_SetItem(path_list, i, pos); // Steals reference
}
return path_list;
}
@ -1812,72 +1829,63 @@ PyObject* UIGrid::py_center_camera(PyUIGridObject* self, PyObject* args) {
PyMethodDef UIGrid::methods[] = {
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
"compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
"Compute field of view from a position.\n\n"
"Args:\n"
" x: X coordinate of the viewer\n"
" y: Y coordinate of the viewer\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" radius: Maximum view distance (0 = unlimited)\n"
" light_walls: Whether walls are lit when visible\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
"is_in_fov(x: int, y: int) -> bool\n\n"
"Updates the internal FOV state. Use is_in_fov(pos) to query visibility."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
"is_in_fov(pos) -> bool\n\n"
"Check if a cell is in the field of view.\n\n"
"Args:\n"
" x: X coordinate to check\n"
" y: Y coordinate to check\n\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" True if the cell is visible, False otherwise\n\n"
"Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Find A* path between two points.\n\n"
"Args:\n"
" x1: Starting X coordinate\n"
" y1: Starting Y coordinate\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n"
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n"
"Compute Dijkstra map from root position.\n\n"
"Args:\n"
" root_x: X coordinate of the root/target\n"
" root_y: Y coordinate of the root/target\n"
" root: Root position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Precomputes distances from all reachable cells to the root.\n"
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
"Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_distance(pos) -> Optional[float]\n\n"
"Get distance from Dijkstra root to position.\n\n"
"Args:\n"
" x: X coordinate to query\n"
" y: Y coordinate to query\n\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" Distance as float, or None if position is unreachable or invalid\n\n"
"Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
"Get path from position to Dijkstra root.\n\n"
"Args:\n"
" x: Starting X coordinate\n"
" y: Starting Y coordinate\n\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" List of (x, y) tuples representing path to root, empty if unreachable\n\n"
"Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
"compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Compute A* path between two points.\n\n"
"Args:\n"
" x1: Starting X coordinate\n"
" y1: Starting Y coordinate\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
@ -1917,72 +1925,63 @@ PyMethodDef UIGrid_all_methods[] = {
UIDRAWABLE_METHODS,
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
{"compute_fov", (PyCFunction)UIGrid::py_compute_fov, METH_VARARGS | METH_KEYWORDS,
"compute_fov(x: int, y: int, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
"compute_fov(pos, radius: int = 0, light_walls: bool = True, algorithm: int = FOV_BASIC) -> None\n\n"
"Compute field of view from a position.\n\n"
"Args:\n"
" x: X coordinate of the viewer\n"
" y: Y coordinate of the viewer\n"
" pos: Position as (x, y) tuple, list, or Vector\n"
" radius: Maximum view distance (0 = unlimited)\n"
" light_walls: Whether walls are lit when visible\n"
" algorithm: FOV algorithm to use (FOV_BASIC, FOV_DIAMOND, FOV_SHADOW, FOV_PERMISSIVE_0-8)\n\n"
"Updates the internal FOV state. Use is_in_fov(x, y) to query visibility."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS,
"is_in_fov(x: int, y: int) -> bool\n\n"
"Updates the internal FOV state. Use is_in_fov(pos) to query visibility."},
{"is_in_fov", (PyCFunction)UIGrid::py_is_in_fov, METH_VARARGS | METH_KEYWORDS,
"is_in_fov(pos) -> bool\n\n"
"Check if a cell is in the field of view.\n\n"
"Args:\n"
" x: X coordinate to check\n"
" y: Y coordinate to check\n\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" True if the cell is visible, False otherwise\n\n"
"Must call compute_fov() first to calculate visibility."},
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
{"find_path", (PyCFunction)UIGrid::py_find_path, METH_VARARGS | METH_KEYWORDS,
"find_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Find A* path between two points.\n\n"
"Args:\n"
" x1: Starting X coordinate\n"
" y1: Starting Y coordinate\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
"Uses A* algorithm with walkability from grid cells."},
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root_x: int, root_y: int, diagonal_cost: float = 1.41) -> None\n\n"
{"compute_dijkstra", (PyCFunction)UIGrid::py_compute_dijkstra, METH_VARARGS | METH_KEYWORDS,
"compute_dijkstra(root, diagonal_cost: float = 1.41) -> None\n\n"
"Compute Dijkstra map from root position.\n\n"
"Args:\n"
" root_x: X coordinate of the root/target\n"
" root_y: Y coordinate of the root/target\n"
" root: Root position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Precomputes distances from all reachable cells to the root.\n"
"Use get_dijkstra_distance() and get_dijkstra_path() to query results.\n"
"Useful for multiple entities pathfinding to the same target."},
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS,
"get_dijkstra_distance(x: int, y: int) -> Optional[float]\n\n"
{"get_dijkstra_distance", (PyCFunction)UIGrid::py_get_dijkstra_distance, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_distance(pos) -> Optional[float]\n\n"
"Get distance from Dijkstra root to position.\n\n"
"Args:\n"
" x: X coordinate to query\n"
" y: Y coordinate to query\n\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" Distance as float, or None if position is unreachable or invalid\n\n"
"Must call compute_dijkstra() first."},
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS,
"get_dijkstra_path(x: int, y: int) -> List[Tuple[int, int]]\n\n"
{"get_dijkstra_path", (PyCFunction)UIGrid::py_get_dijkstra_path, METH_VARARGS | METH_KEYWORDS,
"get_dijkstra_path(pos) -> List[Tuple[int, int]]\n\n"
"Get path from position to Dijkstra root.\n\n"
"Args:\n"
" x: Starting X coordinate\n"
" y: Starting Y coordinate\n\n"
" pos: Position as (x, y) tuple, list, or Vector\n\n"
"Returns:\n"
" List of (x, y) tuples representing path to root, empty if unreachable\n\n"
"Must call compute_dijkstra() first. Path includes start but not root position."},
{"compute_astar_path", (PyCFunction)UIGrid::py_compute_astar_path, METH_VARARGS | METH_KEYWORDS,
"compute_astar_path(x1: int, y1: int, x2: int, y2: int, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"compute_astar_path(start, end, diagonal_cost: float = 1.41) -> List[Tuple[int, int]]\n\n"
"Compute A* path between two points.\n\n"
"Args:\n"
" x1: Starting X coordinate\n"
" y1: Starting Y coordinate\n"
" x2: Target X coordinate\n"
" y2: Target Y coordinate\n"
" start: Starting position as (x, y) tuple, list, or Vector\n"
" end: Target position as (x, y) tuple, list, or Vector\n"
" diagonal_cost: Cost of diagonal movement (default: 1.41)\n\n"
"Returns:\n"
" List of (x, y) tuples representing the path, empty list if no path exists\n\n"
@ -2055,7 +2054,7 @@ PyGetSetDef UIGrid::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
"Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UIGRID},
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
@ -2083,11 +2082,11 @@ PyGetSetDef UIGrid::getsetters[] = {
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIGRID),
// #142 - Grid cell mouse events
{"on_cell_enter", (getter)UIGrid::get_on_cell_enter, (setter)UIGrid::set_on_cell_enter,
"Callback when mouse enters a grid cell. Called with (cell_x, cell_y).", NULL},
"Callback when mouse enters a grid cell. Called with (cell_pos: Vector).", NULL},
{"on_cell_exit", (getter)UIGrid::get_on_cell_exit, (setter)UIGrid::set_on_cell_exit,
"Callback when mouse exits a grid cell. Called with (cell_x, cell_y).", NULL},
"Callback when mouse exits a grid cell. Called with (cell_pos: Vector).", NULL},
{"on_cell_click", (getter)UIGrid::get_on_cell_click, (setter)UIGrid::set_on_cell_click,
"Callback when a grid cell is clicked. Called with (cell_x, cell_y).", NULL},
"Callback when a grid cell is clicked. Called with (cell_pos: Vector).", NULL},
{"hovered_cell", (getter)UIGrid::get_hovered_cell, NULL,
"Currently hovered cell as (x, y) tuple, or None if not hovering.", NULL},
{NULL} /* Sentinel */
@ -2249,29 +2248,39 @@ void UIGrid::updateCellHover(sf::Vector2f mousepos) {
if (new_cell != hovered_cell) {
// Fire exit callback for old cell
if (hovered_cell.has_value() && on_cell_exit_callable) {
PyObject* args = Py_BuildValue("(ii)", hovered_cell->x, hovered_cell->y);
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell exit callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
// Create Vector object for cell position
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)hovered_cell->x, (float)hovered_cell->y);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_exit_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell exit callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
}
}
// Fire enter callback for new cell
if (new_cell.has_value() && on_cell_enter_callable) {
PyObject* args = Py_BuildValue("(ii)", new_cell->x, new_cell->y);
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell enter callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
// Create Vector object for cell position
PyObject* cell_pos = PyObject_CallFunction((PyObject*)&mcrfpydef::PyVectorType, "ff", (float)new_cell->x, (float)new_cell->y);
if (cell_pos) {
PyObject* args = Py_BuildValue("(O)", cell_pos);
Py_DECREF(cell_pos);
PyObject* result = PyObject_CallObject(on_cell_enter_callable->borrow(), args);
Py_DECREF(args);
if (!result) {
std::cerr << "Cell enter callback raised an exception:" << std::endl;
PyErr_Print();
PyErr_Clear();
} else {
Py_DECREF(result);
}
}
}

View file

@ -165,11 +165,11 @@ public:
static int set_fov_radius(PyUIGridObject* self, PyObject* value, void* closure);
static PyObject* py_at(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, PyObject* kwds);
static PyObject* py_find_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_dijkstra(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args);
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args);
static PyObject* py_get_dijkstra_distance(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_get_dijkstra_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds);
static PyObject* py_entities_in_radius(PyUIGridObject* self, PyObject* args, PyObject* kwds); // #115
static PyObject* py_center_camera(PyUIGridObject* self, PyObject* args); // #169

View file

@ -451,7 +451,7 @@ PyGetSetDef UILine::getsetters[] = {
{"thickness", (getter)UILine::get_thickness, (setter)UILine::set_thickness,
MCRF_PROPERTY(thickness, "Line thickness in pixels."), NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, "Callable executed when line is clicked."),
MCRF_PROPERTY(on_click, "Callable executed when line is clicked. Function receives (pos: Vector, button: str, action: str)."),
(void*)PyObjectsEnum::UILINE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index, "Z-order for rendering (lower values rendered first)."),

View file

@ -343,7 +343,7 @@ PyGetSetDef UISprite::getsetters[] = {
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click,
"Callable executed when object is clicked. "
"Function receives (x, y) coordinates of click."
"Function receives (pos: Vector, button: str, action: str)."
), (void*)PyObjectsEnum::UISPRITE},
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int,
MCRF_PROPERTY(z_index,