using custom libtcod-headless 2.2.2 feature branch: fixes to convolution, gradient method
This commit is contained in:
parent
09fa4f4665
commit
39a12028a0
3 changed files with 565 additions and 665 deletions
|
|
@ -9,6 +9,7 @@
|
||||||
#include <ctime> // For time-based seeds
|
#include <ctime> // For time-based seeds
|
||||||
#include <vector> // For BSP node collection
|
#include <vector> // For BSP node collection
|
||||||
#include <algorithm> // For std::min
|
#include <algorithm> // For std::min
|
||||||
|
#include <cfloat> // For FLT_MAX
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Region Parameter System - standardized handling of pos, source_pos, size
|
// Region Parameter System - standardized handling of pos, source_pos, size
|
||||||
|
|
@ -224,7 +225,7 @@ PyGetSetDef PyHeightMap::getsetters[] = {
|
||||||
PyMappingMethods PyHeightMap::mapping_methods = {
|
PyMappingMethods PyHeightMap::mapping_methods = {
|
||||||
.mp_length = nullptr, // __len__ not needed
|
.mp_length = nullptr, // __len__ not needed
|
||||||
.mp_subscript = (binaryfunc)PyHeightMap::subscript, // __getitem__
|
.mp_subscript = (binaryfunc)PyHeightMap::subscript, // __getitem__
|
||||||
.mp_ass_subscript = nullptr // __setitem__ (read-only for now)
|
.mp_ass_subscript = (objobjargproc)PyHeightMap::subscript_assign // __setitem__
|
||||||
};
|
};
|
||||||
|
|
||||||
// Method definitions
|
// Method definitions
|
||||||
|
|
@ -438,16 +439,58 @@ PyMethodDef PyHeightMap::methods[] = {
|
||||||
MCRF_ARG("iterations", "Number of smoothing passes (default 1)")
|
MCRF_ARG("iterations", "Number of smoothing passes (default 1)")
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
MCRF_RETURNS("HeightMap: self, for method chaining")
|
||||||
)},
|
)},
|
||||||
{"kernel_transform", (PyCFunction)PyHeightMap::kernel_transform, METH_VARARGS | METH_KEYWORDS,
|
// Convolution methods (libtcod 2.2.2+)
|
||||||
MCRF_METHOD(HeightMap, kernel_transform,
|
{"sparse_kernel", (PyCFunction)PyHeightMap::sparse_kernel, METH_VARARGS | METH_KEYWORDS,
|
||||||
MCRF_SIG("(weights: dict[tuple[int, int], float], *, min: float = 0.0, max: float = 1e6)", "HeightMap"),
|
MCRF_METHOD(HeightMap, sparse_kernel,
|
||||||
MCRF_DESC("Apply a convolution kernel to the heightmap. Keys are (dx, dy) offsets, values are weights."),
|
MCRF_SIG("(weights: dict[tuple[int, int], float], *, min_level: float = -inf, max_level: float = inf)", "HeightMap"),
|
||||||
|
MCRF_DESC("Apply sparse convolution kernel, returning a NEW HeightMap with results."),
|
||||||
MCRF_ARGS_START
|
MCRF_ARGS_START
|
||||||
MCRF_ARG("weights", "Dict mapping (dx, dy) offsets to weight values")
|
MCRF_ARG("weights", "Dict mapping (dx, dy) offsets to weight values")
|
||||||
MCRF_ARG("min", "Only transform cells with value >= min (default: 0.0)")
|
MCRF_ARG("min_level", "Only transform cells with value >= min_level (default: -inf)")
|
||||||
MCRF_ARG("max", "Only transform cells with value <= max (default: 1e6)")
|
MCRF_ARG("max_level", "Only transform cells with value <= max_level (default: inf)")
|
||||||
MCRF_RETURNS("HeightMap: self, for method chaining")
|
MCRF_RETURNS("HeightMap: new heightmap with convolution result")
|
||||||
MCRF_NOTE("Use for edge detection, blur, sharpen, and other convolution effects")
|
)},
|
||||||
|
{"sparse_kernel_from", (PyCFunction)PyHeightMap::sparse_kernel_from, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(HeightMap, sparse_kernel_from,
|
||||||
|
MCRF_SIG("(source: HeightMap, weights: dict[tuple[int, int], float], *, min_level: float = -inf, max_level: float = inf)", "None"),
|
||||||
|
MCRF_DESC("Apply sparse convolution from source heightmap into self (for reusing destination buffers)."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("source", "Source HeightMap to convolve from")
|
||||||
|
MCRF_ARG("weights", "Dict mapping (dx, dy) offsets to weight values")
|
||||||
|
MCRF_ARG("min_level", "Only transform cells with value >= min_level (default: -inf)")
|
||||||
|
MCRF_ARG("max_level", "Only transform cells with value <= max_level (default: inf)")
|
||||||
|
MCRF_RETURNS("None")
|
||||||
|
)},
|
||||||
|
{"kernel3", (PyCFunction)PyHeightMap::kernel3, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(HeightMap, kernel3,
|
||||||
|
MCRF_SIG("(weights: Sequence[float], *, normalize: bool = True)", "HeightMap"),
|
||||||
|
MCRF_DESC("Apply 3x3 convolution kernel, returning a NEW HeightMap with results."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("weights", "9 floats as flat list [w0..w8] or nested [[r0],[r1],[r2]]")
|
||||||
|
MCRF_ARG("normalize", "Divide result by sum of weights (default: True)")
|
||||||
|
MCRF_RETURNS("HeightMap: new heightmap with convolution result")
|
||||||
|
MCRF_NOTE("Kernel layout: [0,1,2] = top row, [3,4,5] = middle, [6,7,8] = bottom")
|
||||||
|
)},
|
||||||
|
{"kernel3_from", (PyCFunction)PyHeightMap::kernel3_from, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(HeightMap, kernel3_from,
|
||||||
|
MCRF_SIG("(source: HeightMap, weights: Sequence[float], *, normalize: bool = True)", "None"),
|
||||||
|
MCRF_DESC("Apply 3x3 convolution from source heightmap into self (for reusing destination buffers)."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("source", "Source HeightMap to convolve from")
|
||||||
|
MCRF_ARG("weights", "9 floats as flat list [w0..w8] or nested [[r0],[r1],[r2]]")
|
||||||
|
MCRF_ARG("normalize", "Divide result by sum of weights (default: True)")
|
||||||
|
MCRF_RETURNS("None")
|
||||||
|
MCRF_NOTE("Kernel layout: [0,1,2] = top row, [3,4,5] = middle, [6,7,8] = bottom")
|
||||||
|
)},
|
||||||
|
{"gradients", (PyCFunction)PyHeightMap::gradients, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
MCRF_METHOD(HeightMap, gradients,
|
||||||
|
MCRF_SIG("(dx=True, dy=True)", "HeightMap | tuple[HeightMap, HeightMap] | None"),
|
||||||
|
MCRF_DESC("Compute gradient (partial derivatives) of the heightmap."),
|
||||||
|
MCRF_ARGS_START
|
||||||
|
MCRF_ARG("dx", "HeightMap to write dx into, True to create new, False to skip")
|
||||||
|
MCRF_ARG("dy", "HeightMap to write dy into, True to create new, False to skip")
|
||||||
|
MCRF_RETURNS("Depends on args: (dx, dy) tuple, single HeightMap, or None")
|
||||||
|
MCRF_NOTE("Pass existing HeightMaps for dx/dy to reuse buffers in hot loops")
|
||||||
)},
|
)},
|
||||||
// Combination operations (#194) - with region support
|
// Combination operations (#194) - with region support
|
||||||
{"add", (PyCFunction)PyHeightMap::add, METH_VARARGS | METH_KEYWORDS,
|
{"add", (PyCFunction)PyHeightMap::add, METH_VARARGS | METH_KEYWORDS,
|
||||||
|
|
@ -1112,6 +1155,48 @@ PyObject* PyHeightMap::subscript(PyHeightMapObject* self, PyObject* key)
|
||||||
return PyFloat_FromDouble(value);
|
return PyFloat_FromDouble(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscript assign: hmap[x, y] = value (shorthand for set)
|
||||||
|
int PyHeightMap::subscript_assign(PyHeightMapObject* self, PyObject* key, PyObject* value)
|
||||||
|
{
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle deletion (not supported)
|
||||||
|
if (value == nullptr) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "cannot delete HeightMap elements");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int x, y;
|
||||||
|
if (!PyPosition_FromObjectInt(key, &x, &y)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds check
|
||||||
|
if (x < 0 || x >= self->heightmap->w || y < 0 || y >= self->heightmap->h) {
|
||||||
|
PyErr_Format(PyExc_IndexError,
|
||||||
|
"Position (%d, %d) out of bounds for HeightMap of size (%d, %d)",
|
||||||
|
x, y, self->heightmap->w, self->heightmap->h);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse value as float
|
||||||
|
float fval;
|
||||||
|
if (PyFloat_Check(value)) {
|
||||||
|
fval = static_cast<float>(PyFloat_AsDouble(value));
|
||||||
|
} else if (PyLong_Check(value)) {
|
||||||
|
fval = static_cast<float>(PyLong_AsLong(value));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "value must be numeric (int or float)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TCOD_heightmap_set_value(self->heightmap, x, y, fval);
|
||||||
|
return 0; // Success
|
||||||
|
}
|
||||||
|
|
||||||
// Threshold operations (#197) - return NEW HeightMaps
|
// Threshold operations (#197) - return NEW HeightMaps
|
||||||
|
|
||||||
// Helper: Parse range from tuple or list
|
// Helper: Parse range from tuple or list
|
||||||
|
|
@ -1148,6 +1233,9 @@ static bool ParseRange(PyObject* range_obj, float* min_val, float* max_val)
|
||||||
return !PyErr_Occurred();
|
return !PyErr_Occurred();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward declaration for helper used by convolution methods
|
||||||
|
static PyHeightMapObject* validateOtherHeightMapType(PyObject* other_obj, const char* method_name);
|
||||||
|
|
||||||
// Helper: Create a new HeightMap object with same dimensions
|
// Helper: Create a new HeightMap object with same dimensions
|
||||||
static PyHeightMapObject* CreateNewHeightMap(int width, int height)
|
static PyHeightMapObject* CreateNewHeightMap(int width, int height)
|
||||||
{
|
{
|
||||||
|
|
@ -1630,14 +1718,219 @@ PyObject* PyHeightMap::smooth(PyHeightMapObject* self, PyObject* args, PyObject*
|
||||||
return (PyObject*)self;
|
return (PyObject*)self;
|
||||||
}
|
}
|
||||||
|
|
||||||
// kernel_transform - apply custom convolution kernel (#198)
|
// =============================================================================
|
||||||
PyObject* PyHeightMap::kernel_transform(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
// Convolution methods (libtcod 2.2.2+)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Helper: Parse weights dict into arrays for sparse kernel
|
||||||
|
// Returns kernel_size on success, -1 on error
|
||||||
|
static Py_ssize_t ParseWeightsDict(PyObject* weights_dict,
|
||||||
|
std::vector<int>& dx,
|
||||||
|
std::vector<int>& dy,
|
||||||
|
std::vector<float>& weight)
|
||||||
|
{
|
||||||
|
if (!PyDict_Check(weights_dict)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights must be a dict");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t kernel_size = PyDict_Size(weights_dict);
|
||||||
|
if (kernel_size <= 0) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "weights dict cannot be empty");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dx.resize(kernel_size);
|
||||||
|
dy.resize(kernel_size);
|
||||||
|
weight.resize(kernel_size);
|
||||||
|
|
||||||
|
PyObject* key;
|
||||||
|
PyObject* value;
|
||||||
|
Py_ssize_t pos = 0;
|
||||||
|
Py_ssize_t idx = 0;
|
||||||
|
|
||||||
|
while (PyDict_Next(weights_dict, &pos, &key, &value)) {
|
||||||
|
int key_dx = 0, key_dy = 0;
|
||||||
|
|
||||||
|
if (PyTuple_Check(key) && PyTuple_Size(key) == 2) {
|
||||||
|
PyObject* x_obj = PyTuple_GetItem(key, 0);
|
||||||
|
PyObject* y_obj = PyTuple_GetItem(key, 1);
|
||||||
|
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights keys must be (int, int) tuples");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
key_dx = PyLong_AsLong(x_obj);
|
||||||
|
key_dy = PyLong_AsLong(y_obj);
|
||||||
|
} else if (PyList_Check(key) && PyList_Size(key) == 2) {
|
||||||
|
PyObject* x_obj = PyList_GetItem(key, 0);
|
||||||
|
PyObject* y_obj = PyList_GetItem(key, 1);
|
||||||
|
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights keys must be [int, int] lists");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
key_dx = PyLong_AsLong(x_obj);
|
||||||
|
key_dy = PyLong_AsLong(y_obj);
|
||||||
|
} else if (PyObject_HasAttrString(key, "x") && PyObject_HasAttrString(key, "y")) {
|
||||||
|
PyObject* x_attr = PyObject_GetAttrString(key, "x");
|
||||||
|
PyObject* y_attr = PyObject_GetAttrString(key, "y");
|
||||||
|
if (!x_attr || !y_attr) {
|
||||||
|
Py_XDECREF(x_attr);
|
||||||
|
Py_XDECREF(y_attr);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights keys must be (dx, dy) tuples, lists, or Vectors");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
key_dx = static_cast<int>(PyFloat_Check(x_attr) ? PyFloat_AsDouble(x_attr) : PyLong_AsLong(x_attr));
|
||||||
|
key_dy = static_cast<int>(PyFloat_Check(y_attr) ? PyFloat_AsDouble(y_attr) : PyLong_AsLong(y_attr));
|
||||||
|
Py_DECREF(x_attr);
|
||||||
|
Py_DECREF(y_attr);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights keys must be (dx, dy) tuples, lists, or Vectors");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
float w = 0.0f;
|
||||||
|
if (PyFloat_Check(value)) {
|
||||||
|
w = static_cast<float>(PyFloat_AsDouble(value));
|
||||||
|
} else if (PyLong_Check(value)) {
|
||||||
|
w = static_cast<float>(PyLong_AsLong(value));
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights values must be numeric (int or float)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
dx[idx] = key_dx;
|
||||||
|
dy[idx] = key_dy;
|
||||||
|
weight[idx] = w;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return kernel_size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Parse 3x3 kernel from flat or nested sequence
|
||||||
|
// Returns true on success, sets error and returns false on failure
|
||||||
|
static bool ParseKernel3(PyObject* weights_obj, float kernel[9])
|
||||||
|
{
|
||||||
|
// Check if it's a sequence
|
||||||
|
if (!PySequence_Check(weights_obj)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "weights must be a sequence (list or tuple)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_ssize_t len = PySequence_Size(weights_obj);
|
||||||
|
|
||||||
|
if (len == 9) {
|
||||||
|
// Flat format: [w0, w1, w2, w3, w4, w5, w6, w7, w8]
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
PyObject* item = PySequence_GetItem(weights_obj, i);
|
||||||
|
if (!item) return false;
|
||||||
|
|
||||||
|
if (PyFloat_Check(item)) {
|
||||||
|
kernel[i] = static_cast<float>(PyFloat_AsDouble(item));
|
||||||
|
} else if (PyLong_Check(item)) {
|
||||||
|
kernel[i] = static_cast<float>(PyLong_AsLong(item));
|
||||||
|
} else {
|
||||||
|
Py_DECREF(item);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "kernel weights must be numeric");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Py_DECREF(item);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else if (len == 3) {
|
||||||
|
// Nested format: [[r0], [r1], [r2]] where each row has 3 elements
|
||||||
|
for (int row = 0; row < 3; row++) {
|
||||||
|
PyObject* row_obj = PySequence_GetItem(weights_obj, row);
|
||||||
|
if (!row_obj) return false;
|
||||||
|
|
||||||
|
if (!PySequence_Check(row_obj) || PySequence_Size(row_obj) != 3) {
|
||||||
|
Py_DECREF(row_obj);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "nested kernel must have 3 rows of 3 elements each");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int col = 0; col < 3; col++) {
|
||||||
|
PyObject* item = PySequence_GetItem(row_obj, col);
|
||||||
|
if (!item) {
|
||||||
|
Py_DECREF(row_obj);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PyFloat_Check(item)) {
|
||||||
|
kernel[row * 3 + col] = static_cast<float>(PyFloat_AsDouble(item));
|
||||||
|
} else if (PyLong_Check(item)) {
|
||||||
|
kernel[row * 3 + col] = static_cast<float>(PyLong_AsLong(item));
|
||||||
|
} else {
|
||||||
|
Py_DECREF(item);
|
||||||
|
Py_DECREF(row_obj);
|
||||||
|
PyErr_SetString(PyExc_TypeError, "kernel weights must be numeric");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Py_DECREF(item);
|
||||||
|
}
|
||||||
|
Py_DECREF(row_obj);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "weights must be 9 elements (flat) or 3x3 nested");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sparse_kernel_from - apply sparse convolution from source into self
|
||||||
|
PyObject* PyHeightMap::sparse_kernel_from(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
PyObject* source_obj = nullptr;
|
||||||
|
PyObject* weights_dict = nullptr;
|
||||||
|
float min_level = -FLT_MAX;
|
||||||
|
float max_level = FLT_MAX;
|
||||||
|
|
||||||
|
static const char* kwlist[] = {"source", "weights", "min_level", "max_level", nullptr};
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|ff", const_cast<char**>(kwlist),
|
||||||
|
&source_obj, &weights_dict, &min_level, &max_level)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate source
|
||||||
|
PyHeightMapObject* source = validateOtherHeightMapType(source_obj, "sparse_kernel_from");
|
||||||
|
if (!source) return nullptr;
|
||||||
|
|
||||||
|
// Check dimensions match
|
||||||
|
if (source->heightmap->w != self->heightmap->w ||
|
||||||
|
source->heightmap->h != self->heightmap->h) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "source and destination HeightMaps must have same dimensions");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse weights
|
||||||
|
std::vector<int> dx, dy;
|
||||||
|
std::vector<float> weight;
|
||||||
|
Py_ssize_t kernel_size = ParseWeightsDict(weights_dict, dx, dy, weight);
|
||||||
|
if (kernel_size < 0) return nullptr;
|
||||||
|
|
||||||
|
// Apply the kernel transform
|
||||||
|
TCOD_heightmap_kernel_transform_hm(source->heightmap, self->heightmap,
|
||||||
|
static_cast<int>(kernel_size),
|
||||||
|
dx.data(), dy.data(), weight.data(),
|
||||||
|
min_level, max_level);
|
||||||
|
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sparse_kernel - apply sparse convolution, return new HeightMap
|
||||||
|
PyObject* PyHeightMap::sparse_kernel(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
PyObject* weights_dict = nullptr;
|
PyObject* weights_dict = nullptr;
|
||||||
float min_level = 0.0f;
|
float min_level = -FLT_MAX;
|
||||||
float max_level = 1000000.0f;
|
float max_level = FLT_MAX;
|
||||||
|
|
||||||
static const char* kwlist[] = {"weights", "min", "max", nullptr};
|
static const char* kwlist[] = {"weights", "min_level", "max_level", nullptr};
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ff", const_cast<char**>(kwlist),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|ff", const_cast<char**>(kwlist),
|
||||||
&weights_dict, &min_level, &max_level)) {
|
&weights_dict, &min_level, &max_level)) {
|
||||||
|
|
@ -1649,93 +1942,200 @@ PyObject* PyHeightMap::kernel_transform(PyHeightMapObject* self, PyObject* args,
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!PyDict_Check(weights_dict)) {
|
// Create new HeightMap for result
|
||||||
PyErr_SetString(PyExc_TypeError, "weights must be a dict");
|
PyHeightMapObject* result = CreateNewHeightMap(self->heightmap->w, self->heightmap->h);
|
||||||
|
if (!result) return nullptr;
|
||||||
|
|
||||||
|
// Parse weights
|
||||||
|
std::vector<int> dx, dy;
|
||||||
|
std::vector<float> weight;
|
||||||
|
Py_ssize_t kernel_size = ParseWeightsDict(weights_dict, dx, dy, weight);
|
||||||
|
if (kernel_size < 0) {
|
||||||
|
Py_DECREF(result);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
Py_ssize_t kernel_size = PyDict_Size(weights_dict);
|
|
||||||
if (kernel_size <= 0) {
|
|
||||||
PyErr_SetString(PyExc_ValueError, "weights dict cannot be empty");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate arrays for the kernel
|
|
||||||
std::vector<int> dx(kernel_size);
|
|
||||||
std::vector<int> dy(kernel_size);
|
|
||||||
std::vector<float> weight(kernel_size);
|
|
||||||
|
|
||||||
// Iterate through the dict
|
|
||||||
PyObject* key;
|
|
||||||
PyObject* value;
|
|
||||||
Py_ssize_t pos = 0;
|
|
||||||
Py_ssize_t idx = 0;
|
|
||||||
|
|
||||||
while (PyDict_Next(weights_dict, &pos, &key, &value)) {
|
|
||||||
// Parse the key as (dx, dy) - can be tuple, list, or Vector
|
|
||||||
int key_dx = 0, key_dy = 0;
|
|
||||||
|
|
||||||
if (PyTuple_Check(key) && PyTuple_Size(key) == 2) {
|
|
||||||
PyObject* x_obj = PyTuple_GetItem(key, 0);
|
|
||||||
PyObject* y_obj = PyTuple_GetItem(key, 1);
|
|
||||||
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "weights keys must be (int, int) tuples");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
key_dx = PyLong_AsLong(x_obj);
|
|
||||||
key_dy = PyLong_AsLong(y_obj);
|
|
||||||
} else if (PyList_Check(key) && PyList_Size(key) == 2) {
|
|
||||||
PyObject* x_obj = PyList_GetItem(key, 0);
|
|
||||||
PyObject* y_obj = PyList_GetItem(key, 1);
|
|
||||||
if (!PyLong_Check(x_obj) || !PyLong_Check(y_obj)) {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "weights keys must be [int, int] lists");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
key_dx = PyLong_AsLong(x_obj);
|
|
||||||
key_dy = PyLong_AsLong(y_obj);
|
|
||||||
} else if (PyObject_HasAttrString(key, "x") && PyObject_HasAttrString(key, "y")) {
|
|
||||||
// Vector-like object
|
|
||||||
PyObject* x_attr = PyObject_GetAttrString(key, "x");
|
|
||||||
PyObject* y_attr = PyObject_GetAttrString(key, "y");
|
|
||||||
if (!x_attr || !y_attr) {
|
|
||||||
Py_XDECREF(x_attr);
|
|
||||||
Py_XDECREF(y_attr);
|
|
||||||
PyErr_SetString(PyExc_TypeError, "weights keys must be (dx, dy) tuples, lists, or Vectors");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
key_dx = static_cast<int>(PyFloat_Check(x_attr) ? PyFloat_AsDouble(x_attr) : PyLong_AsLong(x_attr));
|
|
||||||
key_dy = static_cast<int>(PyFloat_Check(y_attr) ? PyFloat_AsDouble(y_attr) : PyLong_AsLong(y_attr));
|
|
||||||
Py_DECREF(x_attr);
|
|
||||||
Py_DECREF(y_attr);
|
|
||||||
} else {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "weights keys must be (dx, dy) tuples, lists, or Vectors");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the value as float
|
|
||||||
float w = 0.0f;
|
|
||||||
if (PyFloat_Check(value)) {
|
|
||||||
w = static_cast<float>(PyFloat_AsDouble(value));
|
|
||||||
} else if (PyLong_Check(value)) {
|
|
||||||
w = static_cast<float>(PyLong_AsLong(value));
|
|
||||||
} else {
|
|
||||||
PyErr_SetString(PyExc_TypeError, "weights values must be numeric (int or float)");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
dx[idx] = key_dx;
|
|
||||||
dy[idx] = key_dy;
|
|
||||||
weight[idx] = w;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply the kernel transform
|
// Apply the kernel transform
|
||||||
TCOD_heightmap_kernel_transform(self->heightmap, static_cast<int>(kernel_size),
|
TCOD_heightmap_kernel_transform_hm(self->heightmap, result->heightmap,
|
||||||
dx.data(), dy.data(), weight.data(),
|
static_cast<int>(kernel_size),
|
||||||
min_level, max_level);
|
dx.data(), dy.data(), weight.data(),
|
||||||
|
min_level, max_level);
|
||||||
|
|
||||||
Py_INCREF(self);
|
return (PyObject*)result;
|
||||||
return (PyObject*)self;
|
}
|
||||||
|
|
||||||
|
// kernel3_from - apply 3x3 convolution from source into self
|
||||||
|
PyObject* PyHeightMap::kernel3_from(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
PyObject* source_obj = nullptr;
|
||||||
|
PyObject* weights_obj = nullptr;
|
||||||
|
int normalize = 1; // Python bool
|
||||||
|
|
||||||
|
static const char* kwlist[] = {"source", "weights", "normalize", nullptr};
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|p", const_cast<char**>(kwlist),
|
||||||
|
&source_obj, &weights_obj, &normalize)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate source
|
||||||
|
PyHeightMapObject* source = validateOtherHeightMapType(source_obj, "kernel3_from");
|
||||||
|
if (!source) return nullptr;
|
||||||
|
|
||||||
|
// Check dimensions match
|
||||||
|
if (source->heightmap->w != self->heightmap->w ||
|
||||||
|
source->heightmap->h != self->heightmap->h) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "source and destination HeightMaps must have same dimensions");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse kernel
|
||||||
|
float kernel[9];
|
||||||
|
if (!ParseKernel3(weights_obj, kernel)) return nullptr;
|
||||||
|
|
||||||
|
// Apply convolution
|
||||||
|
TCOD_heightmap_convolve3x3(source->heightmap, self->heightmap, kernel, normalize != 0);
|
||||||
|
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kernel3 - apply 3x3 convolution, return new HeightMap
|
||||||
|
PyObject* PyHeightMap::kernel3(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
PyObject* weights_obj = nullptr;
|
||||||
|
int normalize = 1;
|
||||||
|
|
||||||
|
static const char* kwlist[] = {"weights", "normalize", nullptr};
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|p", const_cast<char**>(kwlist),
|
||||||
|
&weights_obj, &normalize)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new HeightMap for result
|
||||||
|
PyHeightMapObject* result = CreateNewHeightMap(self->heightmap->w, self->heightmap->h);
|
||||||
|
if (!result) return nullptr;
|
||||||
|
|
||||||
|
// Parse kernel
|
||||||
|
float kernel[9];
|
||||||
|
if (!ParseKernel3(weights_obj, kernel)) {
|
||||||
|
Py_DECREF(result);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply convolution
|
||||||
|
TCOD_heightmap_convolve3x3(self->heightmap, result->heightmap, kernel, normalize != 0);
|
||||||
|
|
||||||
|
return (PyObject*)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// gradients - compute partial derivatives
|
||||||
|
// Usage:
|
||||||
|
// source.gradients(dx_hm, dy_hm) - write to existing HeightMaps, return None
|
||||||
|
// dx, dy = source.gradients() - create new HeightMaps, return tuple
|
||||||
|
// dx = source.gradients(dy=False) - skip dy, return single HeightMap
|
||||||
|
PyObject* PyHeightMap::gradients(PyHeightMapObject* self, PyObject* args, PyObject* kwds)
|
||||||
|
{
|
||||||
|
PyObject* dx_arg = Py_True;
|
||||||
|
PyObject* dy_arg = Py_True;
|
||||||
|
|
||||||
|
static const char* kwlist[] = {"dx", "dy", nullptr};
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OO", const_cast<char**>(kwlist),
|
||||||
|
&dx_arg, &dy_arg)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self->heightmap) {
|
||||||
|
PyErr_SetString(PyExc_RuntimeError, "HeightMap not initialized");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
int w = self->heightmap->w;
|
||||||
|
int h = self->heightmap->h;
|
||||||
|
|
||||||
|
// Determine what to do with dx
|
||||||
|
PyHeightMapObject* dx_hm = nullptr;
|
||||||
|
bool dx_return_new = false;
|
||||||
|
bool dx_skip = false;
|
||||||
|
|
||||||
|
if (dx_arg == Py_True) {
|
||||||
|
// Create new HeightMap for dx
|
||||||
|
dx_hm = CreateNewHeightMap(w, h);
|
||||||
|
if (!dx_hm) return nullptr;
|
||||||
|
dx_return_new = true;
|
||||||
|
} else if (dx_arg == Py_False || dx_arg == Py_None) {
|
||||||
|
// Skip dx
|
||||||
|
dx_skip = true;
|
||||||
|
} else {
|
||||||
|
// Should be a HeightMap to write into
|
||||||
|
dx_hm = validateOtherHeightMapType(dx_arg, "gradients");
|
||||||
|
if (!dx_hm) return nullptr;
|
||||||
|
if (dx_hm->heightmap->w != w || dx_hm->heightmap->h != h) {
|
||||||
|
PyErr_SetString(PyExc_ValueError, "dx HeightMap must have same dimensions as source");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine what to do with dy
|
||||||
|
PyHeightMapObject* dy_hm = nullptr;
|
||||||
|
bool dy_return_new = false;
|
||||||
|
bool dy_skip = false;
|
||||||
|
|
||||||
|
if (dy_arg == Py_True) {
|
||||||
|
// Create new HeightMap for dy
|
||||||
|
dy_hm = CreateNewHeightMap(w, h);
|
||||||
|
if (!dy_hm) {
|
||||||
|
if (dx_return_new) Py_DECREF(dx_hm);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
dy_return_new = true;
|
||||||
|
} else if (dy_arg == Py_False || dy_arg == Py_None) {
|
||||||
|
// Skip dy
|
||||||
|
dy_skip = true;
|
||||||
|
} else {
|
||||||
|
// Should be a HeightMap to write into
|
||||||
|
dy_hm = validateOtherHeightMapType(dy_arg, "gradients");
|
||||||
|
if (!dy_hm) {
|
||||||
|
if (dx_return_new) Py_DECREF(dx_hm);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
if (dy_hm->heightmap->w != w || dy_hm->heightmap->h != h) {
|
||||||
|
if (dx_return_new) Py_DECREF(dx_hm);
|
||||||
|
PyErr_SetString(PyExc_ValueError, "dy HeightMap must have same dimensions as source");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the gradient function
|
||||||
|
TCOD_heightmap_gradient(self->heightmap,
|
||||||
|
dx_skip ? nullptr : dx_hm->heightmap,
|
||||||
|
dy_skip ? nullptr : dy_hm->heightmap);
|
||||||
|
|
||||||
|
// Build return value
|
||||||
|
if (dx_return_new && dy_return_new) {
|
||||||
|
// Return tuple of (dx, dy)
|
||||||
|
return Py_BuildValue("(OO)", dx_hm, dy_hm);
|
||||||
|
} else if (dx_return_new) {
|
||||||
|
// Return just dx
|
||||||
|
return (PyObject*)dx_hm;
|
||||||
|
} else if (dy_return_new) {
|
||||||
|
// Return just dy
|
||||||
|
return (PyObject*)dy_hm;
|
||||||
|
} else {
|
||||||
|
// Nothing to return
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,17 @@ public:
|
||||||
static PyObject* rain_erosion(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* rain_erosion(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* dig_bezier(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* dig_bezier(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* smooth(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* smooth(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
static PyObject* kernel_transform(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
|
// Correct convolution methods (using new libtcod functions)
|
||||||
|
static PyObject* sparse_kernel(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* sparse_kernel_from(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* kernel3(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* kernel3_from(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
static PyObject* gradients(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
// Subscript support for hmap[x, y] syntax
|
// Subscript support for hmap[x, y] syntax
|
||||||
static PyObject* subscript(PyHeightMapObject* self, PyObject* key);
|
static PyObject* subscript(PyHeightMapObject* self, PyObject* key);
|
||||||
|
static int subscript_assign(PyHeightMapObject* self, PyObject* key, PyObject* value);
|
||||||
|
|
||||||
// Combination operations (#194) - mutate self, return self for chaining, support region parameters
|
// Combination operations (#194) - mutate self, return self for chaining, support region parameters
|
||||||
static PyObject* add(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
static PyObject* add(PyHeightMapObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
|
||||||
629
stubs/mcrfpy.pyi
629
stubs/mcrfpy.pyi
|
|
@ -4,278 +4,34 @@ Core game engine interface for creating roguelike games with Python.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
|
from typing import Any, List, Dict, Tuple, Optional, Callable, Union, overload
|
||||||
from enum import IntEnum
|
|
||||||
|
|
||||||
# Type aliases
|
# Type aliases
|
||||||
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid', 'Line', 'Circle', 'Arc']
|
UIElement = Union['Frame', 'Caption', 'Sprite', 'Grid', 'Line', 'Circle', 'Arc']
|
||||||
Transition = Union[str, None]
|
Transition = Union[str, None]
|
||||||
|
|
||||||
# Enums
|
|
||||||
|
|
||||||
class Key(IntEnum):
|
|
||||||
"""Keyboard key codes.
|
|
||||||
|
|
||||||
These enum values compare equal to their legacy string equivalents
|
|
||||||
for backwards compatibility:
|
|
||||||
Key.ESCAPE == 'Escape' # True
|
|
||||||
Key.LEFT_SHIFT == 'LShift' # True
|
|
||||||
"""
|
|
||||||
# Letters
|
|
||||||
A = 0
|
|
||||||
B = 1
|
|
||||||
C = 2
|
|
||||||
D = 3
|
|
||||||
E = 4
|
|
||||||
F = 5
|
|
||||||
G = 6
|
|
||||||
H = 7
|
|
||||||
I = 8
|
|
||||||
J = 9
|
|
||||||
K = 10
|
|
||||||
L = 11
|
|
||||||
M = 12
|
|
||||||
N = 13
|
|
||||||
O = 14
|
|
||||||
P = 15
|
|
||||||
Q = 16
|
|
||||||
R = 17
|
|
||||||
S = 18
|
|
||||||
T = 19
|
|
||||||
U = 20
|
|
||||||
V = 21
|
|
||||||
W = 22
|
|
||||||
X = 23
|
|
||||||
Y = 24
|
|
||||||
Z = 25
|
|
||||||
# Number row
|
|
||||||
NUM_0 = 26
|
|
||||||
NUM_1 = 27
|
|
||||||
NUM_2 = 28
|
|
||||||
NUM_3 = 29
|
|
||||||
NUM_4 = 30
|
|
||||||
NUM_5 = 31
|
|
||||||
NUM_6 = 32
|
|
||||||
NUM_7 = 33
|
|
||||||
NUM_8 = 34
|
|
||||||
NUM_9 = 35
|
|
||||||
# Control keys
|
|
||||||
ESCAPE = 36
|
|
||||||
LEFT_CONTROL = 37
|
|
||||||
LEFT_SHIFT = 38
|
|
||||||
LEFT_ALT = 39
|
|
||||||
LEFT_SYSTEM = 40
|
|
||||||
RIGHT_CONTROL = 41
|
|
||||||
RIGHT_SHIFT = 42
|
|
||||||
RIGHT_ALT = 43
|
|
||||||
RIGHT_SYSTEM = 44
|
|
||||||
MENU = 45
|
|
||||||
# Punctuation
|
|
||||||
LEFT_BRACKET = 46
|
|
||||||
RIGHT_BRACKET = 47
|
|
||||||
SEMICOLON = 48
|
|
||||||
COMMA = 49
|
|
||||||
PERIOD = 50
|
|
||||||
APOSTROPHE = 51
|
|
||||||
SLASH = 52
|
|
||||||
BACKSLASH = 53
|
|
||||||
GRAVE = 54
|
|
||||||
EQUAL = 55
|
|
||||||
HYPHEN = 56
|
|
||||||
# Whitespace/editing
|
|
||||||
SPACE = 57
|
|
||||||
ENTER = 58
|
|
||||||
BACKSPACE = 59
|
|
||||||
TAB = 60
|
|
||||||
# Navigation
|
|
||||||
PAGE_UP = 61
|
|
||||||
PAGE_DOWN = 62
|
|
||||||
END = 63
|
|
||||||
HOME = 64
|
|
||||||
INSERT = 65
|
|
||||||
DELETE = 66
|
|
||||||
# Numpad operators
|
|
||||||
ADD = 67
|
|
||||||
SUBTRACT = 68
|
|
||||||
MULTIPLY = 69
|
|
||||||
DIVIDE = 70
|
|
||||||
# Arrows
|
|
||||||
LEFT = 71
|
|
||||||
RIGHT = 72
|
|
||||||
UP = 73
|
|
||||||
DOWN = 74
|
|
||||||
# Numpad numbers
|
|
||||||
NUMPAD_0 = 75
|
|
||||||
NUMPAD_1 = 76
|
|
||||||
NUMPAD_2 = 77
|
|
||||||
NUMPAD_3 = 78
|
|
||||||
NUMPAD_4 = 79
|
|
||||||
NUMPAD_5 = 80
|
|
||||||
NUMPAD_6 = 81
|
|
||||||
NUMPAD_7 = 82
|
|
||||||
NUMPAD_8 = 83
|
|
||||||
NUMPAD_9 = 84
|
|
||||||
# Function keys
|
|
||||||
F1 = 85
|
|
||||||
F2 = 86
|
|
||||||
F3 = 87
|
|
||||||
F4 = 88
|
|
||||||
F5 = 89
|
|
||||||
F6 = 90
|
|
||||||
F7 = 91
|
|
||||||
F8 = 92
|
|
||||||
F9 = 93
|
|
||||||
F10 = 94
|
|
||||||
F11 = 95
|
|
||||||
F12 = 96
|
|
||||||
F13 = 97
|
|
||||||
F14 = 98
|
|
||||||
F15 = 99
|
|
||||||
# Misc
|
|
||||||
PAUSE = 100
|
|
||||||
UNKNOWN = -1
|
|
||||||
|
|
||||||
class MouseButton(IntEnum):
|
|
||||||
"""Mouse button codes.
|
|
||||||
|
|
||||||
These enum values compare equal to their legacy string equivalents
|
|
||||||
for backwards compatibility:
|
|
||||||
MouseButton.LEFT == 'left' # True
|
|
||||||
MouseButton.RIGHT == 'right' # True
|
|
||||||
"""
|
|
||||||
LEFT = 0
|
|
||||||
RIGHT = 1
|
|
||||||
MIDDLE = 2
|
|
||||||
X1 = 3
|
|
||||||
X2 = 4
|
|
||||||
|
|
||||||
class InputState(IntEnum):
|
|
||||||
"""Input event states (pressed/released).
|
|
||||||
|
|
||||||
These enum values compare equal to their legacy string equivalents
|
|
||||||
for backwards compatibility:
|
|
||||||
InputState.PRESSED == 'start' # True
|
|
||||||
InputState.RELEASED == 'end' # True
|
|
||||||
"""
|
|
||||||
PRESSED = 0
|
|
||||||
RELEASED = 1
|
|
||||||
|
|
||||||
class Easing(IntEnum):
|
|
||||||
"""Easing functions for animations."""
|
|
||||||
LINEAR = 0
|
|
||||||
EASE_IN = 1
|
|
||||||
EASE_OUT = 2
|
|
||||||
EASE_IN_OUT = 3
|
|
||||||
EASE_IN_QUAD = 4
|
|
||||||
EASE_OUT_QUAD = 5
|
|
||||||
EASE_IN_OUT_QUAD = 6
|
|
||||||
EASE_IN_CUBIC = 7
|
|
||||||
EASE_OUT_CUBIC = 8
|
|
||||||
EASE_IN_OUT_CUBIC = 9
|
|
||||||
EASE_IN_QUART = 10
|
|
||||||
EASE_OUT_QUART = 11
|
|
||||||
EASE_IN_OUT_QUART = 12
|
|
||||||
EASE_IN_SINE = 13
|
|
||||||
EASE_OUT_SINE = 14
|
|
||||||
EASE_IN_OUT_SINE = 15
|
|
||||||
EASE_IN_EXPO = 16
|
|
||||||
EASE_OUT_EXPO = 17
|
|
||||||
EASE_IN_OUT_EXPO = 18
|
|
||||||
EASE_IN_CIRC = 19
|
|
||||||
EASE_OUT_CIRC = 20
|
|
||||||
EASE_IN_OUT_CIRC = 21
|
|
||||||
EASE_IN_ELASTIC = 22
|
|
||||||
EASE_OUT_ELASTIC = 23
|
|
||||||
EASE_IN_OUT_ELASTIC = 24
|
|
||||||
EASE_IN_BACK = 25
|
|
||||||
EASE_OUT_BACK = 26
|
|
||||||
EASE_IN_OUT_BACK = 27
|
|
||||||
EASE_IN_BOUNCE = 28
|
|
||||||
EASE_OUT_BOUNCE = 29
|
|
||||||
EASE_IN_OUT_BOUNCE = 30
|
|
||||||
|
|
||||||
class FOV(IntEnum):
|
|
||||||
"""Field of view algorithms for visibility calculations."""
|
|
||||||
BASIC = 0
|
|
||||||
DIAMOND = 1
|
|
||||||
SHADOW = 2
|
|
||||||
PERMISSIVE_0 = 3
|
|
||||||
PERMISSIVE_1 = 4
|
|
||||||
PERMISSIVE_2 = 5
|
|
||||||
PERMISSIVE_3 = 6
|
|
||||||
PERMISSIVE_4 = 7
|
|
||||||
PERMISSIVE_5 = 8
|
|
||||||
PERMISSIVE_6 = 9
|
|
||||||
PERMISSIVE_7 = 10
|
|
||||||
PERMISSIVE_8 = 11
|
|
||||||
RESTRICTIVE = 12
|
|
||||||
SYMMETRIC_SHADOWCAST = 13
|
|
||||||
|
|
||||||
class Alignment(IntEnum):
|
|
||||||
"""Alignment positions for automatic child positioning relative to parent bounds.
|
|
||||||
|
|
||||||
When a drawable has an alignment set and is added to a parent, its position
|
|
||||||
is automatically calculated based on the parent's bounds. The position is
|
|
||||||
updated whenever the parent is resized.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
parent = mcrfpy.Frame(pos=(0, 0), size=(400, 300))
|
|
||||||
child = mcrfpy.Caption(text="Centered!", align=mcrfpy.Alignment.CENTER)
|
|
||||||
parent.children.append(child) # child is auto-positioned to center
|
|
||||||
parent.w = 800 # child position updates automatically
|
|
||||||
|
|
||||||
Set align=None to disable automatic positioning and use manual coordinates.
|
|
||||||
"""
|
|
||||||
TOP_LEFT = 0
|
|
||||||
TOP_CENTER = 1
|
|
||||||
TOP_RIGHT = 2
|
|
||||||
CENTER_LEFT = 3
|
|
||||||
CENTER = 4
|
|
||||||
CENTER_RIGHT = 5
|
|
||||||
BOTTOM_LEFT = 6
|
|
||||||
BOTTOM_CENTER = 7
|
|
||||||
BOTTOM_RIGHT = 8
|
|
||||||
|
|
||||||
# Classes
|
# Classes
|
||||||
|
|
||||||
class Color:
|
class Color:
|
||||||
"""RGBA color representation.
|
"""SFML Color Object for RGBA colors."""
|
||||||
|
|
||||||
Note:
|
|
||||||
When accessing colors from UI elements (e.g., frame.fill_color),
|
|
||||||
you receive a COPY of the color. Modifying it doesn't affect the
|
|
||||||
original. To change a component:
|
|
||||||
|
|
||||||
# This does NOT work:
|
|
||||||
frame.fill_color.r = 255 # Modifies a temporary copy
|
|
||||||
|
|
||||||
# Do this instead:
|
|
||||||
c = frame.fill_color
|
|
||||||
c.r = 255
|
|
||||||
frame.fill_color = c
|
|
||||||
|
|
||||||
# Or use Animation for sub-properties:
|
|
||||||
anim = mcrfpy.Animation('fill_color.r', 255, 0.5, 'linear')
|
|
||||||
anim.start(frame)
|
|
||||||
"""
|
|
||||||
|
|
||||||
r: int
|
r: int
|
||||||
g: int
|
g: int
|
||||||
b: int
|
b: int
|
||||||
a: int
|
a: int
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __init__(self) -> None: ...
|
def __init__(self) -> None: ...
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
|
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
|
||||||
|
|
||||||
def from_hex(self, hex_string: str) -> 'Color':
|
def from_hex(self, hex_string: str) -> 'Color':
|
||||||
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
|
"""Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
|
||||||
...
|
...
|
||||||
|
|
||||||
def to_hex(self) -> str:
|
def to_hex(self) -> str:
|
||||||
"""Convert color to hex string format."""
|
"""Convert color to hex string format."""
|
||||||
...
|
...
|
||||||
|
|
||||||
def lerp(self, other: 'Color', t: float) -> 'Color':
|
def lerp(self, other: 'Color', t: float) -> 'Color':
|
||||||
"""Linear interpolation between two colors."""
|
"""Linear interpolation between two colors."""
|
||||||
...
|
...
|
||||||
|
|
@ -311,160 +67,12 @@ class Texture:
|
||||||
|
|
||||||
class Font:
|
class Font:
|
||||||
"""SFML Font Object for text rendering."""
|
"""SFML Font Object for text rendering."""
|
||||||
|
|
||||||
def __init__(self, filename: str) -> None: ...
|
def __init__(self, filename: str) -> None: ...
|
||||||
|
|
||||||
filename: str
|
filename: str
|
||||||
family: str
|
family: str
|
||||||
|
|
||||||
class Sound:
|
|
||||||
"""Sound effect object for short audio clips.
|
|
||||||
|
|
||||||
Sounds are loaded entirely into memory, making them suitable for
|
|
||||||
short sound effects that need to be played with minimal latency.
|
|
||||||
Multiple Sound instances can play simultaneously.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filename: str) -> None:
|
|
||||||
"""Load a sound effect from a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Path to the sound file (WAV, OGG, FLAC supported)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the file cannot be loaded
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
volume: float
|
|
||||||
"""Volume level from 0 (silent) to 100 (full volume)."""
|
|
||||||
|
|
||||||
loop: bool
|
|
||||||
"""Whether the sound loops when it reaches the end."""
|
|
||||||
|
|
||||||
playing: bool
|
|
||||||
"""True if the sound is currently playing (read-only)."""
|
|
||||||
|
|
||||||
duration: float
|
|
||||||
"""Total duration of the sound in seconds (read-only)."""
|
|
||||||
|
|
||||||
source: str
|
|
||||||
"""Filename path used to load this sound (read-only)."""
|
|
||||||
|
|
||||||
def play(self) -> None:
|
|
||||||
"""Start or resume playing the sound."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def pause(self) -> None:
|
|
||||||
"""Pause the sound. Use play() to resume."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""Stop playing and reset to the beginning."""
|
|
||||||
...
|
|
||||||
|
|
||||||
class Music:
|
|
||||||
"""Streaming music object for longer audio tracks.
|
|
||||||
|
|
||||||
Music is streamed from disk rather than loaded entirely into memory,
|
|
||||||
making it suitable for longer audio tracks like background music.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, filename: str) -> None:
|
|
||||||
"""Load a music track from a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename: Path to the music file (WAV, OGG, FLAC supported)
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If the file cannot be loaded
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
volume: float
|
|
||||||
"""Volume level from 0 (silent) to 100 (full volume)."""
|
|
||||||
|
|
||||||
loop: bool
|
|
||||||
"""Whether the music loops when it reaches the end."""
|
|
||||||
|
|
||||||
playing: bool
|
|
||||||
"""True if the music is currently playing (read-only)."""
|
|
||||||
|
|
||||||
duration: float
|
|
||||||
"""Total duration of the music in seconds (read-only)."""
|
|
||||||
|
|
||||||
position: float
|
|
||||||
"""Current playback position in seconds. Can be set to seek."""
|
|
||||||
|
|
||||||
source: str
|
|
||||||
"""Filename path used to load this music (read-only)."""
|
|
||||||
|
|
||||||
def play(self) -> None:
|
|
||||||
"""Start or resume playing the music."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def pause(self) -> None:
|
|
||||||
"""Pause the music. Use play() to resume."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
|
||||||
"""Stop playing and reset to the beginning."""
|
|
||||||
...
|
|
||||||
|
|
||||||
class Keyboard:
|
|
||||||
"""Keyboard state singleton for checking modifier keys.
|
|
||||||
|
|
||||||
Access via mcrfpy.keyboard (singleton instance).
|
|
||||||
Queries real-time keyboard state from SFML.
|
|
||||||
"""
|
|
||||||
|
|
||||||
shift: bool
|
|
||||||
"""True if either Shift key is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
ctrl: bool
|
|
||||||
"""True if either Control key is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
alt: bool
|
|
||||||
"""True if either Alt key is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
system: bool
|
|
||||||
"""True if either System key (Win/Cmd) is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
class Mouse:
|
|
||||||
"""Mouse state singleton for reading button/position state and controlling cursor.
|
|
||||||
|
|
||||||
Access via mcrfpy.mouse (singleton instance).
|
|
||||||
Queries real-time mouse state from SFML. In headless mode, returns
|
|
||||||
simulated position from mcrfpy.automation calls.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Position (read-only)
|
|
||||||
x: int
|
|
||||||
"""Current mouse X position in window coordinates (read-only)."""
|
|
||||||
|
|
||||||
y: int
|
|
||||||
"""Current mouse Y position in window coordinates (read-only)."""
|
|
||||||
|
|
||||||
pos: Vector
|
|
||||||
"""Current mouse position as Vector (read-only)."""
|
|
||||||
|
|
||||||
# Button state (read-only)
|
|
||||||
left: bool
|
|
||||||
"""True if left mouse button is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
right: bool
|
|
||||||
"""True if right mouse button is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
middle: bool
|
|
||||||
"""True if middle mouse button is currently pressed (read-only)."""
|
|
||||||
|
|
||||||
# Cursor control (read-write)
|
|
||||||
visible: bool
|
|
||||||
"""Whether the mouse cursor is visible (default: True)."""
|
|
||||||
|
|
||||||
grabbed: bool
|
|
||||||
"""Whether the mouse cursor is confined to the window (default: False)."""
|
|
||||||
|
|
||||||
class Drawable:
|
class Drawable:
|
||||||
"""Base class for all drawable UI elements."""
|
"""Base class for all drawable UI elements."""
|
||||||
|
|
||||||
|
|
@ -484,16 +92,6 @@ class Drawable:
|
||||||
# Read-only hover state (#140)
|
# Read-only hover state (#140)
|
||||||
hovered: bool
|
hovered: bool
|
||||||
|
|
||||||
# Alignment system - automatic positioning relative to parent
|
|
||||||
align: Optional[Alignment]
|
|
||||||
"""Alignment relative to parent bounds. Set to None for manual positioning."""
|
|
||||||
margin: float
|
|
||||||
"""General margin from edge when aligned (applies to both axes unless overridden)."""
|
|
||||||
horiz_margin: float
|
|
||||||
"""Horizontal margin override (0 = use general margin)."""
|
|
||||||
vert_margin: float
|
|
||||||
"""Vertical margin override (0 = use general margin)."""
|
|
||||||
|
|
||||||
def get_bounds(self) -> Tuple[float, float, float, float]:
|
def get_bounds(self) -> Tuple[float, float, float, float]:
|
||||||
"""Get bounding box as (x, y, width, height)."""
|
"""Get bounding box as (x, y, width, height)."""
|
||||||
...
|
...
|
||||||
|
|
@ -518,12 +116,7 @@ class Frame(Drawable):
|
||||||
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
|
def __init__(self, x: float = 0, y: float = 0, w: float = 0, h: float = 0,
|
||||||
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
||||||
outline: float = 0, on_click: Optional[Callable] = None,
|
outline: float = 0, on_click: Optional[Callable] = None,
|
||||||
children: Optional[List[UIElement]] = None,
|
children: Optional[List[UIElement]] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None,
|
|
||||||
size: Optional[Tuple[float, float]] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
w: float
|
w: float
|
||||||
h: float
|
h: float
|
||||||
|
|
@ -546,12 +139,7 @@ class Caption(Drawable):
|
||||||
def __init__(self, text: str = '', x: float = 0, y: float = 0,
|
def __init__(self, text: str = '', x: float = 0, y: float = 0,
|
||||||
font: Optional[Font] = None, fill_color: Optional[Color] = None,
|
font: Optional[Font] = None, fill_color: Optional[Color] = None,
|
||||||
outline_color: Optional[Color] = None, outline: float = 0,
|
outline_color: Optional[Color] = None, outline: float = 0,
|
||||||
on_click: Optional[Callable] = None,
|
on_click: Optional[Callable] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None,
|
|
||||||
size: Optional[Tuple[float, float]] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
text: str
|
text: str
|
||||||
font: Font
|
font: Font
|
||||||
|
|
@ -573,12 +161,7 @@ class Sprite(Drawable):
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
|
def __init__(self, x: float = 0, y: float = 0, texture: Optional[Texture] = None,
|
||||||
sprite_index: int = 0, scale: float = 1.0,
|
sprite_index: int = 0, scale: float = 1.0,
|
||||||
on_click: Optional[Callable] = None,
|
on_click: Optional[Callable] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None,
|
|
||||||
size: Optional[Tuple[float, float]] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
texture: Texture
|
texture: Texture
|
||||||
sprite_index: int
|
sprite_index: int
|
||||||
|
|
@ -598,12 +181,7 @@ class Grid(Drawable):
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
|
def __init__(self, x: float = 0, y: float = 0, grid_size: Tuple[int, int] = (20, 20),
|
||||||
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
|
texture: Optional[Texture] = None, tile_width: int = 16, tile_height: int = 16,
|
||||||
scale: float = 1.0, on_click: Optional[Callable] = None,
|
scale: float = 1.0, on_click: Optional[Callable] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None, pos: Optional[Tuple[float, float]] = None,
|
|
||||||
size: Optional[Tuple[float, float]] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
grid_size: Tuple[int, int]
|
grid_size: Tuple[int, int]
|
||||||
tile_width: int
|
tile_width: int
|
||||||
|
|
@ -631,11 +209,7 @@ class Line(Drawable):
|
||||||
def __init__(self, start: Optional[Tuple[float, float]] = None,
|
def __init__(self, start: Optional[Tuple[float, float]] = None,
|
||||||
end: Optional[Tuple[float, float]] = None,
|
end: Optional[Tuple[float, float]] = None,
|
||||||
thickness: float = 1.0, color: Optional[Color] = None,
|
thickness: float = 1.0, color: Optional[Color] = None,
|
||||||
on_click: Optional[Callable] = None,
|
on_click: Optional[Callable] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
start: Vector
|
start: Vector
|
||||||
end: Vector
|
end: Vector
|
||||||
|
|
@ -654,11 +228,7 @@ class Circle(Drawable):
|
||||||
@overload
|
@overload
|
||||||
def __init__(self, radius: float = 0, center: Optional[Tuple[float, float]] = None,
|
def __init__(self, radius: float = 0, center: Optional[Tuple[float, float]] = None,
|
||||||
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
fill_color: Optional[Color] = None, outline_color: Optional[Color] = None,
|
||||||
outline: float = 0, on_click: Optional[Callable] = None,
|
outline: float = 0, on_click: Optional[Callable] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
radius: float
|
radius: float
|
||||||
center: Vector
|
center: Vector
|
||||||
|
|
@ -679,11 +249,7 @@ class Arc(Drawable):
|
||||||
def __init__(self, center: Optional[Tuple[float, float]] = None, radius: float = 0,
|
def __init__(self, center: Optional[Tuple[float, float]] = None, radius: float = 0,
|
||||||
start_angle: float = 0, end_angle: float = 90,
|
start_angle: float = 0, end_angle: float = 90,
|
||||||
color: Optional[Color] = None, thickness: float = 1.0,
|
color: Optional[Color] = None, thickness: float = 1.0,
|
||||||
on_click: Optional[Callable] = None,
|
on_click: Optional[Callable] = None) -> None: ...
|
||||||
visible: bool = True, opacity: float = 1.0, z_index: int = 0,
|
|
||||||
name: Optional[str] = None,
|
|
||||||
align: Optional[Alignment] = None, margin: float = 0.0,
|
|
||||||
horiz_margin: float = 0.0, vert_margin: float = 0.0) -> None: ...
|
|
||||||
|
|
||||||
center: Vector
|
center: Vector
|
||||||
radius: float
|
radius: float
|
||||||
|
|
@ -855,134 +421,61 @@ class Window:
|
||||||
...
|
...
|
||||||
|
|
||||||
class Animation:
|
class Animation:
|
||||||
"""Animation for interpolating UI properties over time.
|
"""Animation object for animating UI properties."""
|
||||||
|
|
||||||
Create an animation targeting a specific property, then call start() on a
|
target: Any
|
||||||
UI element to begin the animation. The AnimationManager handles updates
|
property: str
|
||||||
automatically.
|
duration: float
|
||||||
|
easing: str
|
||||||
Example:
|
loop: bool
|
||||||
# Move a frame to x=500 over 2 seconds with easing
|
on_complete: Optional[Callable]
|
||||||
anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')
|
|
||||||
anim.start(my_frame)
|
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any,
|
||||||
|
duration: float, easing: str = 'linear', loop: bool = False,
|
||||||
# Animate color with completion callback
|
on_complete: Optional[Callable] = None) -> None: ...
|
||||||
def on_done(anim, target):
|
|
||||||
print('Fade complete!')
|
def start(self) -> None:
|
||||||
fade = mcrfpy.Animation('fill_color.a', 0, 1.0, callback=on_done)
|
"""Start the animation."""
|
||||||
fade.start(my_sprite)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def property(self) -> str:
|
|
||||||
"""Target property name being animated (read-only)."""
|
|
||||||
...
|
...
|
||||||
|
|
||||||
@property
|
|
||||||
def duration(self) -> float:
|
|
||||||
"""Animation duration in seconds (read-only)."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def elapsed(self) -> float:
|
|
||||||
"""Time elapsed since animation started in seconds (read-only)."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_complete(self) -> bool:
|
|
||||||
"""Whether the animation has finished (read-only)."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_delta(self) -> bool:
|
|
||||||
"""Whether animation uses delta/additive mode (read-only)."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
property: str,
|
|
||||||
target: Union[float, int, Tuple[float, float], Tuple[int, int, int], Tuple[int, int, int, int], List[int], str],
|
|
||||||
duration: float,
|
|
||||||
easing: str = 'linear',
|
|
||||||
delta: bool = False,
|
|
||||||
callback: Optional[Callable[['Animation', Any], None]] = None) -> None:
|
|
||||||
"""Create an animation for a UI property.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
property: Property name to animate. Common properties:
|
|
||||||
- Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size'
|
|
||||||
- Appearance: 'fill_color', 'outline_color', 'opacity'
|
|
||||||
- Sprite: 'sprite_index', 'scale'
|
|
||||||
- Grid: 'center', 'zoom'
|
|
||||||
- Sub-properties: 'fill_color.r', 'fill_color.g', etc.
|
|
||||||
target: Target value. Type depends on property:
|
|
||||||
- float: For x, y, w, h, scale, opacity, zoom
|
|
||||||
- int: For sprite_index
|
|
||||||
- (r, g, b) or (r, g, b, a): For colors
|
|
||||||
- (x, y): For pos, size, center
|
|
||||||
- [int, ...]: For sprite animation sequences
|
|
||||||
- str: For text animation
|
|
||||||
duration: Animation duration in seconds.
|
|
||||||
easing: Easing function. Options: 'linear', 'easeIn', 'easeOut',
|
|
||||||
'easeInOut', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
|
|
||||||
'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
|
|
||||||
'easeInElastic', 'easeOutElastic', 'easeInOutElastic',
|
|
||||||
'easeInBounce', 'easeOutBounce', 'easeInOutBounce', and more.
|
|
||||||
delta: If True, target value is added to start value.
|
|
||||||
callback: Function(animation, target) called on completion.
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def start(self, target: UIElement, conflict_mode: str = 'replace') -> None:
|
|
||||||
"""Start the animation on a UI element.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)
|
|
||||||
conflict_mode: How to handle if property is already animating:
|
|
||||||
- 'replace': Stop existing animation, start new one (default)
|
|
||||||
- 'queue': Wait for existing animation to complete
|
|
||||||
- 'error': Raise RuntimeError if property is busy
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def update(self, dt: float) -> bool:
|
def update(self, dt: float) -> bool:
|
||||||
"""Update animation by time delta. Returns True if still running.
|
"""Update animation, returns True if still running."""
|
||||||
|
|
||||||
Note: Normally called automatically by AnimationManager.
|
|
||||||
"""
|
|
||||||
...
|
...
|
||||||
|
|
||||||
def get_current_value(self) -> Any:
|
def get_current_value(self) -> Any:
|
||||||
"""Get the current interpolated value."""
|
"""Get the current interpolated value."""
|
||||||
...
|
...
|
||||||
|
|
||||||
def complete(self) -> None:
|
|
||||||
"""Complete the animation immediately, jumping to final value."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def hasValidTarget(self) -> bool:
|
|
||||||
"""Check if the animation target still exists."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
"""Return string representation showing property, duration, and status."""
|
|
||||||
...
|
|
||||||
|
|
||||||
# Module-level attributes
|
|
||||||
|
|
||||||
__version__: str
|
|
||||||
"""McRogueFace version string (e.g., '1.0.0')."""
|
|
||||||
|
|
||||||
keyboard: Keyboard
|
|
||||||
"""Keyboard state singleton for checking modifier keys."""
|
|
||||||
|
|
||||||
mouse: Mouse
|
|
||||||
"""Mouse state singleton for reading button/position state and controlling cursor."""
|
|
||||||
|
|
||||||
window: Window
|
|
||||||
"""Window singleton for controlling window properties."""
|
|
||||||
|
|
||||||
# Module functions
|
# Module functions
|
||||||
|
|
||||||
|
def createSoundBuffer(filename: str) -> int:
|
||||||
|
"""Load a sound effect from a file and return its buffer ID."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def loadMusic(filename: str) -> None:
|
||||||
|
"""Load and immediately play background music from a file."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def setMusicVolume(volume: int) -> None:
|
||||||
|
"""Set the global music volume (0-100)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def setSoundVolume(volume: int) -> None:
|
||||||
|
"""Set the global sound effects volume (0-100)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def playSound(buffer_id: int) -> None:
|
||||||
|
"""Play a sound effect using a previously loaded buffer."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def getMusicVolume() -> int:
|
||||||
|
"""Get the current music volume level (0-100)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def getSoundVolume() -> int:
|
||||||
|
"""Get the current sound effects volume level (0-100)."""
|
||||||
|
...
|
||||||
|
|
||||||
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
def sceneUI(scene: Optional[str] = None) -> UICollection:
|
||||||
"""Get all UI elements for a scene."""
|
"""Get all UI elements for a scene."""
|
||||||
...
|
...
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue