#include "PyAnimation.h" #include "McRFPy_API.h" #include "McRFPy_Doc.h" #include "UIDrawable.h" #include "UIFrame.h" #include "UICaption.h" #include "UISprite.h" #include "UIGrid.h" #include "UIEntity.h" #include "UI.h" // For the PyTypeObject definitions #include PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds) { PyAnimationObject* self = (PyAnimationObject*)type->tp_alloc(type, 0); if (self != NULL) { // Will be initialized in init } return (PyObject*)self; } int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr}; const char* property_name; PyObject* target_value; float duration; const char* easing_name = "linear"; int delta = 0; PyObject* callback = nullptr; if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast(keywords), &property_name, &target_value, &duration, &easing_name, &delta, &callback)) { return -1; } // Validate callback is callable if provided if (callback && callback != Py_None && !PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback must be callable"); return -1; } // Convert None to nullptr for C++ if (callback == Py_None) { callback = nullptr; } // Convert Python target value to AnimationValue AnimationValue animValue; if (PyFloat_Check(target_value)) { animValue = static_cast(PyFloat_AsDouble(target_value)); } else if (PyLong_Check(target_value)) { animValue = static_cast(PyLong_AsLong(target_value)); } else if (PyList_Check(target_value)) { // List of integers for sprite animation std::vector indices; Py_ssize_t size = PyList_Size(target_value); for (Py_ssize_t i = 0; i < size; i++) { PyObject* item = PyList_GetItem(target_value, i); if (PyLong_Check(item)) { indices.push_back(PyLong_AsLong(item)); } else { PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers"); return -1; } } animValue = indices; } else if (PyTuple_Check(target_value)) { Py_ssize_t size = PyTuple_Size(target_value); if (size == 2) { // Vector2f float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0)); float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1)); animValue = sf::Vector2f(x, y); } else if (size == 3 || size == 4) { // Color (RGB or RGBA) int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0)); int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1)); int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2)); int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255; animValue = sf::Color(r, g, b, a); } else { PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)"); return -1; } } else if (PyUnicode_Check(target_value)) { // String for text animation const char* str = PyUnicode_AsUTF8(target_value); animValue = std::string(str); } else { PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string"); return -1; } // Get easing function EasingFunction easingFunc = EasingFunctions::getByName(easing_name); // Create the Animation self->data = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0, callback); return 0; } void PyAnimation::dealloc(PyAnimationObject* self) { self->data.reset(); Py_TYPE(self)->tp_free((PyObject*)self); } PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) { return PyUnicode_FromString(self->data->getTargetProperty().c_str()); } PyObject* PyAnimation::get_duration(PyAnimationObject* self, void* closure) { return PyFloat_FromDouble(self->data->getDuration()); } PyObject* PyAnimation::get_elapsed(PyAnimationObject* self, void* closure) { return PyFloat_FromDouble(self->data->getElapsed()); } PyObject* PyAnimation::get_is_complete(PyAnimationObject* self, void* closure) { return PyBool_FromLong(self->data->isComplete()); } PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) { return PyBool_FromLong(self->data->isDelta()); } PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) { PyObject* target_obj; if (!PyArg_ParseTuple(args, "O", &target_obj)) { return NULL; } // Get type objects from the module to ensure they're initialized PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"); PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"); PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); PyObject* entity_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); bool handled = false; // Use PyObject_IsInstance to support inheritance if (frame_type && PyObject_IsInstance(target_obj, frame_type)) { PyUIFrameObject* frame = (PyUIFrameObject*)target_obj; if (frame->data) { self->data->start(frame->data); AnimationManager::getInstance().addAnimation(self->data); handled = true; } } else if (caption_type && PyObject_IsInstance(target_obj, caption_type)) { PyUICaptionObject* caption = (PyUICaptionObject*)target_obj; if (caption->data) { self->data->start(caption->data); AnimationManager::getInstance().addAnimation(self->data); handled = true; } } else if (sprite_type && PyObject_IsInstance(target_obj, sprite_type)) { PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj; if (sprite->data) { self->data->start(sprite->data); AnimationManager::getInstance().addAnimation(self->data); handled = true; } } else if (grid_type && PyObject_IsInstance(target_obj, grid_type)) { PyUIGridObject* grid = (PyUIGridObject*)target_obj; if (grid->data) { self->data->start(grid->data); AnimationManager::getInstance().addAnimation(self->data); handled = true; } } else if (entity_type && PyObject_IsInstance(target_obj, entity_type)) { // Special handling for Entity since it doesn't inherit from UIDrawable PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; if (entity->data) { self->data->startEntity(entity->data); AnimationManager::getInstance().addAnimation(self->data); handled = true; } } // Clean up references Py_XDECREF(frame_type); Py_XDECREF(caption_type); Py_XDECREF(sprite_type); Py_XDECREF(grid_type); Py_XDECREF(entity_type); if (!handled) { PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity (or a subclass of these)"); return NULL; } Py_RETURN_NONE; } PyObject* PyAnimation::update(PyAnimationObject* self, PyObject* args) { float deltaTime; if (!PyArg_ParseTuple(args, "f", &deltaTime)) { return NULL; } bool still_running = self->data->update(deltaTime); return PyBool_FromLong(still_running); } PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args) { AnimationValue value = self->data->getCurrentValue(); // Convert AnimationValue back to Python return std::visit([](const auto& val) -> PyObject* { using T = std::decay_t; if constexpr (std::is_same_v) { return PyFloat_FromDouble(val); } else if constexpr (std::is_same_v) { return PyLong_FromLong(val); } else if constexpr (std::is_same_v>) { // This shouldn't happen as we interpolate to int return PyLong_FromLong(0); } else if constexpr (std::is_same_v) { return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a); } else if constexpr (std::is_same_v) { return Py_BuildValue("(ff)", val.x, val.y); } else if constexpr (std::is_same_v) { return PyUnicode_FromString(val.c_str()); } Py_RETURN_NONE; }, value); } PyObject* PyAnimation::complete(PyAnimationObject* self, PyObject* args) { if (self->data) { self->data->complete(); } Py_RETURN_NONE; } PyObject* PyAnimation::has_valid_target(PyAnimationObject* self, PyObject* args) { if (self->data && self->data->hasValidTarget()) { Py_RETURN_TRUE; } Py_RETURN_FALSE; } PyGetSetDef PyAnimation::getsetters[] = { {"property", (getter)get_property, NULL, MCRF_PROPERTY(property, "Target property name (str, read-only). The property being animated (e.g., 'pos', 'opacity', 'sprite_index')."), NULL}, {"duration", (getter)get_duration, NULL, MCRF_PROPERTY(duration, "Animation duration in seconds (float, read-only). Total time for the animation to complete."), NULL}, {"elapsed", (getter)get_elapsed, NULL, MCRF_PROPERTY(elapsed, "Elapsed time in seconds (float, read-only). Time since the animation started."), NULL}, {"is_complete", (getter)get_is_complete, NULL, MCRF_PROPERTY(is_complete, "Whether animation is complete (bool, read-only). True when elapsed >= duration or complete() was called."), NULL}, {"is_delta", (getter)get_is_delta, NULL, MCRF_PROPERTY(is_delta, "Whether animation uses delta mode (bool, read-only). In delta mode, the target value is added to the starting value."), NULL}, {NULL} }; PyMethodDef PyAnimation::methods[] = { {"start", (PyCFunction)start, METH_VARARGS, MCRF_METHOD(Animation, start, MCRF_SIG("(target: UIDrawable)", "None"), MCRF_DESC("Start the animation on a target UI element."), MCRF_ARGS_START MCRF_ARG("target", "The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)") MCRF_RETURNS("None") MCRF_NOTE("The animation will automatically stop if the target is destroyed. Call AnimationManager.update(delta_time) each frame to progress animations.") )}, {"update", (PyCFunction)update, METH_VARARGS, MCRF_METHOD(Animation, update, MCRF_SIG("(delta_time: float)", "bool"), MCRF_DESC("Update the animation by the given time delta."), MCRF_ARGS_START MCRF_ARG("delta_time", "Time elapsed since last update in seconds") MCRF_RETURNS("bool: True if animation is still running, False if complete") MCRF_NOTE("Typically called by AnimationManager automatically. Manual calls only needed for custom animation control.") )}, {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, MCRF_METHOD(Animation, get_current_value, MCRF_SIG("()", "Any"), MCRF_DESC("Get the current interpolated value of the animation."), MCRF_RETURNS("Any: Current value (type depends on property: float, int, Color tuple, Vector tuple, or str)") MCRF_NOTE("Return type matches the target property type. For sprite_index returns int, for pos returns (x, y), for fill_color returns (r, g, b, a).") )}, {"complete", (PyCFunction)complete, METH_NOARGS, MCRF_METHOD(Animation, complete, MCRF_SIG("()", "None"), MCRF_DESC("Complete the animation immediately by jumping to the final value."), MCRF_RETURNS("None") MCRF_NOTE("Sets elapsed = duration and applies target value immediately. Completion callback will be called if set.") )}, {"hasValidTarget", (PyCFunction)has_valid_target, METH_NOARGS, MCRF_METHOD(Animation, hasValidTarget, MCRF_SIG("()", "bool"), MCRF_DESC("Check if the animation still has a valid target."), MCRF_RETURNS("bool: True if the target still exists, False if it was destroyed") MCRF_NOTE("Animations automatically clean up when targets are destroyed. Use this to check if manual cleanup is needed.") )}, {NULL} };