Implement Entity3D.animate(), closes #242
Replaced the NotImplementedError stub with a full animation implementation. Entity3D now supports animating: x, y, z, world_x, world_y, world_z, rotation, rot_y, scale, scale_x, scale_y, scale_z, sprite_index, visible. Added Entity3D as a third target type in the Animation system (alongside UIDrawable and UIEntity), with startEntity3D(), applyValue(Entity3D*), and proper callback support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9e2444da69
commit
ef05152ea0
3 changed files with 203 additions and 5 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
#include "Animation.h"
|
#include "Animation.h"
|
||||||
#include "UIDrawable.h"
|
#include "UIDrawable.h"
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
|
#include "3d/Entity3D.h"
|
||||||
#include "PyAnimation.h"
|
#include "PyAnimation.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
|
|
@ -168,8 +169,46 @@ void Animation::startEntity(std::shared_ptr<UIEntity> target) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Animation::startEntity3D(std::shared_ptr<mcrf::Entity3D> target) {
|
||||||
|
if (!target) return;
|
||||||
|
|
||||||
|
entity3dTargetWeak = target;
|
||||||
|
elapsed = 0.0f;
|
||||||
|
callbackTriggered = false;
|
||||||
|
|
||||||
|
// Capture the starting value from the entity
|
||||||
|
std::visit([this, target](const auto& val) {
|
||||||
|
using T = std::decay_t<decltype(val)>;
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, float>) {
|
||||||
|
float value = 0.0f;
|
||||||
|
if (target->getProperty(targetProperty, value)) {
|
||||||
|
startValue = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<T, int>) {
|
||||||
|
// For sprite_index/visible: capture via float and convert
|
||||||
|
float fvalue = 0.0f;
|
||||||
|
if (target->getProperty(targetProperty, fvalue)) {
|
||||||
|
startValue = static_cast<int>(fvalue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Entity3D doesn't support other types
|
||||||
|
}, targetValue);
|
||||||
|
|
||||||
|
// For zero-duration animations, apply final value immediately
|
||||||
|
if (duration <= 0.0f) {
|
||||||
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
|
applyValue(target.get(), finalValue);
|
||||||
|
if (pythonCallback && !callbackTriggered) {
|
||||||
|
triggerCallback();
|
||||||
|
}
|
||||||
|
callbackTriggered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool Animation::hasValidTarget() const {
|
bool Animation::hasValidTarget() const {
|
||||||
return !targetWeak.expired() || !entityTargetWeak.expired();
|
return !targetWeak.expired() || !entityTargetWeak.expired() || !entity3dTargetWeak.expired();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::clearCallback() {
|
void Animation::clearCallback() {
|
||||||
|
|
@ -198,6 +237,10 @@ void Animation::complete() {
|
||||||
AnimationValue finalValue = interpolate(1.0f);
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
applyValue(entity.get(), finalValue);
|
applyValue(entity.get(), finalValue);
|
||||||
}
|
}
|
||||||
|
else if (auto entity3d = entity3dTargetWeak.lock()) {
|
||||||
|
AnimationValue finalValue = interpolate(1.0f);
|
||||||
|
applyValue(entity3d.get(), finalValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Animation::stop() {
|
void Animation::stop() {
|
||||||
|
|
@ -215,9 +258,10 @@ bool Animation::update(float deltaTime) {
|
||||||
// Try to lock weak_ptr to get shared_ptr
|
// Try to lock weak_ptr to get shared_ptr
|
||||||
std::shared_ptr<UIDrawable> target = targetWeak.lock();
|
std::shared_ptr<UIDrawable> target = targetWeak.lock();
|
||||||
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
|
std::shared_ptr<UIEntity> entity = entityTargetWeak.lock();
|
||||||
|
std::shared_ptr<mcrf::Entity3D> entity3d = entity3dTargetWeak.lock();
|
||||||
|
|
||||||
// If both are null, target was destroyed
|
// If all are null, target was destroyed
|
||||||
if (!target && !entity) {
|
if (!target && !entity && !entity3d) {
|
||||||
return false; // Remove this animation
|
return false; // Remove this animation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,6 +275,8 @@ bool Animation::update(float deltaTime) {
|
||||||
applyValue(target.get(), finalValue);
|
applyValue(target.get(), finalValue);
|
||||||
} else if (entity) {
|
} else if (entity) {
|
||||||
applyValue(entity.get(), finalValue);
|
applyValue(entity.get(), finalValue);
|
||||||
|
} else if (entity3d) {
|
||||||
|
applyValue(entity3d.get(), finalValue);
|
||||||
}
|
}
|
||||||
// Trigger callback
|
// Trigger callback
|
||||||
if (pythonCallback) {
|
if (pythonCallback) {
|
||||||
|
|
@ -256,6 +302,8 @@ bool Animation::update(float deltaTime) {
|
||||||
applyValue(target.get(), currentValue);
|
applyValue(target.get(), currentValue);
|
||||||
} else if (entity) {
|
} else if (entity) {
|
||||||
applyValue(entity.get(), currentValue);
|
applyValue(entity.get(), currentValue);
|
||||||
|
} else if (entity3d) {
|
||||||
|
applyValue(entity3d.get(), currentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trigger callback when animation completes
|
// Trigger callback when animation completes
|
||||||
|
|
@ -414,6 +462,22 @@ void Animation::applyValue(UIEntity* entity, const AnimationValue& value) {
|
||||||
}, value);
|
}, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Animation::applyValue(mcrf::Entity3D* entity, const AnimationValue& value) {
|
||||||
|
if (!entity) return;
|
||||||
|
|
||||||
|
std::visit([this, entity](const auto& val) {
|
||||||
|
using T = std::decay_t<decltype(val)>;
|
||||||
|
|
||||||
|
if constexpr (std::is_same_v<T, float>) {
|
||||||
|
entity->setProperty(targetProperty, val);
|
||||||
|
}
|
||||||
|
else if constexpr (std::is_same_v<T, int>) {
|
||||||
|
entity->setProperty(targetProperty, val);
|
||||||
|
}
|
||||||
|
// Entity3D doesn't support other types
|
||||||
|
}, value);
|
||||||
|
}
|
||||||
|
|
||||||
// #229 - Helper to convert UIDrawable target to Python object
|
// #229 - Helper to convert UIDrawable target to Python object
|
||||||
static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
||||||
if (!drawable) {
|
if (!drawable) {
|
||||||
|
|
@ -560,6 +624,37 @@ static PyObject* convertEntityToPython(std::shared_ptr<UIEntity> entity) {
|
||||||
return (PyObject*)pyObj;
|
return (PyObject*)pyObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to convert Entity3D target to Python object
|
||||||
|
static PyObject* convertEntity3DToPython(std::shared_ptr<mcrf::Entity3D> entity) {
|
||||||
|
if (!entity) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the entity's cached Python self pointer if available
|
||||||
|
if (entity->self) {
|
||||||
|
Py_INCREF(entity->self);
|
||||||
|
return entity->self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new wrapper
|
||||||
|
PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity3D");
|
||||||
|
if (!type) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pyObj = (PyEntity3DObject*)type->tp_alloc(type, 0);
|
||||||
|
Py_DECREF(type);
|
||||||
|
|
||||||
|
if (!pyObj) {
|
||||||
|
Py_RETURN_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
pyObj->data = entity;
|
||||||
|
pyObj->weakreflist = NULL;
|
||||||
|
|
||||||
|
return (PyObject*)pyObj;
|
||||||
|
}
|
||||||
|
|
||||||
// #229 - Helper to convert AnimationValue to Python object
|
// #229 - Helper to convert AnimationValue to Python object
|
||||||
static PyObject* animationValueToPython(const AnimationValue& value) {
|
static PyObject* animationValueToPython(const AnimationValue& value) {
|
||||||
return std::visit([](const auto& val) -> PyObject* {
|
return std::visit([](const auto& val) -> PyObject* {
|
||||||
|
|
@ -609,6 +704,8 @@ void Animation::triggerCallback() {
|
||||||
targetObj = convertDrawableToPython(drawable);
|
targetObj = convertDrawableToPython(drawable);
|
||||||
} else if (auto entity = entityTargetWeak.lock()) {
|
} else if (auto entity = entityTargetWeak.lock()) {
|
||||||
targetObj = convertEntityToPython(entity);
|
targetObj = convertEntityToPython(entity);
|
||||||
|
} else if (auto entity3d = entity3dTargetWeak.lock()) {
|
||||||
|
targetObj = convertEntity3DToPython(entity3d);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If target conversion failed, use None
|
// If target conversion failed, use None
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
// Forward declarations
|
// Forward declarations
|
||||||
class UIDrawable;
|
class UIDrawable;
|
||||||
class UIEntity;
|
class UIEntity;
|
||||||
|
namespace mcrf { class Entity3D; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConflictMode - How to handle multiple animations on the same property (#120)
|
* ConflictMode - How to handle multiple animations on the same property (#120)
|
||||||
|
|
@ -59,6 +60,9 @@ public:
|
||||||
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
|
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
|
||||||
void startEntity(std::shared_ptr<UIEntity> target);
|
void startEntity(std::shared_ptr<UIEntity> target);
|
||||||
|
|
||||||
|
// Apply this animation to a 3D entity
|
||||||
|
void startEntity3D(std::shared_ptr<mcrf::Entity3D> target);
|
||||||
|
|
||||||
// Complete the animation immediately (jump to final value)
|
// Complete the animation immediately (jump to final value)
|
||||||
void complete();
|
void complete();
|
||||||
|
|
||||||
|
|
@ -90,6 +94,7 @@ public:
|
||||||
void* getTargetPtr() const {
|
void* getTargetPtr() const {
|
||||||
if (auto sp = targetWeak.lock()) return sp.get();
|
if (auto sp = targetWeak.lock()) return sp.get();
|
||||||
if (auto sp = entityTargetWeak.lock()) return sp.get();
|
if (auto sp = entityTargetWeak.lock()) return sp.get();
|
||||||
|
if (auto sp = entity3dTargetWeak.lock()) return sp.get();
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +111,7 @@ private:
|
||||||
// RAII: Use weak_ptr for safe target tracking
|
// RAII: Use weak_ptr for safe target tracking
|
||||||
std::weak_ptr<UIDrawable> targetWeak;
|
std::weak_ptr<UIDrawable> targetWeak;
|
||||||
std::weak_ptr<UIEntity> entityTargetWeak;
|
std::weak_ptr<UIEntity> entityTargetWeak;
|
||||||
|
std::weak_ptr<mcrf::Entity3D> entity3dTargetWeak;
|
||||||
|
|
||||||
// Callback support
|
// Callback support
|
||||||
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
|
PyObject* pythonCallback = nullptr; // Python callback function (we own a reference)
|
||||||
|
|
@ -121,6 +127,7 @@ private:
|
||||||
// Helper to apply value to target
|
// Helper to apply value to target
|
||||||
void applyValue(UIDrawable* target, const AnimationValue& value);
|
void applyValue(UIDrawable* target, const AnimationValue& value);
|
||||||
void applyValue(UIEntity* entity, const AnimationValue& value);
|
void applyValue(UIEntity* entity, const AnimationValue& value);
|
||||||
|
void applyValue(mcrf::Entity3D* entity, const AnimationValue& value);
|
||||||
|
|
||||||
// Trigger callback when animation completes
|
// Trigger callback when animation completes
|
||||||
void triggerCallback();
|
void triggerCallback();
|
||||||
|
|
|
||||||
94
tests/unit/test_entity3d_animate.py
Normal file
94
tests/unit/test_entity3d_animate.py
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
"""Test Entity3D.animate() (issue #242)"""
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
vp = mcrfpy.Viewport3D(pos=(0,0), size=(100,100))
|
||||||
|
vp.set_grid_size(16, 16)
|
||||||
|
|
||||||
|
# Test 1: Basic x animation
|
||||||
|
e = mcrfpy.Entity3D(pos=(0,0), scale=1.0)
|
||||||
|
vp.entities.append(e)
|
||||||
|
start_x = e.world_pos[0]
|
||||||
|
anim = e.animate('x', 10.0, 1.0, 'linear')
|
||||||
|
if anim is None:
|
||||||
|
errors.append("animate() should return Animation object")
|
||||||
|
for _ in range(5):
|
||||||
|
mcrfpy.step(0.25)
|
||||||
|
if abs(e.world_pos[0] - 10.0) > 0.5:
|
||||||
|
errors.append(f"x animation: expected ~10.0, got {e.world_pos[0]}")
|
||||||
|
|
||||||
|
# Test 2: Scale animation
|
||||||
|
e2 = mcrfpy.Entity3D(pos=(0,0), scale=1.0)
|
||||||
|
vp.entities.append(e2)
|
||||||
|
e2.animate('scale', 5.0, 0.5, 'linear')
|
||||||
|
for _ in range(3):
|
||||||
|
mcrfpy.step(0.25)
|
||||||
|
if abs(e2.scale - 5.0) > 0.5:
|
||||||
|
errors.append(f"scale animation: expected ~5.0, got {e2.scale}")
|
||||||
|
|
||||||
|
# Test 3: Rotation animation
|
||||||
|
e3 = mcrfpy.Entity3D(pos=(0,0), scale=1.0)
|
||||||
|
vp.entities.append(e3)
|
||||||
|
e3.animate('rotation', 90.0, 0.5, 'easeInOut')
|
||||||
|
for _ in range(3):
|
||||||
|
mcrfpy.step(0.25)
|
||||||
|
if abs(e3.rotation - 90.0) > 0.5:
|
||||||
|
errors.append(f"rotation animation: expected ~90.0, got {e3.rotation}")
|
||||||
|
|
||||||
|
# Test 4: Delta animation
|
||||||
|
e4 = mcrfpy.Entity3D(pos=(3,3), scale=1.0)
|
||||||
|
vp.entities.append(e4)
|
||||||
|
start_x = e4.world_pos[0]
|
||||||
|
e4.animate('x', 2.0, 0.5, 'linear', delta=True)
|
||||||
|
for _ in range(3):
|
||||||
|
mcrfpy.step(0.25)
|
||||||
|
expected = start_x + 2.0
|
||||||
|
if abs(e4.world_pos[0] - expected) > 0.5:
|
||||||
|
errors.append(f"delta animation: expected ~{expected}, got {e4.world_pos[0]}")
|
||||||
|
|
||||||
|
# Test 5: Invalid property raises ValueError
|
||||||
|
try:
|
||||||
|
e.animate('nonexistent', 1.0, 1.0, 'linear')
|
||||||
|
errors.append("Invalid property should raise ValueError")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Test 6: Invalid target type raises TypeError
|
||||||
|
try:
|
||||||
|
e.animate('x', "not_a_number", 1.0, 'linear')
|
||||||
|
errors.append("String target should raise TypeError")
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Test 7: Callback
|
||||||
|
callback_called = [False]
|
||||||
|
def on_complete(target, prop, value):
|
||||||
|
callback_called[0] = True
|
||||||
|
|
||||||
|
e5 = mcrfpy.Entity3D(pos=(0,0), scale=1.0)
|
||||||
|
vp.entities.append(e5)
|
||||||
|
e5.animate('x', 5.0, 0.25, 'linear', callback=on_complete)
|
||||||
|
for _ in range(3):
|
||||||
|
mcrfpy.step(0.15)
|
||||||
|
if not callback_called[0]:
|
||||||
|
errors.append("Animation callback was not called")
|
||||||
|
|
||||||
|
# Test 8: Easing enum
|
||||||
|
e6 = mcrfpy.Entity3D(pos=(0,0), scale=1.0)
|
||||||
|
vp.entities.append(e6)
|
||||||
|
try:
|
||||||
|
e6.animate('x', 5.0, 1.0, mcrfpy.Easing.EASE_IN_OUT)
|
||||||
|
except Exception as ex:
|
||||||
|
errors.append(f"Easing enum should work: {ex}")
|
||||||
|
for _ in range(5):
|
||||||
|
mcrfpy.step(0.25)
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
for err in errors:
|
||||||
|
print(f"FAIL: {err}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("PASS: Entity3D.animate() (issue #242)", file=sys.stderr)
|
||||||
|
sys.exit(0)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue