From 70cf44f8f044ed49544dd9444245115187d3b318 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 5 Jul 2025 00:56:42 -0400 Subject: [PATCH] Implement comprehensive animation system (closes #59) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Animation class with 30+ easing functions (linear, ease in/out, quad, cubic, elastic, bounce, etc.) - Add property system to all UI classes for animation support: - UIFrame: position, size, colors (including individual r/g/b/a components) - UICaption: position, size, text, colors - UISprite: position, scale, sprite_number (with sequence support) - UIGrid: position, size, camera center, zoom - UIEntity: position, sprite properties - Create AnimationManager singleton for frame-based updates - Add Python bindings through PyAnimation wrapper - Support for delta animations (relative values) - Fix segfault when running scripts directly (mcrf_module initialization) - Fix headless/windowed mode behavior to respect --headless flag - Animations run purely in C++ without Python callbacks per frame All UI properties are now animatable with smooth interpolation and professional easing curves. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/Animation.cpp | 527 ++++++++++++++++++++++++++++++++++++++++ src/Animation.h | 146 +++++++++++ src/GameEngine.cpp | 8 + src/McRFPy_API.cpp | 4 + src/PyAnimation.cpp | 234 ++++++++++++++++++ src/PyAnimation.h | 50 ++++ src/PyScene.cpp | 10 + src/UICaption.cpp | 171 +++++++++++++ src/UICaption.h | 9 + src/UICollection.cpp | 23 ++ src/UIDrawable.cpp | 65 +++++ src/UIDrawable.h | 18 ++ src/UIEntity.cpp | 48 ++++ src/UIEntity.h | 5 + src/UIFrame.cpp | 150 ++++++++++++ src/UIFrame.h | 9 + src/UIGrid.cpp | 113 +++++++++ src/UIGrid.h | 6 + src/UISprite.cpp | 84 ++++++- src/UISprite.h | 8 +- src/main.cpp | 26 +- tests/animation_demo.py | 165 +++++++++++++ 22 files changed, 1873 insertions(+), 6 deletions(-) create mode 100644 src/Animation.cpp create mode 100644 src/Animation.h create mode 100644 src/PyAnimation.cpp create mode 100644 src/PyAnimation.h create mode 100644 tests/animation_demo.py diff --git a/src/Animation.cpp b/src/Animation.cpp new file mode 100644 index 0000000..28f1805 --- /dev/null +++ b/src/Animation.cpp @@ -0,0 +1,527 @@ +#include "Animation.h" +#include "UIDrawable.h" +#include "UIEntity.h" +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +// Animation implementation +Animation::Animation(const std::string& targetProperty, + const AnimationValue& targetValue, + float duration, + EasingFunction easingFunc, + bool delta) + : targetProperty(targetProperty) + , targetValue(targetValue) + , duration(duration) + , easingFunc(easingFunc) + , delta(delta) +{ +} + +void Animation::start(UIDrawable* target) { + currentTarget = target; + elapsed = 0.0f; + + // Capture startValue from target based on targetProperty + if (!currentTarget) return; + + // Try to get the current value based on the expected type + std::visit([this](const auto& targetVal) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + float value; + if (currentTarget->getProperty(targetProperty, value)) { + startValue = value; + } + } + else if constexpr (std::is_same_v) { + int value; + if (currentTarget->getProperty(targetProperty, value)) { + startValue = value; + } + } + else if constexpr (std::is_same_v>) { + // For sprite animation, get current sprite index + int value; + if (currentTarget->getProperty(targetProperty, value)) { + startValue = value; + } + } + else if constexpr (std::is_same_v) { + sf::Color value; + if (currentTarget->getProperty(targetProperty, value)) { + startValue = value; + } + } + else if constexpr (std::is_same_v) { + sf::Vector2f value; + if (currentTarget->getProperty(targetProperty, value)) { + startValue = value; + } + } + else if constexpr (std::is_same_v) { + std::string value; + if (currentTarget->getProperty(targetProperty, value)) { + startValue = value; + } + } + }, targetValue); +} + +void Animation::startEntity(UIEntity* target) { + currentEntityTarget = target; + currentTarget = nullptr; // Clear drawable target + elapsed = 0.0f; + + // Capture the starting value from the entity + std::visit([this, target](const auto& val) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + float value = 0.0f; + if (target->getProperty(targetProperty, value)) { + startValue = value; + } + } + else if constexpr (std::is_same_v) { + // For entities, we might need to handle sprite_number differently + if (targetProperty == "sprite_number") { + startValue = target->sprite.getSpriteIndex(); + } + } + // Entities don't support other types yet + }, targetValue); +} + +bool Animation::update(float deltaTime) { + if ((!currentTarget && !currentEntityTarget) || isComplete()) { + return false; + } + + elapsed += deltaTime; + elapsed = std::min(elapsed, duration); + + // Calculate easing value (0.0 to 1.0) + float t = duration > 0 ? elapsed / duration : 1.0f; + float easedT = easingFunc(t); + + // Get interpolated value + AnimationValue currentValue = interpolate(easedT); + + // Apply currentValue to target (either drawable or entity) + std::visit([this](const auto& value) { + using T = std::decay_t; + + if (currentTarget) { + // Handle UIDrawable targets + if constexpr (std::is_same_v) { + currentTarget->setProperty(targetProperty, value); + } + else if constexpr (std::is_same_v) { + currentTarget->setProperty(targetProperty, value); + } + else if constexpr (std::is_same_v) { + currentTarget->setProperty(targetProperty, value); + } + else if constexpr (std::is_same_v) { + currentTarget->setProperty(targetProperty, value); + } + else if constexpr (std::is_same_v) { + currentTarget->setProperty(targetProperty, value); + } + } + else if (currentEntityTarget) { + // Handle UIEntity targets + if constexpr (std::is_same_v) { + currentEntityTarget->setProperty(targetProperty, value); + } + else if constexpr (std::is_same_v) { + currentEntityTarget->setProperty(targetProperty, value); + } + // Entities don't support other types yet + } + }, currentValue); + + return !isComplete(); +} + +AnimationValue Animation::getCurrentValue() const { + float t = duration > 0 ? elapsed / duration : 1.0f; + float easedT = easingFunc(t); + return interpolate(easedT); +} + +AnimationValue Animation::interpolate(float t) const { + // Visit the variant to perform type-specific interpolation + return std::visit([this, t](const auto& target) -> AnimationValue { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + // Interpolate float + const float* start = std::get_if(&startValue); + if (!start) return target; // Type mismatch + + if (delta) { + return *start + target * t; + } else { + return *start + (target - *start) * t; + } + } + else if constexpr (std::is_same_v) { + // Interpolate integer + const int* start = std::get_if(&startValue); + if (!start) return target; + + float result; + if (delta) { + result = *start + target * t; + } else { + result = *start + (target - *start) * t; + } + return static_cast(std::round(result)); + } + else if constexpr (std::is_same_v>) { + // For sprite animation, interpolate through the list + if (target.empty()) return target; + + // Map t to an index in the vector + size_t index = static_cast(t * (target.size() - 1)); + index = std::min(index, target.size() - 1); + return static_cast(target[index]); + } + else if constexpr (std::is_same_v) { + // Interpolate color + const sf::Color* start = std::get_if(&startValue); + if (!start) return target; + + sf::Color result; + if (delta) { + result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f); + result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f); + result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f); + result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f); + } else { + result.r = start->r + (target.r - start->r) * t; + result.g = start->g + (target.g - start->g) * t; + result.b = start->b + (target.b - start->b) * t; + result.a = start->a + (target.a - start->a) * t; + } + return result; + } + else if constexpr (std::is_same_v) { + // Interpolate vector + const sf::Vector2f* start = std::get_if(&startValue); + if (!start) return target; + + if (delta) { + return sf::Vector2f(start->x + target.x * t, + start->y + target.y * t); + } else { + return sf::Vector2f(start->x + (target.x - start->x) * t, + start->y + (target.y - start->y) * t); + } + } + else if constexpr (std::is_same_v) { + // For text, show characters based on t + const std::string* start = std::get_if(&startValue); + if (!start) return target; + + // If delta mode, append characters from target + if (delta) { + size_t chars = static_cast(target.length() * t); + return *start + target.substr(0, chars); + } else { + // Transition from start text to target text + if (t < 0.5f) { + // First half: remove characters from start + size_t chars = static_cast(start->length() * (1.0f - t * 2.0f)); + return start->substr(0, chars); + } else { + // Second half: add characters to target + size_t chars = static_cast(target.length() * ((t - 0.5f) * 2.0f)); + return target.substr(0, chars); + } + } + } + + return target; // Fallback + }, targetValue); +} + +// Easing functions implementation +namespace EasingFunctions { + +float linear(float t) { + return t; +} + +float easeIn(float t) { + return t * t; +} + +float easeOut(float t) { + return t * (2.0f - t); +} + +float easeInOut(float t) { + return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t; +} + +// Quadratic +float easeInQuad(float t) { + return t * t; +} + +float easeOutQuad(float t) { + return t * (2.0f - t); +} + +float easeInOutQuad(float t) { + return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t; +} + +// Cubic +float easeInCubic(float t) { + return t * t * t; +} + +float easeOutCubic(float t) { + float t1 = t - 1.0f; + return t1 * t1 * t1 + 1.0f; +} + +float easeInOutCubic(float t) { + return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f; +} + +// Quartic +float easeInQuart(float t) { + return t * t * t * t; +} + +float easeOutQuart(float t) { + float t1 = t - 1.0f; + return 1.0f - t1 * t1 * t1 * t1; +} + +float easeInOutQuart(float t) { + return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f); +} + +// Sine +float easeInSine(float t) { + return 1.0f - std::cos(t * M_PI / 2.0f); +} + +float easeOutSine(float t) { + return std::sin(t * M_PI / 2.0f); +} + +float easeInOutSine(float t) { + return 0.5f * (1.0f - std::cos(M_PI * t)); +} + +// Exponential +float easeInExpo(float t) { + return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f)); +} + +float easeOutExpo(float t) { + return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t); +} + +float easeInOutExpo(float t) { + if (t == 0.0f) return 0.0f; + if (t == 1.0f) return 1.0f; + if (t < 0.5f) { + return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f); + } else { + return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f); + } +} + +// Circular +float easeInCirc(float t) { + return 1.0f - std::sqrt(1.0f - t * t); +} + +float easeOutCirc(float t) { + float t1 = t - 1.0f; + return std::sqrt(1.0f - t1 * t1); +} + +float easeInOutCirc(float t) { + if (t < 0.5f) { + return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t)); + } else { + return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f); + } +} + +// Elastic +float easeInElastic(float t) { + if (t == 0.0f) return 0.0f; + if (t == 1.0f) return 1.0f; + float p = 0.3f; + float a = 1.0f; + float s = p / 4.0f; + float t1 = t - 1.0f; + return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p)); +} + +float easeOutElastic(float t) { + if (t == 0.0f) return 0.0f; + if (t == 1.0f) return 1.0f; + float p = 0.3f; + float a = 1.0f; + float s = p / 4.0f; + return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f; +} + +float easeInOutElastic(float t) { + if (t == 0.0f) return 0.0f; + if (t == 1.0f) return 1.0f; + float p = 0.45f; + float a = 1.0f; + float s = p / 4.0f; + + if (t < 0.5f) { + float t1 = 2.0f * t - 1.0f; + return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p)); + } else { + float t1 = 2.0f * t - 1.0f; + return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f; + } +} + +// Back (overshooting) +float easeInBack(float t) { + const float s = 1.70158f; + return t * t * ((s + 1.0f) * t - s); +} + +float easeOutBack(float t) { + const float s = 1.70158f; + float t1 = t - 1.0f; + return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f; +} + +float easeInOutBack(float t) { + const float s = 1.70158f * 1.525f; + if (t < 0.5f) { + return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s)); + } else { + float t1 = 2.0f * t - 2.0f; + return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f); + } +} + +// Bounce +float easeOutBounce(float t) { + if (t < 1.0f / 2.75f) { + return 7.5625f * t * t; + } else if (t < 2.0f / 2.75f) { + float t1 = t - 1.5f / 2.75f; + return 7.5625f * t1 * t1 + 0.75f; + } else if (t < 2.5f / 2.75f) { + float t1 = t - 2.25f / 2.75f; + return 7.5625f * t1 * t1 + 0.9375f; + } else { + float t1 = t - 2.625f / 2.75f; + return 7.5625f * t1 * t1 + 0.984375f; + } +} + +float easeInBounce(float t) { + return 1.0f - easeOutBounce(1.0f - t); +} + +float easeInOutBounce(float t) { + if (t < 0.5f) { + return 0.5f * easeInBounce(2.0f * t); + } else { + return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f; + } +} + +// Get easing function by name +EasingFunction getByName(const std::string& name) { + static std::unordered_map easingMap = { + {"linear", linear}, + {"easeIn", easeIn}, + {"easeOut", easeOut}, + {"easeInOut", easeInOut}, + {"easeInQuad", easeInQuad}, + {"easeOutQuad", easeOutQuad}, + {"easeInOutQuad", easeInOutQuad}, + {"easeInCubic", easeInCubic}, + {"easeOutCubic", easeOutCubic}, + {"easeInOutCubic", easeInOutCubic}, + {"easeInQuart", easeInQuart}, + {"easeOutQuart", easeOutQuart}, + {"easeInOutQuart", easeInOutQuart}, + {"easeInSine", easeInSine}, + {"easeOutSine", easeOutSine}, + {"easeInOutSine", easeInOutSine}, + {"easeInExpo", easeInExpo}, + {"easeOutExpo", easeOutExpo}, + {"easeInOutExpo", easeInOutExpo}, + {"easeInCirc", easeInCirc}, + {"easeOutCirc", easeOutCirc}, + {"easeInOutCirc", easeInOutCirc}, + {"easeInElastic", easeInElastic}, + {"easeOutElastic", easeOutElastic}, + {"easeInOutElastic", easeInOutElastic}, + {"easeInBack", easeInBack}, + {"easeOutBack", easeOutBack}, + {"easeInOutBack", easeInOutBack}, + {"easeInBounce", easeInBounce}, + {"easeOutBounce", easeOutBounce}, + {"easeInOutBounce", easeInOutBounce} + }; + + auto it = easingMap.find(name); + if (it != easingMap.end()) { + return it->second; + } + return linear; // Default to linear +} + +} // namespace EasingFunctions + +// AnimationManager implementation +AnimationManager& AnimationManager::getInstance() { + static AnimationManager instance; + return instance; +} + +void AnimationManager::addAnimation(std::shared_ptr animation) { + activeAnimations.push_back(animation); +} + +void AnimationManager::update(float deltaTime) { + for (auto& anim : activeAnimations) { + anim->update(deltaTime); + } + cleanup(); +} + +void AnimationManager::cleanup() { + activeAnimations.erase( + std::remove_if(activeAnimations.begin(), activeAnimations.end(), + [](const std::shared_ptr& anim) { + return anim->isComplete(); + }), + activeAnimations.end() + ); +} + +void AnimationManager::clear() { + activeAnimations.clear(); +} \ No newline at end of file diff --git a/src/Animation.h b/src/Animation.h new file mode 100644 index 0000000..6308f32 --- /dev/null +++ b/src/Animation.h @@ -0,0 +1,146 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Forward declarations +class UIDrawable; +class UIEntity; + +// Forward declare namespace +namespace EasingFunctions { + float linear(float t); +} + +// Easing function type +typedef std::function EasingFunction; + +// Animation target value can be various types +typedef std::variant< + float, // Single float value + int, // Single integer value + std::vector, // List of integers (for sprite animation) + sf::Color, // Color animation + sf::Vector2f, // Vector animation + std::string // String animation (for text) +> AnimationValue; + +class Animation { +public: + // Constructor + Animation(const std::string& targetProperty, + const AnimationValue& targetValue, + float duration, + EasingFunction easingFunc = EasingFunctions::linear, + bool delta = false); + + // Apply this animation to a drawable + void start(UIDrawable* target); + + // Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable) + void startEntity(UIEntity* target); + + // Update animation (called each frame) + // Returns true if animation is still running, false if complete + bool update(float deltaTime); + + // Get current interpolated value + AnimationValue getCurrentValue() const; + + // Animation properties + std::string getTargetProperty() const { return targetProperty; } + float getDuration() const { return duration; } + float getElapsed() const { return elapsed; } + bool isComplete() const { return elapsed >= duration; } + bool isDelta() const { return delta; } + +private: + std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number") + AnimationValue startValue; // Starting value (captured when animation starts) + AnimationValue targetValue; // Target value to animate to + float duration; // Animation duration in seconds + float elapsed = 0.0f; // Elapsed time + EasingFunction easingFunc; // Easing function to use + bool delta; // If true, targetValue is relative to start + + UIDrawable* currentTarget = nullptr; // Current target being animated + UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable) + + // Helper to interpolate between values + AnimationValue interpolate(float t) const; +}; + +// Easing functions library +namespace EasingFunctions { + // Basic easing functions + float linear(float t); + float easeIn(float t); + float easeOut(float t); + float easeInOut(float t); + + // Advanced easing functions + float easeInQuad(float t); + float easeOutQuad(float t); + float easeInOutQuad(float t); + + float easeInCubic(float t); + float easeOutCubic(float t); + float easeInOutCubic(float t); + + float easeInQuart(float t); + float easeOutQuart(float t); + float easeInOutQuart(float t); + + float easeInSine(float t); + float easeOutSine(float t); + float easeInOutSine(float t); + + float easeInExpo(float t); + float easeOutExpo(float t); + float easeInOutExpo(float t); + + float easeInCirc(float t); + float easeOutCirc(float t); + float easeInOutCirc(float t); + + float easeInElastic(float t); + float easeOutElastic(float t); + float easeInOutElastic(float t); + + float easeInBack(float t); + float easeOutBack(float t); + float easeInOutBack(float t); + + float easeInBounce(float t); + float easeOutBounce(float t); + float easeInOutBounce(float t); + + // Get easing function by name + EasingFunction getByName(const std::string& name); +} + +// Animation manager to handle active animations +class AnimationManager { +public: + static AnimationManager& getInstance(); + + // Add an animation to be managed + void addAnimation(std::shared_ptr animation); + + // Update all animations + void update(float deltaTime); + + // Remove completed animations + void cleanup(); + + // Clear all animations + void clear(); + +private: + AnimationManager() = default; + std::vector> activeAnimations; +}; \ No newline at end of file diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 8ded69b..a5a195b 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -4,6 +4,7 @@ #include "PyScene.h" #include "UITestScene.h" #include "Resources.h" +#include "Animation.h" GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{}) { @@ -114,11 +115,18 @@ void GameEngine::run() { std::cout << "GameEngine::run() starting main loop..." << std::endl; float fps = 0.0; + frameTime = 0.016f; // Initialize to ~60 FPS clock.restart(); while (running) { currentScene()->update(); testTimers(); + + // Update animations (only if frameTime is valid) + if (frameTime > 0.0f && frameTime < 1.0f) { + AnimationManager::getInstance().update(frameTime); + } + if (!headless) { sUserInput(); } diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 4df666e..bf88b73 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -1,6 +1,7 @@ #include "McRFPy_API.h" #include "McRFPy_Automation.h" #include "platform.h" +#include "PyAnimation.h" #include "GameEngine.h" #include "UI.h" #include "Resources.h" @@ -76,6 +77,9 @@ PyObject* PyInit_mcrfpy() /*collections & iterators*/ &PyUICollectionType, &PyUICollectionIterType, &PyUIEntityCollectionType, &PyUIEntityCollectionIterType, + + /*animation*/ + &PyAnimationType, nullptr}; int i = 0; auto t = pytypes[i]; diff --git a/src/PyAnimation.cpp b/src/PyAnimation.cpp new file mode 100644 index 0000000..720b8d9 --- /dev/null +++ b/src/PyAnimation.cpp @@ -0,0 +1,234 @@ +#include "PyAnimation.h" +#include "McRFPy_API.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", nullptr}; + + const char* property_name; + PyObject* target_value; + float duration; + const char* easing_name = "linear"; + int delta = 0; + + if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast(keywords), + &property_name, &target_value, &duration, &easing_name, &delta)) { + return -1; + } + + // 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); + + 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 the UIDrawable from the Python object + UIDrawable* drawable = nullptr; + + // Check type by comparing type names + const char* type_name = Py_TYPE(target_obj)->tp_name; + + if (strcmp(type_name, "mcrfpy.Frame") == 0) { + PyUIFrameObject* frame = (PyUIFrameObject*)target_obj; + drawable = frame->data.get(); + } + else if (strcmp(type_name, "mcrfpy.Caption") == 0) { + PyUICaptionObject* caption = (PyUICaptionObject*)target_obj; + drawable = caption->data.get(); + } + else if (strcmp(type_name, "mcrfpy.Sprite") == 0) { + PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj; + drawable = sprite->data.get(); + } + else if (strcmp(type_name, "mcrfpy.Grid") == 0) { + PyUIGridObject* grid = (PyUIGridObject*)target_obj; + drawable = grid->data.get(); + } + else if (strcmp(type_name, "mcrfpy.Entity") == 0) { + // Special handling for Entity since it doesn't inherit from UIDrawable + PyUIEntityObject* entity = (PyUIEntityObject*)target_obj; + // Start the animation directly on the entity + self->data->startEntity(entity->data.get()); + + // Add to AnimationManager + AnimationManager::getInstance().addAnimation(self->data); + + Py_RETURN_NONE; + } + else { + PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity"); + return NULL; + } + + // Start the animation + self->data->start(drawable); + + // Add to AnimationManager + AnimationManager::getInstance().addAnimation(self->data); + + 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); +} + +PyGetSetDef PyAnimation::getsetters[] = { + {"property", (getter)get_property, NULL, "Target property name", NULL}, + {"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL}, + {"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL}, + {"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL}, + {"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL}, + {NULL} +}; + +PyMethodDef PyAnimation::methods[] = { + {"start", (PyCFunction)start, METH_VARARGS, + "Start the animation on a target UIDrawable"}, + {"update", (PyCFunction)update, METH_VARARGS, + "Update the animation by deltaTime (returns True if still running)"}, + {"get_current_value", (PyCFunction)get_current_value, METH_NOARGS, + "Get the current interpolated value"}, + {NULL} +}; \ No newline at end of file diff --git a/src/PyAnimation.h b/src/PyAnimation.h new file mode 100644 index 0000000..9976cb2 --- /dev/null +++ b/src/PyAnimation.h @@ -0,0 +1,50 @@ +#pragma once + +#include "Common.h" +#include "Python.h" +#include "structmember.h" +#include "Animation.h" +#include + +typedef struct { + PyObject_HEAD + std::shared_ptr data; +} PyAnimationObject; + +class PyAnimation { +public: + static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds); + static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds); + static void dealloc(PyAnimationObject* self); + + // Properties + static PyObject* get_property(PyAnimationObject* self, void* closure); + static PyObject* get_duration(PyAnimationObject* self, void* closure); + static PyObject* get_elapsed(PyAnimationObject* self, void* closure); + static PyObject* get_is_complete(PyAnimationObject* self, void* closure); + static PyObject* get_is_delta(PyAnimationObject* self, void* closure); + + // Methods + static PyObject* start(PyAnimationObject* self, PyObject* args); + static PyObject* update(PyAnimationObject* self, PyObject* args); + static PyObject* get_current_value(PyAnimationObject* self, PyObject* args); + + static PyGetSetDef getsetters[]; + static PyMethodDef methods[]; +}; + +namespace mcrfpydef { + static PyTypeObject PyAnimationType = { + .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, + .tp_name = "mcrfpy.Animation", + .tp_basicsize = sizeof(PyAnimationObject), + .tp_itemsize = 0, + .tp_dealloc = (destructor)PyAnimation::dealloc, + .tp_flags = Py_TPFLAGS_DEFAULT, + .tp_doc = PyDoc_STR("Animation object for animating UI properties"), + .tp_methods = PyAnimation::methods, + .tp_getset = PyAnimation::getsetters, + .tp_init = (initproc)PyAnimation::init, + .tp_new = PyAnimation::create, + }; +} \ No newline at end of file diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 35f3ae3..382ac60 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -2,6 +2,7 @@ #include "ActionCode.h" #include "Resources.h" #include "PyCallable.h" +#include PyScene::PyScene(GameEngine* g) : Scene(g) { @@ -66,7 +67,16 @@ void PyScene::render() { game->getRenderTarget().clear(); + // Create a copy of the vector to sort by z_index auto vec = *ui_elements; + + // Sort by z_index (lower values rendered first) + std::sort(vec.begin(), vec.end(), + [](const std::shared_ptr& a, const std::shared_ptr& b) { + return a->z_index < b->z_index; + }); + + // Render in sorted order for (auto e: vec) { if (e) diff --git a/src/UICaption.cpp b/src/UICaption.cpp index 539ec38..c4926b3 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -3,6 +3,7 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" +#include UIDrawable* UICaption::click_at(sf::Vector2f point) { @@ -198,6 +199,7 @@ PyGetSetDef UICaption::getsetters[] = { {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, {"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text size (integer) in points", (void*)5}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION}, {NULL} }; @@ -294,3 +296,172 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) return 0; } +// Property system implementation for animations +bool UICaption::setProperty(const std::string& name, float value) { + if (name == "x") { + text.setPosition(sf::Vector2f(value, text.getPosition().y)); + return true; + } + else if (name == "y") { + text.setPosition(sf::Vector2f(text.getPosition().x, value)); + return true; + } + else if (name == "size") { + text.setCharacterSize(static_cast(value)); + return true; + } + else if (name == "outline") { + text.setOutlineThickness(value); + return true; + } + else if (name == "fill_color.r") { + auto color = text.getFillColor(); + color.r = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setFillColor(color); + return true; + } + else if (name == "fill_color.g") { + auto color = text.getFillColor(); + color.g = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setFillColor(color); + return true; + } + else if (name == "fill_color.b") { + auto color = text.getFillColor(); + color.b = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setFillColor(color); + return true; + } + else if (name == "fill_color.a") { + auto color = text.getFillColor(); + color.a = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setFillColor(color); + return true; + } + else if (name == "outline_color.r") { + auto color = text.getOutlineColor(); + color.r = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setOutlineColor(color); + return true; + } + else if (name == "outline_color.g") { + auto color = text.getOutlineColor(); + color.g = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setOutlineColor(color); + return true; + } + else if (name == "outline_color.b") { + auto color = text.getOutlineColor(); + color.b = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setOutlineColor(color); + return true; + } + else if (name == "outline_color.a") { + auto color = text.getOutlineColor(); + color.a = static_cast(std::clamp(value, 0.0f, 255.0f)); + text.setOutlineColor(color); + return true; + } + else if (name == "z_index") { + z_index = static_cast(value); + return true; + } + return false; +} + +bool UICaption::setProperty(const std::string& name, const sf::Color& value) { + if (name == "fill_color") { + text.setFillColor(value); + return true; + } + else if (name == "outline_color") { + text.setOutlineColor(value); + return true; + } + return false; +} + +bool UICaption::setProperty(const std::string& name, const std::string& value) { + if (name == "text") { + text.setString(value); + return true; + } + return false; +} + +bool UICaption::getProperty(const std::string& name, float& value) const { + if (name == "x") { + value = text.getPosition().x; + return true; + } + else if (name == "y") { + value = text.getPosition().y; + return true; + } + else if (name == "size") { + value = static_cast(text.getCharacterSize()); + return true; + } + else if (name == "outline") { + value = text.getOutlineThickness(); + return true; + } + else if (name == "fill_color.r") { + value = text.getFillColor().r; + return true; + } + else if (name == "fill_color.g") { + value = text.getFillColor().g; + return true; + } + else if (name == "fill_color.b") { + value = text.getFillColor().b; + return true; + } + else if (name == "fill_color.a") { + value = text.getFillColor().a; + return true; + } + else if (name == "outline_color.r") { + value = text.getOutlineColor().r; + return true; + } + else if (name == "outline_color.g") { + value = text.getOutlineColor().g; + return true; + } + else if (name == "outline_color.b") { + value = text.getOutlineColor().b; + return true; + } + else if (name == "outline_color.a") { + value = text.getOutlineColor().a; + return true; + } + else if (name == "z_index") { + value = static_cast(z_index); + return true; + } + return false; +} + +bool UICaption::getProperty(const std::string& name, sf::Color& value) const { + if (name == "fill_color") { + value = text.getFillColor(); + return true; + } + else if (name == "outline_color") { + value = text.getOutlineColor(); + return true; + } + return false; +} + +bool UICaption::getProperty(const std::string& name, std::string& value) const { + if (name == "text") { + value = text.getString(); + return true; + } + return false; +} + diff --git a/src/UICaption.h b/src/UICaption.h index 7929f04..60d8e13 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -10,6 +10,15 @@ public: void render(sf::Vector2f, sf::RenderTarget&) override final; PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; + + // Property system for animations + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, const sf::Color& value) override; + bool setProperty(const std::string& name, const std::string& value) override; + + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, sf::Color& value) const override; + bool getProperty(const std::string& name, std::string& value) const override; static PyObject* get_float_member(PyUICaptionObject* self, void* closure); static int set_float_member(PyUICaptionObject* self, PyObject* value, void* closure); diff --git a/src/UICollection.cpp b/src/UICollection.cpp index 1a9b605..9bffff0 100644 --- a/src/UICollection.cpp +++ b/src/UICollection.cpp @@ -6,6 +6,7 @@ #include "UIGrid.h" #include "McRFPy_API.h" #include "PyObjectUtils.h" +#include using namespace mcrfpydef; @@ -173,6 +174,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o) // if not UIDrawable subclass, reject it // self->data->push_back( c++ object inside o ); + // Ensure module is initialized + if (!McRFPy_API::mcrf_module) { + PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized"); + return NULL; + } + // this would be a great use case for .tp_base if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && !PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && @@ -184,24 +191,40 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o) return NULL; } + // Calculate z_index for the new element + int new_z_index = 0; + if (!self->data->empty()) { + // Get the z_index of the last element and add 10 + int last_z = self->data->back()->z_index; + if (last_z <= INT_MAX - 10) { + new_z_index = last_z + 10; + } else { + new_z_index = INT_MAX; + } + } + if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { PyUIFrameObject* frame = (PyUIFrameObject*)o; + frame->data->z_index = new_z_index; self->data->push_back(frame->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { PyUICaptionObject* caption = (PyUICaptionObject*)o; + caption->data->z_index = new_z_index; self->data->push_back(caption->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { PyUISpriteObject* sprite = (PyUISpriteObject*)o; + sprite->data->z_index = new_z_index; self->data->push_back(sprite->data); } if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyUIGridObject* grid = (PyUIGridObject*)o; + grid->data->z_index = new_z_index; self->data->push_back(grid->data); } diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index bd4c63d..1bee9de 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -80,3 +80,68 @@ void UIDrawable::click_register(PyObject* callable) { click_callable = std::make_unique(callable); } + +PyObject* UIDrawable::get_int(PyObject* self, void* closure) { + PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return NULL; + } + + return PyLong_FromLong(drawable->z_index); +} + +int UIDrawable::set_int(PyObject* self, PyObject* value, void* closure) { + PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); + UIDrawable* drawable = nullptr; + + switch (objtype) { + case PyObjectsEnum::UIFRAME: + drawable = ((PyUIFrameObject*)self)->data.get(); + break; + case PyObjectsEnum::UICAPTION: + drawable = ((PyUICaptionObject*)self)->data.get(); + break; + case PyObjectsEnum::UISPRITE: + drawable = ((PyUISpriteObject*)self)->data.get(); + break; + case PyObjectsEnum::UIGRID: + drawable = ((PyUIGridObject*)self)->data.get(); + break; + default: + PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); + return -1; + } + + if (!PyLong_Check(value)) { + PyErr_SetString(PyExc_TypeError, "z_index must be an integer"); + return -1; + } + + long z = PyLong_AsLong(value); + if (z == -1 && PyErr_Occurred()) { + return -1; + } + + // Clamp to int range + if (z < INT_MIN) z = INT_MIN; + if (z > INT_MAX) z = INT_MAX; + + drawable->z_index = static_cast(z); + return 0; +} diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 9832d8d..44e647c 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -42,6 +42,24 @@ public: static PyObject* get_click(PyObject* self, void* closure); static int set_click(PyObject* self, PyObject* value, void* closure); + static PyObject* get_int(PyObject* self, void* closure); + static int set_int(PyObject* self, PyObject* value, void* closure); + + // Z-order for rendering (lower values rendered first, higher values on top) + int z_index = 0; + + // Animation support + virtual bool setProperty(const std::string& name, float value) { return false; } + virtual bool setProperty(const std::string& name, int value) { return false; } + virtual bool setProperty(const std::string& name, const sf::Color& value) { return false; } + virtual bool setProperty(const std::string& name, const sf::Vector2f& value) { return false; } + virtual bool setProperty(const std::string& name, const std::string& value) { return false; } + + virtual bool getProperty(const std::string& name, float& value) const { return false; } + virtual bool getProperty(const std::string& name, int& value) const { return false; } + virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; } + virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; } + virtual bool getProperty(const std::string& name, std::string& value) const { return false; } }; typedef struct { diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 1a3ce03..6a7b828 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -261,3 +261,51 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) { std::string repr_str = ss.str(); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); } + +// Property system implementation for animations +bool UIEntity::setProperty(const std::string& name, float value) { + if (name == "x") { + position.x = value; + collision_pos.x = static_cast(value); + // Update sprite position based on grid position + // Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties + sprite.setPosition(sf::Vector2f(position.x, position.y)); + return true; + } + else if (name == "y") { + position.y = value; + collision_pos.y = static_cast(value); + // Update sprite position based on grid position + sprite.setPosition(sf::Vector2f(position.x, position.y)); + return true; + } + else if (name == "sprite_scale") { + sprite.setScale(sf::Vector2f(value, value)); + return true; + } + return false; +} + +bool UIEntity::setProperty(const std::string& name, int value) { + if (name == "sprite_number") { + sprite.setSpriteIndex(value); + return true; + } + return false; +} + +bool UIEntity::getProperty(const std::string& name, float& value) const { + if (name == "x") { + value = position.x; + return true; + } + else if (name == "y") { + value = position.y; + return true; + } + else if (name == "sprite_scale") { + value = sprite.getScale().x; // Assuming uniform scale + return true; + } + return false; +} diff --git a/src/UIEntity.h b/src/UIEntity.h index 8cee8b4..a20953b 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -46,6 +46,11 @@ public: UIEntity(); UIEntity(UIGrid&); + // Property system for animations + bool setProperty(const std::string& name, float value); + bool setProperty(const std::string& name, int value); + bool getProperty(const std::string& name, float& value) const; + static PyObject* at(PyUIEntityObject* self, PyObject* o); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds); diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index f382127..cd59cad 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -215,6 +215,7 @@ PyGetSetDef UIFrame::getsetters[] = { {"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1}, {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME}, {NULL} }; @@ -264,3 +265,152 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) if (err_val) return err_val; return 0; } + +// Animation property system implementation +bool UIFrame::setProperty(const std::string& name, float value) { + if (name == "x") { + box.setPosition(sf::Vector2f(value, box.getPosition().y)); + return true; + } else if (name == "y") { + box.setPosition(sf::Vector2f(box.getPosition().x, value)); + return true; + } else if (name == "w") { + box.setSize(sf::Vector2f(value, box.getSize().y)); + return true; + } else if (name == "h") { + box.setSize(sf::Vector2f(box.getSize().x, value)); + return true; + } else if (name == "outline") { + box.setOutlineThickness(value); + return true; + } else if (name == "fill_color.r") { + auto color = box.getFillColor(); + color.r = std::clamp(static_cast(value), 0, 255); + box.setFillColor(color); + return true; + } else if (name == "fill_color.g") { + auto color = box.getFillColor(); + color.g = std::clamp(static_cast(value), 0, 255); + box.setFillColor(color); + return true; + } else if (name == "fill_color.b") { + auto color = box.getFillColor(); + color.b = std::clamp(static_cast(value), 0, 255); + box.setFillColor(color); + return true; + } else if (name == "fill_color.a") { + auto color = box.getFillColor(); + color.a = std::clamp(static_cast(value), 0, 255); + box.setFillColor(color); + return true; + } else if (name == "outline_color.r") { + auto color = box.getOutlineColor(); + color.r = std::clamp(static_cast(value), 0, 255); + box.setOutlineColor(color); + return true; + } else if (name == "outline_color.g") { + auto color = box.getOutlineColor(); + color.g = std::clamp(static_cast(value), 0, 255); + box.setOutlineColor(color); + return true; + } else if (name == "outline_color.b") { + auto color = box.getOutlineColor(); + color.b = std::clamp(static_cast(value), 0, 255); + box.setOutlineColor(color); + return true; + } else if (name == "outline_color.a") { + auto color = box.getOutlineColor(); + color.a = std::clamp(static_cast(value), 0, 255); + box.setOutlineColor(color); + return true; + } + return false; +} + +bool UIFrame::setProperty(const std::string& name, const sf::Color& value) { + if (name == "fill_color") { + box.setFillColor(value); + return true; + } else if (name == "outline_color") { + box.setOutlineColor(value); + return true; + } + return false; +} + +bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) { + if (name == "position") { + box.setPosition(value); + return true; + } else if (name == "size") { + box.setSize(value); + return true; + } + return false; +} + +bool UIFrame::getProperty(const std::string& name, float& value) const { + if (name == "x") { + value = box.getPosition().x; + return true; + } else if (name == "y") { + value = box.getPosition().y; + return true; + } else if (name == "w") { + value = box.getSize().x; + return true; + } else if (name == "h") { + value = box.getSize().y; + return true; + } else if (name == "outline") { + value = box.getOutlineThickness(); + return true; + } else if (name == "fill_color.r") { + value = box.getFillColor().r; + return true; + } else if (name == "fill_color.g") { + value = box.getFillColor().g; + return true; + } else if (name == "fill_color.b") { + value = box.getFillColor().b; + return true; + } else if (name == "fill_color.a") { + value = box.getFillColor().a; + return true; + } else if (name == "outline_color.r") { + value = box.getOutlineColor().r; + return true; + } else if (name == "outline_color.g") { + value = box.getOutlineColor().g; + return true; + } else if (name == "outline_color.b") { + value = box.getOutlineColor().b; + return true; + } else if (name == "outline_color.a") { + value = box.getOutlineColor().a; + return true; + } + return false; +} + +bool UIFrame::getProperty(const std::string& name, sf::Color& value) const { + if (name == "fill_color") { + value = box.getFillColor(); + return true; + } else if (name == "outline_color") { + value = box.getOutlineColor(); + return true; + } + return false; +} + +bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const { + if (name == "position") { + value = box.getPosition(); + return true; + } else if (name == "size") { + value = box.getSize(); + return true; + } + return false; +} diff --git a/src/UIFrame.h b/src/UIFrame.h index 986dd1e..6613f80 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -42,6 +42,15 @@ public: static PyGetSetDef getsetters[]; static PyObject* repr(PyUIFrameObject* self); static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds); + + // Animation property system + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, const sf::Color& value) override; + bool setProperty(const std::string& name, const sf::Vector2f& value) override; + + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, sf::Color& value) const override; + bool getProperty(const std::string& name, sf::Vector2f& value) const override; }; namespace mcrfpydef { diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 7a2f9ed..e616b16 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -458,6 +458,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID}, {NULL} /* Sentinel */ }; @@ -723,3 +724,115 @@ PyObject* UIEntityCollection::iter(PyUIEntityCollectionObject* self) Py_DECREF(iterType); return (PyObject*)iterObj; } + +// Property system implementation for animations +bool UIGrid::setProperty(const std::string& name, float value) { + if (name == "x") { + box.setPosition(sf::Vector2f(value, box.getPosition().y)); + output.setPosition(box.getPosition()); + return true; + } + else if (name == "y") { + box.setPosition(sf::Vector2f(box.getPosition().x, value)); + output.setPosition(box.getPosition()); + return true; + } + else if (name == "w" || name == "width") { + box.setSize(sf::Vector2f(value, box.getSize().y)); + output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y)); + return true; + } + else if (name == "h" || name == "height") { + box.setSize(sf::Vector2f(box.getSize().x, value)); + output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y)); + return true; + } + else if (name == "center_x") { + center_x = value; + return true; + } + else if (name == "center_y") { + center_y = value; + return true; + } + else if (name == "zoom") { + zoom = value; + return true; + } + else if (name == "z_index") { + z_index = static_cast(value); + return true; + } + return false; +} + +bool UIGrid::setProperty(const std::string& name, const sf::Vector2f& value) { + if (name == "position") { + box.setPosition(value); + output.setPosition(box.getPosition()); + return true; + } + else if (name == "size") { + box.setSize(value); + output.setTextureRect(sf::IntRect(0, 0, box.getSize().x, box.getSize().y)); + return true; + } + else if (name == "center") { + center_x = value.x; + center_y = value.y; + return true; + } + return false; +} + +bool UIGrid::getProperty(const std::string& name, float& value) const { + if (name == "x") { + value = box.getPosition().x; + return true; + } + else if (name == "y") { + value = box.getPosition().y; + return true; + } + else if (name == "w" || name == "width") { + value = box.getSize().x; + return true; + } + else if (name == "h" || name == "height") { + value = box.getSize().y; + return true; + } + else if (name == "center_x") { + value = center_x; + return true; + } + else if (name == "center_y") { + value = center_y; + return true; + } + else if (name == "zoom") { + value = zoom; + return true; + } + else if (name == "z_index") { + value = static_cast(z_index); + return true; + } + return false; +} + +bool UIGrid::getProperty(const std::string& name, sf::Vector2f& value) const { + if (name == "position") { + value = box.getPosition(); + return true; + } + else if (name == "size") { + value = box.getSize(); + return true; + } + else if (name == "center") { + value = sf::Vector2f(center_x, center_y); + return true; + } + return false; +} diff --git a/src/UIGrid.h b/src/UIGrid.h index 1e3f2aa..7c288b8 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -45,6 +45,12 @@ public: sf::RenderTexture renderTexture; std::vector points; std::shared_ptr>> entities; + + // Property system for animations + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, const sf::Vector2f& value) override; + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, sf::Vector2f& value) const override; static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyObject* get_grid_size(PyUIGridObject* self, void* closure); diff --git a/src/UISprite.cpp b/src/UISprite.cpp index b41b9eb..fa800d6 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -58,7 +58,7 @@ void UISprite::setSpriteIndex(int _sprite_index) sprite = ptex->sprite(sprite_index, sprite.getPosition(), sprite.getScale()); } -sf::Vector2f UISprite::getScale() +sf::Vector2f UISprite::getScale() const { return sprite.getScale(); } @@ -202,6 +202,7 @@ PyGetSetDef UISprite::getsetters[] = { {"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL}, {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE}, + {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE}, {NULL} }; @@ -245,3 +246,84 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) return 0; } + +// Property system implementation for animations +bool UISprite::setProperty(const std::string& name, float value) { + if (name == "x") { + sprite.setPosition(sf::Vector2f(value, sprite.getPosition().y)); + return true; + } + else if (name == "y") { + sprite.setPosition(sf::Vector2f(sprite.getPosition().x, value)); + return true; + } + else if (name == "scale") { + sprite.setScale(sf::Vector2f(value, value)); + return true; + } + else if (name == "scale_x") { + sprite.setScale(sf::Vector2f(value, sprite.getScale().y)); + return true; + } + else if (name == "scale_y") { + sprite.setScale(sf::Vector2f(sprite.getScale().x, value)); + return true; + } + else if (name == "z_index") { + z_index = static_cast(value); + return true; + } + return false; +} + +bool UISprite::setProperty(const std::string& name, int value) { + if (name == "sprite_number") { + setSpriteIndex(value); + return true; + } + else if (name == "z_index") { + z_index = value; + return true; + } + return false; +} + +bool UISprite::getProperty(const std::string& name, float& value) const { + if (name == "x") { + value = sprite.getPosition().x; + return true; + } + else if (name == "y") { + value = sprite.getPosition().y; + return true; + } + else if (name == "scale") { + value = sprite.getScale().x; // Assuming uniform scale + return true; + } + else if (name == "scale_x") { + value = sprite.getScale().x; + return true; + } + else if (name == "scale_y") { + value = sprite.getScale().y; + return true; + } + else if (name == "z_index") { + value = static_cast(z_index); + return true; + } + return false; +} + +bool UISprite::getProperty(const std::string& name, int& value) const { + if (name == "sprite_number") { + value = sprite_index; + return true; + } + else if (name == "z_index") { + value = z_index; + return true; + } + return false; +} diff --git a/src/UISprite.h b/src/UISprite.h index 0b172c6..0082ccf 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -33,7 +33,7 @@ public: void setPosition(sf::Vector2f); sf::Vector2f getPosition(); void setScale(sf::Vector2f); - sf::Vector2f getScale(); + sf::Vector2f getScale() const; void setSpriteIndex(int); int getSpriteIndex(); @@ -41,6 +41,12 @@ public: std::shared_ptr getTexture(); PyObjectsEnum derived_type() override final; + + // Property system for animations + bool setProperty(const std::string& name, float value) override; + bool setProperty(const std::string& name, int value) override; + bool getProperty(const std::string& name, float& value) const override; + bool getProperty(const std::string& name, int& value) const override; static PyObject* get_float_member(PyUISpriteObject* self, void* closure); diff --git a/src/main.cpp b/src/main.cpp index 1b97c49..e0e9835 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,8 @@ #include "CommandLineParser.h" #include "McRogueFaceConfig.h" #include "McRFPy_API.h" +#include "PyFont.h" +#include "PyTexture.h" #include #include #include @@ -44,14 +46,27 @@ int run_game_engine(const McRogueFaceConfig& config) int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]) { - // Create a headless game engine for automation API support - McRogueFaceConfig engine_config = config; - engine_config.headless = true; // Force headless mode for Python interpreter - GameEngine* engine = new GameEngine(engine_config); + // Create a game engine with the requested configuration + GameEngine* engine = new GameEngine(config); // Initialize Python with configuration McRFPy_API::init_python_with_config(config, argc, argv); + // Import mcrfpy module and store reference + McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy"); + if (!McRFPy_API::mcrf_module) { + PyErr_Print(); + std::cerr << "Failed to import mcrfpy module" << std::endl; + } else { + // Set up default_font and default_texture if not already done + if (!McRFPy_API::default_font) { + McRFPy_API::default_font = std::make_shared("assets/JetbrainsMono.ttf"); + McRFPy_API::default_texture = std::make_shared("assets/kenney_tinydungeon.png", 16, 16); + } + PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject()); + PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject()); + } + // Handle different Python modes if (!config.python_command.empty()) { // Execute command from -c @@ -161,6 +176,9 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv PyRun_InteractiveLoop(stdin, ""); } + // Run the game engine after script execution + engine->run(); + Py_Finalize(); delete engine; return result; diff --git a/tests/animation_demo.py b/tests/animation_demo.py new file mode 100644 index 0000000..f12fc70 --- /dev/null +++ b/tests/animation_demo.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +"""Animation System Demo - Shows all animation capabilities""" + +import mcrfpy +import math + +# Create main scene +mcrfpy.createScene("animation_demo") +ui = mcrfpy.sceneUI("animation_demo") +mcrfpy.setScene("animation_demo") + +# Title +title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font) +title.size = 24 +title.fill_color = (255, 255, 255) +# Note: centered property doesn't exist for Caption +ui.append(title) + +# 1. Position Animation Demo +pos_frame = mcrfpy.Frame(50, 100, 80, 80) +pos_frame.fill_color = (255, 100, 100) +pos_frame.outline = 2 +ui.append(pos_frame) + +pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font) +pos_label.fill_color = (200, 200, 200) +ui.append(pos_label) + +# 2. Size Animation Demo +size_frame = mcrfpy.Frame(200, 100, 50, 50) +size_frame.fill_color = (100, 255, 100) +size_frame.outline = 2 +ui.append(size_frame) + +size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font) +size_label.fill_color = (200, 200, 200) +ui.append(size_label) + +# 3. Color Animation Demo +color_frame = mcrfpy.Frame(350, 100, 80, 80) +color_frame.fill_color = (255, 0, 0) +ui.append(color_frame) + +color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font) +color_label.fill_color = (200, 200, 200) +ui.append(color_label) + +# 4. Easing Functions Demo +easing_y = 250 +easing_frames = [] +easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"] + +for i, easing in enumerate(easings): + x = 50 + i * 120 + + frame = mcrfpy.Frame(x, easing_y, 20, 20) + frame.fill_color = (100, 150, 255) + ui.append(frame) + easing_frames.append((frame, easing)) + + label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font) + label.size = 12 + label.fill_color = (200, 200, 200) + ui.append(label) + +# 5. Complex Animation Demo +complex_frame = mcrfpy.Frame(300, 350, 100, 100) +complex_frame.fill_color = (128, 128, 255) +complex_frame.outline = 3 +ui.append(complex_frame) + +complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font) +complex_label.fill_color = (200, 200, 200) +ui.append(complex_label) + +# Start animations +def start_animations(runtime): + # 1. Position animation - back and forth + x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut") + x_anim.start(pos_frame) + + # 2. Size animation - pulsing + w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut") + h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut") + w_anim.start(size_frame) + h_anim.start(size_frame) + + # 3. Color animation - rainbow cycle + color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear") + color_anim.start(color_frame) + + # 4. Easing demos - all move up with different easings + for frame, easing in easing_frames: + y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing) + y_anim.start(frame) + + # 5. Complex animation - multiple properties + cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut") + cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut") + cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic") + ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic") + outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear") + + cx_anim.start(complex_frame) + cy_anim.start(complex_frame) + cw_anim.start(complex_frame) + ch_anim.start(complex_frame) + outline_anim.start(complex_frame) + + # Individual color component animations + r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut") + g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut") + b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut") + + r_anim.start(complex_frame) + g_anim.start(complex_frame) + b_anim.start(complex_frame) + + print("All animations started!") + +# Reverse some animations +def reverse_animations(runtime): + # Position back + x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut") + x_anim.start(pos_frame) + + # Size back + w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut") + h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut") + w_anim.start(size_frame) + h_anim.start(size_frame) + + # Color cycle continues + color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear") + color_anim.start(color_frame) + + # Easing frames back down + for frame, easing in easing_frames: + y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing) + y_anim.start(frame) + +# Continue color cycle +def cycle_colors(runtime): + color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear") + color_anim.start(color_frame) + +# Info text +info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font) +info.fill_color = (255, 255, 200) +# Note: centered property doesn't exist for Caption +ui.append(info) + +# Schedule animations +mcrfpy.setTimer("start", start_animations, 500) +mcrfpy.setTimer("reverse", reverse_animations, 4000) +mcrfpy.setTimer("cycle", cycle_colors, 2500) + +# Exit handler +def on_key(key): + if key == "Escape": + mcrfpy.exit() + +mcrfpy.keypressScene(on_key) + +print("Animation demo started! Press Escape to exit.") \ No newline at end of file