Compare commits

...

4 commits

Author SHA1 Message Date
d878c8684d Easing functions as enum 2026-01-04 12:59:28 -05:00
357c2ac7d7 Animation fixes: 0-duration edge case, integer value bug resolution 2026-01-04 00:45:16 -05:00
cec76b63dc Timer overhaul: update tests 2026-01-03 22:44:53 -05:00
5d41292bf6 Timer refactor: stopwatch-like semantics, mcrfpy.timers collection closes #173
Major Timer API improvements:
- Add `stopped` flag to Timer C++ class for proper state management
- Add `start()` method to restart stopped timers (preserves callback)
- Add `stop()` method that removes from engine but preserves callback
- Make `active` property read-write (True=start/resume, False=pause)
- Add `start=True` init parameter to create timers in stopped state
- Add `mcrfpy.timers` module-level collection (tuple of active timers)
- One-shot timers now set stopped=true instead of clearing callback
- Remove deprecated `setTimer()` and `delTimer()` module functions

Timer callbacks now receive (timer, runtime) instead of just (runtime).
Updated all tests to use new Timer API and callback signature.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 22:09:18 -05:00
104 changed files with 1572 additions and 817 deletions

View file

@ -57,15 +57,15 @@ Animation::~Animation() {
void Animation::start(std::shared_ptr<UIDrawable> target) { void Animation::start(std::shared_ptr<UIDrawable> target) {
if (!target) return; if (!target) return;
targetWeak = target; targetWeak = target;
elapsed = 0.0f; elapsed = 0.0f;
callbackTriggered = false; // Reset callback state callbackTriggered = false; // Reset callback state
// Capture start value from target // Capture start value from target
std::visit([this, &target](const auto& targetVal) { std::visit([this, &target](const auto& targetVal) {
using T = std::decay_t<decltype(targetVal)>; using T = std::decay_t<decltype(targetVal)>;
if constexpr (std::is_same_v<T, float>) { if constexpr (std::is_same_v<T, float>) {
float value; float value;
if (target->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
@ -73,9 +73,15 @@ void Animation::start(std::shared_ptr<UIDrawable> target) {
} }
} }
else if constexpr (std::is_same_v<T, int>) { else if constexpr (std::is_same_v<T, int>) {
int value; // Most UI properties use float, so try float first, then int
if (target->getProperty(targetProperty, value)) { float fvalue;
startValue = value; if (target->getProperty(targetProperty, fvalue)) {
startValue = static_cast<int>(fvalue);
} else {
int ivalue;
if (target->getProperty(targetProperty, ivalue)) {
startValue = ivalue;
}
} }
} }
else if constexpr (std::is_same_v<T, std::vector<int>>) { else if constexpr (std::is_same_v<T, std::vector<int>>) {
@ -104,19 +110,29 @@ void Animation::start(std::shared_ptr<UIDrawable> target) {
} }
} }
}, targetValue); }, 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;
}
} }
void Animation::startEntity(std::shared_ptr<UIEntity> target) { void Animation::startEntity(std::shared_ptr<UIEntity> target) {
if (!target) return; if (!target) return;
entityTargetWeak = target; entityTargetWeak = target;
elapsed = 0.0f; elapsed = 0.0f;
callbackTriggered = false; // Reset callback state callbackTriggered = false; // Reset callback state
// Capture the starting value from the entity // Capture the starting value from the entity
std::visit([this, target](const auto& val) { std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>; using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) { if constexpr (std::is_same_v<T, float>) {
float value = 0.0f; float value = 0.0f;
if (target->getProperty(targetProperty, value)) { if (target->getProperty(targetProperty, value)) {
@ -131,6 +147,16 @@ void Animation::startEntity(std::shared_ptr<UIEntity> target) {
} }
// Entities don't support other types yet // Entities don't support other types yet
}, targetValue); }, 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 {
@ -169,39 +195,55 @@ 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();
// If both are null, target was destroyed // If both are null, target was destroyed
if (!target && !entity) { if (!target && !entity) {
return false; // Remove this animation return false; // Remove this animation
} }
// Handle already-complete animations (e.g., duration=0)
// Apply final value once before returning
if (isComplete()) { if (isComplete()) {
if (!callbackTriggered) {
// Apply final value for zero-duration animations
AnimationValue finalValue = interpolate(1.0f);
if (target) {
applyValue(target.get(), finalValue);
} else if (entity) {
applyValue(entity.get(), finalValue);
}
// Trigger callback
if (pythonCallback) {
triggerCallback();
}
callbackTriggered = true;
}
return false; return false;
} }
elapsed += deltaTime; elapsed += deltaTime;
elapsed = std::min(elapsed, duration); elapsed = std::min(elapsed, duration);
// Calculate easing value (0.0 to 1.0) // Calculate easing value (0.0 to 1.0)
float t = duration > 0 ? elapsed / duration : 1.0f; float t = duration > 0 ? elapsed / duration : 1.0f;
float easedT = easingFunc(t); float easedT = easingFunc(t);
// Get interpolated value // Get interpolated value
AnimationValue currentValue = interpolate(easedT); AnimationValue currentValue = interpolate(easedT);
// Apply to whichever target is valid // Apply to whichever target is valid
if (target) { if (target) {
applyValue(target.get(), currentValue); applyValue(target.get(), currentValue);
} else if (entity) { } else if (entity) {
applyValue(entity.get(), currentValue); applyValue(entity.get(), currentValue);
} }
// Trigger callback when animation completes // Trigger callback when animation completes
// Check pythonCallback again in case it was cleared during update // Check pythonCallback again in case it was cleared during update
if (isComplete() && !callbackTriggered && pythonCallback) { if (isComplete() && !callbackTriggered && pythonCallback) {
triggerCallback(); triggerCallback();
} }
return !isComplete(); return !isComplete();
} }
@ -310,15 +352,19 @@ AnimationValue Animation::interpolate(float t) const {
void Animation::applyValue(UIDrawable* target, const AnimationValue& value) { void Animation::applyValue(UIDrawable* target, const AnimationValue& value) {
if (!target) return; if (!target) return;
std::visit([this, target](const auto& val) { std::visit([this, target](const auto& val) {
using T = std::decay_t<decltype(val)>; using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, float>) { if constexpr (std::is_same_v<T, float>) {
target->setProperty(targetProperty, val); target->setProperty(targetProperty, val);
} }
else if constexpr (std::is_same_v<T, int>) { else if constexpr (std::is_same_v<T, int>) {
target->setProperty(targetProperty, val); // Most UI properties use float setProperty, so try float first
if (!target->setProperty(targetProperty, static_cast<float>(val))) {
// Fall back to int if float didn't work
target->setProperty(targetProperty, val);
}
} }
else if constexpr (std::is_same_v<T, sf::Color>) { else if constexpr (std::is_same_v<T, sf::Color>) {
target->setProperty(targetProperty, val); target->setProperty(targetProperty, val);

View file

@ -357,51 +357,35 @@ std::shared_ptr<Timer> GameEngine::getTimer(const std::string& name)
return nullptr; return nullptr;
} }
void GameEngine::manageTimer(std::string name, PyObject* target, int interval) // Note: manageTimer() removed in #173 - use Timer objects directly
{
auto it = timers.find(name);
// #153 - In headless mode, use simulation_time instead of real-time clock
int now = headless ? simulation_time : runtime.getElapsedTime().asMilliseconds();
if (it != timers.end()) // overwrite existing
{
if (target == NULL || target == Py_None)
{
// Delete: Overwrite existing timer with one that calls None. This will be deleted in the next timer check
// see gitea issue #4: this allows for a timer to be deleted during its own call to itself
timers[name] = std::make_shared<Timer>(Py_None, 1000, now);
return;
}
}
if (target == NULL || target == Py_None)
{
std::cout << "Refusing to initialize timer to None. It's not an error, it's just pointless." << std::endl;
return;
}
timers[name] = std::make_shared<Timer>(target, interval, now);
}
void GameEngine::testTimers() void GameEngine::testTimers()
{ {
int now = runtime.getElapsedTime().asMilliseconds(); int now = headless ? simulation_time : runtime.getElapsedTime().asMilliseconds();
auto it = timers.begin(); auto it = timers.begin();
while (it != timers.end()) while (it != timers.end())
{ {
// Keep a local copy of the timer to prevent use-after-free. // Keep a local copy of the timer to prevent use-after-free.
// If the callback calls delTimer(), the map entry gets replaced, // If the callback calls stop(), the timer may be marked for removal,
// but we need the Timer object to survive until test() returns. // but we need the Timer object to survive until test() returns.
auto timer = it->second; auto timer = it->second;
timer->test(now);
// Remove timers that have been cancelled or are one-shot and fired. // Skip stopped timers (they'll be removed below)
if (!timer->isStopped()) {
timer->test(now);
}
// Remove timers that have been stopped (including one-shot timers that fired).
// The stopped flag is the authoritative marker for "remove from map".
// Note: Check it->second (current map value) in case callback replaced it. // Note: Check it->second (current map value) in case callback replaced it.
if (!it->second->getCallback() || it->second->getCallback() == Py_None) if (it->second->isStopped())
{ {
it = timers.erase(it); it = timers.erase(it);
} }
else else
{
it++; it++;
}
} }
} }

View file

@ -169,7 +169,7 @@ public:
int getFrame() { return currentFrame; } int getFrame() { return currentFrame; }
float getFrameTime() { return frameTime; } float getFrameTime() { return frameTime; }
sf::View getView() { return visible; } sf::View getView() { return visible; }
void manageTimer(std::string, PyObject*, int); // Note: manageTimer() removed in #173 - use Timer objects directly
std::shared_ptr<Timer> getTimer(const std::string& name); std::shared_ptr<Timer> getTimer(const std::string& name);
void setWindowScale(float); void setWindowScale(float);
bool isHeadless() const { return headless; } bool isHeadless() const { return headless; }

View file

@ -10,6 +10,7 @@
#include "PySceneObject.h" #include "PySceneObject.h"
#include "PyFOV.h" #include "PyFOV.h"
#include "PyTransition.h" #include "PyTransition.h"
#include "PyEasing.h"
#include "PySound.h" #include "PySound.h"
#include "PyMusic.h" #include "PyMusic.h"
#include "PyKeyboard.h" #include "PyKeyboard.h"
@ -24,6 +25,7 @@
#include "GridLayers.h" #include "GridLayers.h"
#include "Resources.h" #include "Resources.h"
#include "PyScene.h" #include "PyScene.h"
#include "PythonObjectCache.h"
#include <filesystem> #include <filesystem>
#include <cstring> #include <cstring>
#include <libtcod.h> #include <libtcod.h>
@ -52,6 +54,10 @@ static PyObject* mcrfpy_module_getattr(PyObject* self, PyObject* args)
return McRFPy_API::api_get_scenes(); return McRFPy_API::api_get_scenes();
} }
if (strcmp(name, "timers") == 0) {
return McRFPy_API::api_get_timers();
}
if (strcmp(name, "default_transition") == 0) { if (strcmp(name, "default_transition") == 0) {
return PyTransition::to_python(PyTransition::default_transition); return PyTransition::to_python(PyTransition::default_transition);
} }
@ -80,6 +86,11 @@ static int mcrfpy_module_setattro(PyObject* self, PyObject* name, PyObject* valu
return -1; return -1;
} }
if (strcmp(name_str, "timers") == 0) {
PyErr_SetString(PyExc_AttributeError, "'timers' is read-only");
return -1;
}
if (strcmp(name_str, "default_transition") == 0) { if (strcmp(name_str, "default_transition") == 0) {
TransitionType trans; TransitionType trans;
if (!PyTransition::from_arg(value, &trans, nullptr)) { if (!PyTransition::from_arg(value, &trans, nullptr)) {
@ -138,26 +149,7 @@ static PyTypeObject McRFPyModuleType = {
static PyMethodDef mcrfpyMethods[] = { static PyMethodDef mcrfpyMethods[] = {
{"setTimer", McRFPy_API::_setTimer, METH_VARARGS, // Note: setTimer and delTimer removed in #173 - use Timer objects instead
MCRF_FUNCTION(setTimer,
MCRF_SIG("(name: str, handler: callable, interval: int)", "None"),
MCRF_DESC("Create or update a recurring timer."),
MCRF_ARGS_START
MCRF_ARG("name", "Unique identifier for the timer")
MCRF_ARG("handler", "Function called with (runtime: float) parameter")
MCRF_ARG("interval", "Time between calls in milliseconds")
MCRF_RETURNS("None")
MCRF_NOTE("If a timer with this name exists, it will be replaced. The handler receives the total runtime in seconds as its argument.")
)},
{"delTimer", McRFPy_API::_delTimer, METH_VARARGS,
MCRF_FUNCTION(delTimer,
MCRF_SIG("(name: str)", "None"),
MCRF_DESC("Stop and remove a timer."),
MCRF_ARGS_START
MCRF_ARG("name", "Timer identifier to remove")
MCRF_RETURNS("None")
MCRF_NOTE("No error is raised if the timer doesn't exist.")
)},
{"step", McRFPy_API::_step, METH_VARARGS, {"step", McRFPy_API::_step, METH_VARARGS,
MCRF_FUNCTION(step, MCRF_FUNCTION(step,
MCRF_SIG("(dt: float = None)", "float"), MCRF_SIG("(dt: float = None)", "float"),
@ -438,6 +430,13 @@ PyObject* PyInit_mcrfpy()
// Note: default_transition and default_transition_duration are handled via // Note: default_transition and default_transition_duration are handled via
// mcrfpy_module_getattr/setattro using PyTransition::default_transition/default_duration // mcrfpy_module_getattr/setattro using PyTransition::default_transition/default_duration
// Add Easing enum class (uses Python's IntEnum)
PyObject* easing_class = PyEasing::create_enum_class(m);
if (!easing_class) {
// If enum creation fails, continue without it (non-fatal)
PyErr_Clear();
}
// Add automation submodule // Add automation submodule
PyObject* automation_module = McRFPy_Automation::init_automation_module(); PyObject* automation_module = McRFPy_Automation::init_automation_module();
if (automation_module != NULL) { if (automation_module != NULL) {
@ -883,22 +882,34 @@ PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) {
return Py_None; return Py_None;
} }
PyObject* McRFPy_API::_setTimer(PyObject* self, PyObject* args) { // TODO - compare with UIDrawable mouse & Scene Keyboard methods - inconsistent responsibility for incref/decref around mcrogueface // #173: Get all timers as a tuple of Python Timer objects
const char* name; PyObject* McRFPy_API::api_get_timers()
PyObject* callable; {
int interval; if (!game) {
if (!PyArg_ParseTuple(args, "sOi", &name, &callable, &interval)) return NULL; return PyTuple_New(0);
game->manageTimer(name, callable, interval); }
Py_INCREF(Py_None);
return Py_None;
}
PyObject* McRFPy_API::_delTimer(PyObject* self, PyObject* args) { // Count timers that have Python wrappers
const char* name; std::vector<PyObject*> timer_objs;
if (!PyArg_ParseTuple(args, "s", &name)) return NULL; for (auto& pair : game->timers) {
game->manageTimer(name, NULL, 0); auto& timer = pair.second;
Py_INCREF(Py_None); if (timer && timer->serial_number != 0) {
return Py_None; PyObject* timer_obj = PythonObjectCache::getInstance().lookup(timer->serial_number);
if (timer_obj && timer_obj != Py_None) {
timer_objs.push_back(timer_obj);
}
}
}
PyObject* tuple = PyTuple_New(timer_objs.size());
if (!tuple) return NULL;
for (Py_ssize_t i = 0; i < static_cast<Py_ssize_t>(timer_objs.size()); i++) {
Py_INCREF(timer_objs[i]);
PyTuple_SET_ITEM(tuple, i, timer_objs[i]);
}
return tuple;
} }
// #153 - Headless simulation control // #153 - Headless simulation control

View file

@ -43,9 +43,7 @@ public:
// Internal - used by PySceneObject::activate() // Internal - used by PySceneObject::activate()
static PyObject* _setScene(PyObject*, PyObject*); static PyObject* _setScene(PyObject*, PyObject*);
// timer control // Note: setTimer/delTimer removed in #173 - use Timer objects instead
static PyObject* _setTimer(PyObject*, PyObject*);
static PyObject* _delTimer(PyObject*, PyObject*);
// #153 - Headless simulation control // #153 - Headless simulation control
static PyObject* _step(PyObject*, PyObject*); static PyObject* _step(PyObject*, PyObject*);
@ -88,6 +86,9 @@ public:
static int api_set_current_scene(PyObject* value); static int api_set_current_scene(PyObject* value);
static PyObject* api_get_scenes(); static PyObject* api_get_scenes();
// #173: Module-level timer collection accessor
static PyObject* api_get_timers();
// Exception handling - signal game loop to exit on unhandled Python exceptions // Exception handling - signal game loop to exit on unhandled Python exceptions
static std::atomic<bool> exception_occurred; static std::atomic<bool> exception_occurred;
static std::atomic<int> exit_code; static std::atomic<int> exit_code;

View file

@ -1,6 +1,7 @@
#include "PyAnimation.h" #include "PyAnimation.h"
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "McRFPy_Doc.h" #include "McRFPy_Doc.h"
#include "PyEasing.h"
#include "UIDrawable.h" #include "UIDrawable.h"
#include "UIFrame.h" #include "UIFrame.h"
#include "UICaption.h" #include "UICaption.h"
@ -20,16 +21,16 @@ PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) { int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr}; static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", nullptr};
const char* property_name; const char* property_name;
PyObject* target_value; PyObject* target_value;
float duration; float duration;
const char* easing_name = "linear"; PyObject* easing_arg = Py_None;
int delta = 0; int delta = 0;
PyObject* callback = nullptr; PyObject* callback = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|spO", const_cast<char**>(keywords), if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpO", const_cast<char**>(keywords),
&property_name, &target_value, &duration, &easing_name, &delta, &callback)) { &property_name, &target_value, &duration, &easing_arg, &delta, &callback)) {
return -1; return -1;
} }
@ -98,10 +99,13 @@ int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string"); PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
return -1; return -1;
} }
// Get easing function // Get easing function from argument (enum, string, int, or None)
EasingFunction easingFunc = EasingFunctions::getByName(easing_name); EasingFunction easingFunc;
if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) {
return -1; // Error already set by from_arg
}
// Create the Animation // Create the Animation
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback); self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0, callback);
@ -113,6 +117,48 @@ void PyAnimation::dealloc(PyAnimationObject* self) {
Py_TYPE(self)->tp_free((PyObject*)self); Py_TYPE(self)->tp_free((PyObject*)self);
} }
PyObject* PyAnimation::repr(PyAnimationObject* self) {
if (!self->data) {
return PyUnicode_FromString("<Animation (uninitialized)>");
}
std::string property = self->data->getTargetProperty();
float duration = self->data->getDuration();
float elapsed = self->data->getElapsed();
bool complete = self->data->isComplete();
bool delta = self->data->isDelta();
bool hasTarget = self->data->hasValidTarget();
// Format: <Animation 'property' duration=2.0s elapsed=0.5s running>
// or: <Animation 'property' duration=2.0s complete>
// or: <Animation 'property' duration=2.0s delta complete>
// or: <Animation 'property' duration=2.0s (no target)>
std::string status;
if (!hasTarget) {
status = "(no target)";
} else if (complete) {
status = "complete";
} else {
char buf[32];
snprintf(buf, sizeof(buf), "elapsed=%.2fs", elapsed);
status = buf;
}
char result[256];
if (delta) {
snprintf(result, sizeof(result),
"<Animation '%s' duration=%.2fs delta %s>",
property.c_str(), duration, status.c_str());
} else {
snprintf(result, sizeof(result),
"<Animation '%s' duration=%.2fs %s>",
property.c_str(), duration, status.c_str());
}
return PyUnicode_FromString(result);
}
PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) { PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) {
return PyUnicode_FromString(self->data->getTargetProperty().c_str()); return PyUnicode_FromString(self->data->getTargetProperty().c_str());
} }

View file

@ -16,6 +16,7 @@ public:
static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds); static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds); static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyAnimationObject* self); static void dealloc(PyAnimationObject* self);
static PyObject* repr(PyAnimationObject* self);
// Properties // Properties
static PyObject* get_property(PyAnimationObject* self, void* closure); static PyObject* get_property(PyAnimationObject* self, void* closure);
@ -42,8 +43,59 @@ namespace mcrfpydef {
.tp_basicsize = sizeof(PyAnimationObject), .tp_basicsize = sizeof(PyAnimationObject),
.tp_itemsize = 0, .tp_itemsize = 0,
.tp_dealloc = (destructor)PyAnimation::dealloc, .tp_dealloc = (destructor)PyAnimation::dealloc,
.tp_repr = (reprfunc)PyAnimation::repr,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Animation object for animating UI properties"), .tp_doc = PyDoc_STR(
"Animation(property: str, target: Any, duration: float, easing: str = 'linear', delta: bool = False, callback: Callable = None)\n"
"\n"
"Create an animation that interpolates a property value over time.\n"
"\n"
"Args:\n"
" property: Property name to animate. Valid properties depend on target type:\n"
" - Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size'\n"
" - Appearance: 'fill_color', 'outline_color', 'outline', 'opacity'\n"
" - Sprite: 'sprite_index', 'sprite_number', 'scale'\n"
" - Grid: 'center', 'zoom'\n"
" - Caption: 'text'\n"
" - Sub-properties: 'fill_color.r', 'fill_color.g', 'fill_color.b', 'fill_color.a'\n"
" target: Target value for the animation. Type depends on property:\n"
" - float: For numeric properties (x, y, w, h, scale, opacity, zoom)\n"
" - int: For integer properties (sprite_index)\n"
" - tuple (r, g, b[, a]): For color properties\n"
" - tuple (x, y): For vector properties (pos, size, center)\n"
" - list[int]: For sprite animation sequences\n"
" - str: For text animation\n"
" duration: Animation duration in seconds.\n"
" easing: Easing function name. Options:\n"
" - 'linear' (default)\n"
" - 'easeIn', 'easeOut', 'easeInOut'\n"
" - 'easeInQuad', 'easeOutQuad', 'easeInOutQuad'\n"
" - 'easeInCubic', 'easeOutCubic', 'easeInOutCubic'\n"
" - 'easeInQuart', 'easeOutQuart', 'easeInOutQuart'\n"
" - 'easeInSine', 'easeOutSine', 'easeInOutSine'\n"
" - 'easeInExpo', 'easeOutExpo', 'easeInOutExpo'\n"
" - 'easeInCirc', 'easeOutCirc', 'easeInOutCirc'\n"
" - 'easeInElastic', 'easeOutElastic', 'easeInOutElastic'\n"
" - 'easeInBack', 'easeOutBack', 'easeInOutBack'\n"
" - 'easeInBounce', 'easeOutBounce', 'easeInOutBounce'\n"
" delta: If True, target is relative to start value (additive). Default False.\n"
" callback: Function(animation, target) called when animation completes.\n"
"\n"
"Example:\n"
" # Move a frame from current position to x=500 over 2 seconds\n"
" anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')\n"
" anim.start(my_frame)\n"
"\n"
" # Fade out with callback\n"
" def on_done(anim, target):\n"
" print('Animation complete!')\n"
" fade = mcrfpy.Animation('fill_color.a', 0, 1.0, callback=on_done)\n"
" fade.start(my_sprite)\n"
"\n"
" # Animate through sprite frames\n"
" walk_cycle = mcrfpy.Animation('sprite_index', [0,1,2,3,2,1], 0.5, 'linear')\n"
" walk_cycle.start(my_entity)\n"
),
.tp_methods = PyAnimation::methods, .tp_methods = PyAnimation::methods,
.tp_getset = PyAnimation::getsetters, .tp_getset = PyAnimation::getsetters,
.tp_init = (initproc)PyAnimation::init, .tp_init = (initproc)PyAnimation::init,

View file

@ -47,7 +47,34 @@ namespace mcrfpydef {
.tp_repr = PyColor::repr, .tp_repr = PyColor::repr,
.tp_hash = PyColor::hash, .tp_hash = PyColor::hash,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("SFML Color Object"), .tp_doc = PyDoc_STR(
"Color(r: int = 0, g: int = 0, b: int = 0, a: int = 255)\n"
"\n"
"RGBA color representation.\n"
"\n"
"Args:\n"
" r: Red component (0-255)\n"
" g: Green component (0-255)\n"
" b: Blue component (0-255)\n"
" a: Alpha component (0-255, default 255 = opaque)\n"
"\n"
"Note:\n"
" When accessing colors from UI elements (e.g., frame.fill_color),\n"
" you receive a COPY of the color. Modifying it doesn't affect the\n"
" original. To change a component:\n"
"\n"
" # This does NOT work:\n"
" frame.fill_color.r = 255 # Modifies a temporary copy\n"
"\n"
" # Do this instead:\n"
" c = frame.fill_color\n"
" c.r = 255\n"
" frame.fill_color = c\n"
"\n"
" # Or use Animation for sub-properties:\n"
" anim = mcrfpy.Animation('fill_color.r', 255, 0.5, 'linear')\n"
" anim.start(frame)\n"
),
.tp_methods = PyColor::methods, .tp_methods = PyColor::methods,
.tp_getset = PyColor::getsetters, .tp_getset = PyColor::getsetters,
.tp_init = (initproc)PyColor::init, .tp_init = (initproc)PyColor::init,

228
src/PyEasing.cpp Normal file
View file

@ -0,0 +1,228 @@
#include "PyEasing.h"
#include "McRFPy_API.h"
// Static storage for cached enum class reference
PyObject* PyEasing::easing_enum_class = nullptr;
// Easing function table - maps enum value to function and name
struct EasingEntry {
const char* name;
int value;
EasingFunction func;
};
static const EasingEntry easing_table[] = {
{"LINEAR", 0, EasingFunctions::linear},
{"EASE_IN", 1, EasingFunctions::easeIn},
{"EASE_OUT", 2, EasingFunctions::easeOut},
{"EASE_IN_OUT", 3, EasingFunctions::easeInOut},
{"EASE_IN_QUAD", 4, EasingFunctions::easeInQuad},
{"EASE_OUT_QUAD", 5, EasingFunctions::easeOutQuad},
{"EASE_IN_OUT_QUAD", 6, EasingFunctions::easeInOutQuad},
{"EASE_IN_CUBIC", 7, EasingFunctions::easeInCubic},
{"EASE_OUT_CUBIC", 8, EasingFunctions::easeOutCubic},
{"EASE_IN_OUT_CUBIC", 9, EasingFunctions::easeInOutCubic},
{"EASE_IN_QUART", 10, EasingFunctions::easeInQuart},
{"EASE_OUT_QUART", 11, EasingFunctions::easeOutQuart},
{"EASE_IN_OUT_QUART", 12, EasingFunctions::easeInOutQuart},
{"EASE_IN_SINE", 13, EasingFunctions::easeInSine},
{"EASE_OUT_SINE", 14, EasingFunctions::easeOutSine},
{"EASE_IN_OUT_SINE", 15, EasingFunctions::easeInOutSine},
{"EASE_IN_EXPO", 16, EasingFunctions::easeInExpo},
{"EASE_OUT_EXPO", 17, EasingFunctions::easeOutExpo},
{"EASE_IN_OUT_EXPO", 18, EasingFunctions::easeInOutExpo},
{"EASE_IN_CIRC", 19, EasingFunctions::easeInCirc},
{"EASE_OUT_CIRC", 20, EasingFunctions::easeOutCirc},
{"EASE_IN_OUT_CIRC", 21, EasingFunctions::easeInOutCirc},
{"EASE_IN_ELASTIC", 22, EasingFunctions::easeInElastic},
{"EASE_OUT_ELASTIC", 23, EasingFunctions::easeOutElastic},
{"EASE_IN_OUT_ELASTIC", 24, EasingFunctions::easeInOutElastic},
{"EASE_IN_BACK", 25, EasingFunctions::easeInBack},
{"EASE_OUT_BACK", 26, EasingFunctions::easeOutBack},
{"EASE_IN_OUT_BACK", 27, EasingFunctions::easeInOutBack},
{"EASE_IN_BOUNCE", 28, EasingFunctions::easeInBounce},
{"EASE_OUT_BOUNCE", 29, EasingFunctions::easeOutBounce},
{"EASE_IN_OUT_BOUNCE", 30, EasingFunctions::easeInOutBounce},
};
// Old string names (for backwards compatibility)
static const char* legacy_names[] = {
"linear", "easeIn", "easeOut", "easeInOut",
"easeInQuad", "easeOutQuad", "easeInOutQuad",
"easeInCubic", "easeOutCubic", "easeInOutCubic",
"easeInQuart", "easeOutQuart", "easeInOutQuart",
"easeInSine", "easeOutSine", "easeInOutSine",
"easeInExpo", "easeOutExpo", "easeInOutExpo",
"easeInCirc", "easeOutCirc", "easeInOutCirc",
"easeInElastic", "easeOutElastic", "easeInOutElastic",
"easeInBack", "easeOutBack", "easeInOutBack",
"easeInBounce", "easeOutBounce", "easeInOutBounce"
};
static const int NUM_EASING_ENTRIES = sizeof(easing_table) / sizeof(easing_table[0]);
const char* PyEasing::easing_name(int value) {
if (value >= 0 && value < NUM_EASING_ENTRIES) {
return easing_table[value].name;
}
return "LINEAR";
}
PyObject* PyEasing::create_enum_class(PyObject* module) {
// Import IntEnum from enum module
PyObject* enum_module = PyImport_ImportModule("enum");
if (!enum_module) {
return NULL;
}
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
Py_DECREF(enum_module);
if (!int_enum) {
return NULL;
}
// Create dict of enum members
PyObject* members = PyDict_New();
if (!members) {
Py_DECREF(int_enum);
return NULL;
}
// Add all easing function members
for (int i = 0; i < NUM_EASING_ENTRIES; i++) {
PyObject* value = PyLong_FromLong(easing_table[i].value);
if (!value) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
if (PyDict_SetItemString(members, easing_table[i].name, value) < 0) {
Py_DECREF(value);
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
Py_DECREF(value);
}
// Call IntEnum("Easing", members) to create the enum class
PyObject* name = PyUnicode_FromString("Easing");
if (!name) {
Py_DECREF(members);
Py_DECREF(int_enum);
return NULL;
}
// IntEnum(name, members) using functional API
PyObject* args = PyTuple_Pack(2, name, members);
Py_DECREF(name);
Py_DECREF(members);
if (!args) {
Py_DECREF(int_enum);
return NULL;
}
PyObject* easing_class = PyObject_Call(int_enum, args, NULL);
Py_DECREF(args);
Py_DECREF(int_enum);
if (!easing_class) {
return NULL;
}
// Cache the reference for fast type checking
easing_enum_class = easing_class;
Py_INCREF(easing_enum_class);
// Add to module
if (PyModule_AddObject(module, "Easing", easing_class) < 0) {
Py_DECREF(easing_class);
easing_enum_class = nullptr;
return NULL;
}
return easing_class;
}
int PyEasing::from_arg(PyObject* arg, EasingFunction* out_func, bool* was_none) {
if (was_none) *was_none = false;
// Accept None -> default to linear
if (arg == Py_None) {
if (was_none) *was_none = true;
*out_func = EasingFunctions::linear;
return 1;
}
// Accept Easing enum member (check if it's an instance of our enum)
if (easing_enum_class && PyObject_IsInstance(arg, easing_enum_class)) {
// IntEnum members have a 'value' attribute
PyObject* value = PyObject_GetAttrString(arg, "value");
if (!value) {
return 0;
}
long val = PyLong_AsLong(value);
Py_DECREF(value);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= 0 && val < NUM_EASING_ENTRIES) {
*out_func = easing_table[val].func;
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid Easing value: %ld. Must be 0-%d.", val, NUM_EASING_ENTRIES - 1);
return 0;
}
// Accept int (for backwards compatibility and direct enum value access)
if (PyLong_Check(arg)) {
long val = PyLong_AsLong(arg);
if (val == -1 && PyErr_Occurred()) {
return 0;
}
if (val >= 0 && val < NUM_EASING_ENTRIES) {
*out_func = easing_table[val].func;
return 1;
}
PyErr_Format(PyExc_ValueError,
"Invalid easing value: %ld. Must be 0-%d or use mcrfpy.Easing enum.",
val, NUM_EASING_ENTRIES - 1);
return 0;
}
// Accept string (for backwards compatibility)
if (PyUnicode_Check(arg)) {
const char* name = PyUnicode_AsUTF8(arg);
if (!name) {
return 0;
}
// Check legacy string names first
for (int i = 0; i < NUM_EASING_ENTRIES; i++) {
if (strcmp(name, legacy_names[i]) == 0) {
*out_func = easing_table[i].func;
return 1;
}
}
// Also check enum-style names (EASE_IN_OUT, etc.)
for (int i = 0; i < NUM_EASING_ENTRIES; i++) {
if (strcmp(name, easing_table[i].name) == 0) {
*out_func = easing_table[i].func;
return 1;
}
}
// Build error message with available options
PyErr_Format(PyExc_ValueError,
"Unknown easing function: '%s'. Use mcrfpy.Easing enum (e.g., Easing.EASE_IN_OUT) "
"or legacy string names: 'linear', 'easeIn', 'easeOut', 'easeInOut', 'easeInQuad', etc.",
name);
return 0;
}
PyErr_SetString(PyExc_TypeError,
"Easing must be mcrfpy.Easing enum member, string, int, or None");
return 0;
}

29
src/PyEasing.h Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include "Common.h"
#include "Python.h"
#include "Animation.h"
// Module-level Easing enum class (created at runtime using Python's IntEnum)
// Stored as a module attribute: mcrfpy.Easing
class PyEasing {
public:
// Create the Easing enum class and add to module
// Returns the enum class (new reference), or NULL on error
static PyObject* create_enum_class(PyObject* module);
// Helper to extract easing function from Python arg
// Accepts Easing enum, string (for backwards compatibility), int, or None
// Returns 1 on success, 0 on error (with exception set)
// If arg is None, sets *out_func to linear and sets *was_none to true
static int from_arg(PyObject* arg, EasingFunction* out_func, bool* was_none = nullptr);
// Convert easing enum value to string name
static const char* easing_name(int value);
// Cached reference to the Easing enum class for fast type checking
static PyObject* easing_enum_class;
// Number of easing functions
static const int NUM_EASING_FUNCTIONS = 32;
};

View file

@ -10,13 +10,15 @@ PyObject* PyTimer::repr(PyObject* self) {
PyTimerObject* timer = (PyTimerObject*)self; PyTimerObject* timer = (PyTimerObject*)self;
std::ostringstream oss; std::ostringstream oss;
oss << "<Timer name='" << timer->name << "' "; oss << "<Timer name='" << timer->name << "' ";
if (timer->data) { if (timer->data) {
oss << "interval=" << timer->data->getInterval() << "ms "; oss << "interval=" << timer->data->getInterval() << "ms ";
if (timer->data->isOnce()) { if (timer->data->isOnce()) {
oss << "once=True "; oss << "once=True ";
} }
if (timer->data->isPaused()) { if (timer->data->isStopped()) {
oss << "stopped";
} else if (timer->data->isPaused()) {
oss << "paused"; oss << "paused";
// Get current time to show remaining // Get current time to show remaining
int current_time = 0; int current_time = 0;
@ -25,15 +27,15 @@ PyObject* PyTimer::repr(PyObject* self) {
} }
oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)"; oss << " (remaining=" << timer->data->getRemaining(current_time) << "ms)";
} else if (timer->data->isActive()) { } else if (timer->data->isActive()) {
oss << "active"; oss << "running";
} else { } else {
oss << "cancelled"; oss << "inactive";
} }
} else { } else {
oss << "uninitialized"; oss << "uninitialized";
} }
oss << ">"; oss << ">";
return PyUnicode_FromString(oss.str().c_str()); return PyUnicode_FromString(oss.str().c_str());
} }
@ -48,38 +50,39 @@ PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
} }
int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) { int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
static const char* kwlist[] = {"name", "callback", "interval", "once", NULL}; static const char* kwlist[] = {"name", "callback", "interval", "once", "start", NULL};
const char* name = nullptr; const char* name = nullptr;
PyObject* callback = nullptr; PyObject* callback = nullptr;
int interval = 0; int interval = 0;
int once = 0; // Use int for bool parameter int once = 0; // Use int for bool parameter
int start = 1; // Default: start=True
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|p", const_cast<char**>(kwlist),
&name, &callback, &interval, &once)) { if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi|pp", const_cast<char**>(kwlist),
&name, &callback, &interval, &once, &start)) {
return -1; return -1;
} }
if (!PyCallable_Check(callback)) { if (!PyCallable_Check(callback)) {
PyErr_SetString(PyExc_TypeError, "callback must be callable"); PyErr_SetString(PyExc_TypeError, "callback must be callable");
return -1; return -1;
} }
if (interval <= 0) { if (interval <= 0) {
PyErr_SetString(PyExc_ValueError, "interval must be positive"); PyErr_SetString(PyExc_ValueError, "interval must be positive");
return -1; return -1;
} }
self->name = name; self->name = name;
// Get current time from game engine // Get current time from game engine
int current_time = 0; int current_time = 0;
if (Resources::game) { if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
} }
// Create the timer // Create the timer with start parameter
self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once); self->data = std::make_shared<Timer>(callback, interval, current_time, (bool)once, (bool)start);
// Register in Python object cache // Register in Python object cache
if (self->data->serial_number == 0) { if (self->data->serial_number == 0) {
self->data->serial_number = PythonObjectCache::getInstance().assignSerial(); self->data->serial_number = PythonObjectCache::getInstance().assignSerial();
@ -89,12 +92,17 @@ int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) {
Py_DECREF(weakref); // Cache owns the reference now Py_DECREF(weakref); // Cache owns the reference now
} }
} }
// Register with game engine // Register with game engine only if starting
if (Resources::game) { if (Resources::game && start) {
// If a timer with this name already exists, stop it first
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second != self->data) {
it->second->stop();
}
Resources::game->timers[self->name] = self->data; Resources::game->timers[self->name] = self->data;
} }
return 0; return 0;
} }
@ -103,7 +111,7 @@ void PyTimer::dealloc(PyTimerObject* self) {
if (self->weakreflist != nullptr) { if (self->weakreflist != nullptr) {
PyObject_ClearWeakRefs((PyObject*)self); PyObject_ClearWeakRefs((PyObject*)self);
} }
// Remove from game engine if still registered // Remove from game engine if still registered
if (Resources::game && !self->name.empty()) { if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name); auto it = Resources::game->timers.find(self->name);
@ -111,28 +119,71 @@ void PyTimer::dealloc(PyTimerObject* self) {
Resources::game->timers.erase(it); Resources::game->timers.erase(it);
} }
} }
// Explicitly destroy std::string // Explicitly destroy std::string
self->name.~basic_string(); self->name.~basic_string();
// Clear shared_ptr // Clear shared_ptr - this is the only place that truly destroys the Timer
self->data.reset(); self->data.reset();
Py_TYPE(self)->tp_free((PyObject*)self); Py_TYPE(self)->tp_free((PyObject*)self);
} }
// Timer control methods // Timer control methods
PyObject* PyTimer::start(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
// If another timer has this name, stop it first
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second != self->data) {
it->second->stop();
}
// Add to engine map
Resources::game->timers[self->name] = self->data;
}
self->data->start(current_time);
Py_RETURN_NONE;
}
PyObject* PyTimer::stop(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
// Remove from game engine map (but preserve the Timer data!)
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
self->data->stop();
// NOTE: We do NOT reset self->data here - the timer can be restarted
Py_RETURN_NONE;
}
PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) { PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
if (!self->data) { if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr; return nullptr;
} }
int current_time = 0; int current_time = 0;
if (Resources::game) { if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
} }
self->data->pause(current_time); self->data->pause(current_time);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
@ -142,32 +193,13 @@ PyObject* PyTimer::resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr; return nullptr;
} }
int current_time = 0; int current_time = 0;
if (Resources::game) { if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
} }
self->data->resume(current_time);
Py_RETURN_NONE;
}
PyObject* PyTimer::cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) { self->data->resume(current_time);
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr;
}
// Remove from game engine
if (Resources::game && !self->name.empty()) {
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second == self->data) {
Resources::game->timers.erase(it);
}
}
self->data->cancel();
self->data.reset();
Py_RETURN_NONE; Py_RETURN_NONE;
} }
@ -176,12 +208,23 @@ PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return nullptr; return nullptr;
} }
int current_time = 0; int current_time = 0;
if (Resources::game) { if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
// Ensure timer is in engine map
auto it = Resources::game->timers.find(self->name);
if (it == Resources::game->timers.end()) {
// Timer was stopped, re-add it
Resources::game->timers[self->name] = self->data;
} else if (it->second != self->data) {
// Another timer has this name, stop it and replace
it->second->stop();
Resources::game->timers[self->name] = self->data;
}
} }
self->data->restart(current_time); self->data->restart(current_time);
Py_RETURN_NONE; Py_RETURN_NONE;
} }
@ -240,14 +283,62 @@ PyObject* PyTimer::get_paused(PyTimerObject* self, void* closure) {
return PyBool_FromLong(self->data->isPaused()); return PyBool_FromLong(self->data->isPaused());
} }
PyObject* PyTimer::get_stopped(PyTimerObject* self, void* closure) {
if (!self->data) {
return Py_True; // Uninitialized is effectively stopped
}
return PyBool_FromLong(self->data->isStopped());
}
PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) { PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) {
if (!self->data) { if (!self->data) {
return Py_False; return Py_False;
} }
return PyBool_FromLong(self->data->isActive()); return PyBool_FromLong(self->data->isActive());
} }
int PyTimer::set_active(PyTimerObject* self, PyObject* value, void* closure) {
if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
return -1;
}
bool want_active = PyObject_IsTrue(value);
int current_time = 0;
if (Resources::game) {
current_time = Resources::game->runtime.getElapsedTime().asMilliseconds();
}
if (want_active) {
if (self->data->isStopped()) {
// Reactivate a stopped timer
if (Resources::game) {
// Handle name collision
auto it = Resources::game->timers.find(self->name);
if (it != Resources::game->timers.end() && it->second != self->data) {
it->second->stop();
}
Resources::game->timers[self->name] = self->data;
}
self->data->start(current_time);
} else if (self->data->isPaused()) {
// Resume from pause
self->data->resume(current_time);
}
// If already running, do nothing
} else {
// Setting active=False means pause
if (!self->data->isPaused() && !self->data->isStopped()) {
self->data->pause(current_time);
}
}
return 0;
}
PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) { PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) {
if (!self->data) { if (!self->data) {
PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); PyErr_SetString(PyExc_RuntimeError, "Timer not initialized");
@ -312,19 +403,35 @@ PyGetSetDef PyTimer::getsetters[] = {
{"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval, {"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval,
MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL}, MCRF_PROPERTY(interval, "Timer interval in milliseconds (int). Must be positive. Can be changed while timer is running."), NULL},
{"remaining", (getter)PyTimer::get_remaining, NULL, {"remaining", (getter)PyTimer::get_remaining, NULL,
MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Preserved when timer is paused."), NULL}, MCRF_PROPERTY(remaining, "Time remaining until next trigger in milliseconds (int, read-only). Full interval when stopped."), NULL},
{"paused", (getter)PyTimer::get_paused, NULL, {"paused", (getter)PyTimer::get_paused, NULL,
MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL}, MCRF_PROPERTY(paused, "Whether the timer is paused (bool, read-only). Paused timers preserve their remaining time."), NULL},
{"active", (getter)PyTimer::get_active, NULL, {"stopped", (getter)PyTimer::get_stopped, NULL,
MCRF_PROPERTY(active, "Whether the timer is active and not paused (bool, read-only). False if cancelled or paused."), NULL}, MCRF_PROPERTY(stopped, "Whether the timer is stopped (bool, read-only). Stopped timers are not in the engine tick loop but preserve their callback."), NULL},
{"active", (getter)PyTimer::get_active, (setter)PyTimer::set_active,
MCRF_PROPERTY(active, "Running state (bool, read-write). True if running (not paused, not stopped). Set True to start/resume, False to pause."), NULL},
{"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback, {"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback,
MCRF_PROPERTY(callback, "The callback function to be called when timer fires (callable). Can be changed while timer is running."), NULL}, MCRF_PROPERTY(callback, "The callback function (callable). Preserved when stopped, allowing timer restart."), NULL},
{"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once, {"once", (getter)PyTimer::get_once, (setter)PyTimer::set_once,
MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). If False, timer repeats indefinitely."), NULL}, MCRF_PROPERTY(once, "Whether the timer stops after firing once (bool). One-shot timers can be restarted."), NULL},
{NULL} {NULL}
}; };
PyMethodDef PyTimer::methods[] = { PyMethodDef PyTimer::methods[] = {
{"start", (PyCFunction)PyTimer::start, METH_NOARGS,
MCRF_METHOD(Timer, start,
MCRF_SIG("()", "None"),
MCRF_DESC("Start the timer, adding it to the engine tick loop."),
MCRF_RETURNS("None")
MCRF_NOTE("Resets progress and begins counting toward the next fire. If another timer has this name, it will be stopped.")
)},
{"stop", (PyCFunction)PyTimer::stop, METH_NOARGS,
MCRF_METHOD(Timer, stop,
MCRF_SIG("()", "None"),
MCRF_DESC("Stop the timer and remove it from the engine tick loop."),
MCRF_RETURNS("None")
MCRF_NOTE("The callback is preserved, so the timer can be restarted with start() or restart().")
)},
{"pause", (PyCFunction)PyTimer::pause, METH_NOARGS, {"pause", (PyCFunction)PyTimer::pause, METH_NOARGS,
MCRF_METHOD(Timer, pause, MCRF_METHOD(Timer, pause,
MCRF_SIG("()", "None"), MCRF_SIG("()", "None"),
@ -339,19 +446,12 @@ PyMethodDef PyTimer::methods[] = {
MCRF_RETURNS("None") MCRF_RETURNS("None")
MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.") MCRF_NOTE("Has no effect if the timer is not paused. Timer will fire after the remaining time elapses.")
)}, )},
{"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS,
MCRF_METHOD(Timer, cancel,
MCRF_SIG("()", "None"),
MCRF_DESC("Cancel the timer and remove it from the timer system."),
MCRF_RETURNS("None")
MCRF_NOTE("The timer will no longer fire and cannot be restarted. The callback will not be called again.")
)},
{"restart", (PyCFunction)PyTimer::restart, METH_NOARGS, {"restart", (PyCFunction)PyTimer::restart, METH_NOARGS,
MCRF_METHOD(Timer, restart, MCRF_METHOD(Timer, restart,
MCRF_SIG("()", "None"), MCRF_SIG("()", "None"),
MCRF_DESC("Restart the timer from the beginning."), MCRF_DESC("Restart the timer from the beginning and ensure it's running."),
MCRF_RETURNS("None") MCRF_RETURNS("None")
MCRF_NOTE("Resets the timer to fire after a full interval from now, regardless of remaining time.") MCRF_NOTE("Resets progress and adds timer to engine if stopped. Equivalent to stop() followed by start().")
)}, )},
{NULL} {NULL}
}; };

View file

@ -23,9 +23,10 @@ public:
static void dealloc(PyTimerObject* self); static void dealloc(PyTimerObject* self);
// Timer control methods // Timer control methods
static PyObject* start(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* stop(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored));
// Timer property getters // Timer property getters
@ -34,7 +35,9 @@ public:
static int set_interval(PyTimerObject* self, PyObject* value, void* closure); static int set_interval(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_remaining(PyTimerObject* self, void* closure); static PyObject* get_remaining(PyTimerObject* self, void* closure);
static PyObject* get_paused(PyTimerObject* self, void* closure); static PyObject* get_paused(PyTimerObject* self, void* closure);
static PyObject* get_stopped(PyTimerObject* self, void* closure);
static PyObject* get_active(PyTimerObject* self, void* closure); static PyObject* get_active(PyTimerObject* self, void* closure);
static int set_active(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_callback(PyTimerObject* self, void* closure); static PyObject* get_callback(PyTimerObject* self, void* closure);
static int set_callback(PyTimerObject* self, PyObject* value, void* closure); static int set_callback(PyTimerObject* self, PyObject* value, void* closure);
static PyObject* get_once(PyTimerObject* self, void* closure); static PyObject* get_once(PyTimerObject* self, void* closure);
@ -53,35 +56,39 @@ namespace mcrfpydef {
.tp_dealloc = (destructor)PyTimer::dealloc, .tp_dealloc = (destructor)PyTimer::dealloc,
.tp_repr = PyTimer::repr, .tp_repr = PyTimer::repr,
.tp_flags = Py_TPFLAGS_DEFAULT, .tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False)\n\n" .tp_doc = PyDoc_STR("Timer(name, callback, interval, once=False, start=True)\n\n"
"Create a timer that calls a function at regular intervals.\n\n" "Create a timer that calls a function at regular intervals.\n\n"
"Args:\n" "Args:\n"
" name (str): Unique identifier for the timer\n" " name (str): Unique identifier for the timer\n"
" callback (callable): Function to call - receives (timer, runtime) args\n" " callback (callable): Function to call - receives (timer, runtime) args\n"
" interval (int): Time between calls in milliseconds\n" " interval (int): Time between calls in milliseconds\n"
" once (bool): If True, timer stops after first call. Default: False\n\n" " once (bool): If True, timer stops after first call. Default: False\n"
" start (bool): If True, timer starts immediately. Default: True\n\n"
"Attributes:\n" "Attributes:\n"
" interval (int): Time between calls in milliseconds\n" " interval (int): Time between calls in milliseconds\n"
" remaining (int): Time until next call in milliseconds (read-only)\n" " remaining (int): Time until next call in milliseconds (read-only)\n"
" paused (bool): Whether timer is paused (read-only)\n" " paused (bool): Whether timer is paused (read-only)\n"
" active (bool): Whether timer is active and not paused (read-only)\n" " stopped (bool): Whether timer is stopped (read-only)\n"
" callback (callable): The callback function\n" " active (bool): Running state (read-write). Set True to start, False to pause\n"
" callback (callable): The callback function (preserved when stopped)\n"
" once (bool): Whether timer stops after firing once\n\n" " once (bool): Whether timer stops after firing once\n\n"
"Methods:\n" "Methods:\n"
" start(): Start the timer, adding to engine tick loop\n"
" stop(): Stop the timer (removes from engine, preserves callback)\n"
" pause(): Pause the timer, preserving time remaining\n" " pause(): Pause the timer, preserving time remaining\n"
" resume(): Resume a paused timer\n" " resume(): Resume a paused timer\n"
" cancel(): Stop and remove the timer\n" " restart(): Reset timer and ensure it's running\n\n"
" restart(): Reset timer to start from beginning\n\n"
"Example:\n" "Example:\n"
" def on_timer(timer, runtime):\n" " def on_timer(timer, runtime):\n"
" print(f'Timer {timer} fired at {runtime}ms')\n" " print(f'Timer {timer} fired at {runtime}ms')\n"
" if runtime > 5000:\n" " if runtime > 5000:\n"
" timer.cancel()\n" " timer.stop() # Stop but can restart later\n"
" \n" " \n"
" timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n" " timer = mcrfpy.Timer('my_timer', on_timer, 1000)\n"
" timer.pause() # Pause timer\n" " timer.pause() # Pause timer\n"
" timer.resume() # Resume timer\n" " timer.resume() # Resume timer\n"
" timer.once = True # Make it one-shot"), " timer.stop() # Stop completely\n"
" timer.start() # Restart from beginning"),
.tp_methods = PyTimer::methods, .tp_methods = PyTimer::methods,
.tp_getset = PyTimer::getsetters, .tp_getset = PyTimer::getsetters,
.tp_init = (initproc)PyTimer::init, .tp_init = (initproc)PyTimer::init,

View file

@ -4,14 +4,14 @@
#include "McRFPy_API.h" #include "McRFPy_API.h"
#include "GameEngine.h" #include "GameEngine.h"
Timer::Timer(PyObject* _target, int _interval, int now, bool _once) Timer::Timer(PyObject* _target, int _interval, int now, bool _once, bool _start)
: callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now), : callback(std::make_shared<PyCallable>(_target)), interval(_interval), last_ran(now),
paused(false), pause_start_time(0), total_paused_time(0), once(_once) paused(false), pause_start_time(0), total_paused_time(0), once(_once), stopped(!_start)
{} {}
Timer::Timer() Timer::Timer()
: callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0), : callback(std::make_shared<PyCallable>(Py_None)), interval(0), last_ran(0),
paused(false), pause_start_time(0), total_paused_time(0), once(false) paused(false), pause_start_time(0), total_paused_time(0), once(false), stopped(true)
{} {}
Timer::~Timer() { Timer::~Timer() {
@ -22,24 +22,24 @@ Timer::~Timer() {
bool Timer::hasElapsed(int now) const bool Timer::hasElapsed(int now) const
{ {
if (paused) return false; if (paused || stopped) return false;
return now >= last_ran + interval; return now >= last_ran + interval;
} }
bool Timer::test(int now) bool Timer::test(int now)
{ {
if (!callback || callback->isNone()) return false; if (!callback || callback->isNone() || stopped) return false;
if (hasElapsed(now)) if (hasElapsed(now))
{ {
last_ran = now; last_ran = now;
// Get the PyTimer wrapper from cache to pass to callback // Get the PyTimer wrapper from cache to pass to callback
PyObject* timer_obj = nullptr; PyObject* timer_obj = nullptr;
if (serial_number != 0) { if (serial_number != 0) {
timer_obj = PythonObjectCache::getInstance().lookup(serial_number); timer_obj = PythonObjectCache::getInstance().lookup(serial_number);
} }
// Build args: (timer, runtime) or just (runtime) if no wrapper found // Build args: (timer, runtime) or just (runtime) if no wrapper found
PyObject* args; PyObject* args;
if (timer_obj) { if (timer_obj) {
@ -48,10 +48,10 @@ bool Timer::test(int now)
// Fallback to old behavior if no wrapper found // Fallback to old behavior if no wrapper found
args = Py_BuildValue("(i)", now); args = Py_BuildValue("(i)", now);
} }
PyObject* retval = callback->call(args, NULL); PyObject* retval = callback->call(args, NULL);
Py_DECREF(args); Py_DECREF(args);
if (!retval) if (!retval)
{ {
std::cerr << "Timer callback raised an exception:" << std::endl; std::cerr << "Timer callback raised an exception:" << std::endl;
@ -63,16 +63,16 @@ bool Timer::test(int now)
McRFPy_API::signalPythonException(); McRFPy_API::signalPythonException();
} }
} else if (retval != Py_None) } else if (retval != Py_None)
{ {
std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; std::cout << "Timer returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
Py_DECREF(retval); Py_DECREF(retval);
} }
// Handle one-shot timers // Handle one-shot timers: stop but preserve callback for potential restart
if (once) { if (once) {
cancel(); stopped = true; // Will be removed from map by testTimers(), but callback preserved
} }
return true; return true;
} }
return false; return false;
@ -101,23 +101,41 @@ void Timer::restart(int current_time)
{ {
last_ran = current_time; last_ran = current_time;
paused = false; paused = false;
stopped = false; // Ensure timer is running
pause_start_time = 0; pause_start_time = 0;
total_paused_time = 0; total_paused_time = 0;
} }
void Timer::cancel() void Timer::start(int current_time)
{ {
// Cancel by setting callback to None // Start/resume the timer - clear stopped flag, reset progress
callback = std::make_shared<PyCallable>(Py_None); stopped = false;
paused = false;
last_ran = current_time;
pause_start_time = 0;
total_paused_time = 0;
}
void Timer::stop()
{
// Stop the timer - it will be removed from engine map, but callback is preserved
stopped = true;
paused = false;
pause_start_time = 0;
total_paused_time = 0;
} }
bool Timer::isActive() const bool Timer::isActive() const
{ {
return callback && !callback->isNone() && !paused; return callback && !callback->isNone() && !paused && !stopped;
} }
int Timer::getRemaining(int current_time) const int Timer::getRemaining(int current_time) const
{ {
if (stopped) {
// When stopped, progress is reset - full interval remaining
return interval;
}
if (paused) { if (paused) {
// When paused, calculate time remaining from when it was paused // When paused, calculate time remaining from when it was paused
int elapsed_when_paused = pause_start_time - last_ran; int elapsed_when_paused = pause_start_time - last_ran;
@ -129,6 +147,10 @@ int Timer::getRemaining(int current_time) const
int Timer::getElapsed(int current_time) const int Timer::getElapsed(int current_time) const
{ {
if (stopped) {
// When stopped, progress is reset
return 0;
}
if (paused) { if (paused) {
return pause_start_time - last_ran; return pause_start_time - last_ran;
} }

View file

@ -17,37 +17,42 @@ private:
bool paused; bool paused;
int pause_start_time; int pause_start_time;
int total_paused_time; int total_paused_time;
// One-shot timer support // One-shot timer support
bool once; bool once;
// Stopped state: timer is not in engine map, but callback is preserved
bool stopped;
public: public:
uint64_t serial_number = 0; // For Python object cache uint64_t serial_number = 0; // For Python object cache
Timer(); // for map to build Timer(); // for map to build
Timer(PyObject* target, int interval, int now, bool once = false); Timer(PyObject* target, int interval, int now, bool once = false, bool start = true);
~Timer(); ~Timer();
// Core timer functionality // Core timer functionality
bool test(int now); bool test(int now);
bool hasElapsed(int now) const; bool hasElapsed(int now) const;
// Timer control methods // Timer control methods
void pause(int current_time); void pause(int current_time);
void resume(int current_time); void resume(int current_time);
void restart(int current_time); void restart(int current_time);
void cancel(); void start(int current_time); // Clear stopped flag, reset progress
void stop(); // Set stopped flag, preserve callback
// Timer state queries // Timer state queries
bool isPaused() const { return paused; } bool isPaused() const { return paused; }
bool isActive() const; bool isStopped() const { return stopped; }
bool isActive() const; // Running: not paused AND not stopped AND has callback
int getInterval() const { return interval; } int getInterval() const { return interval; }
void setInterval(int new_interval) { interval = new_interval; } void setInterval(int new_interval) { interval = new_interval; }
int getRemaining(int current_time) const; int getRemaining(int current_time) const;
int getElapsed(int current_time) const; int getElapsed(int current_time) const;
bool isOnce() const { return once; } bool isOnce() const { return once; }
void setOnce(bool value) { once = value; } void setOnce(bool value) { once = value; }
// Callback management // Callback management
PyObject* getCallback(); PyObject* getCallback();
void setCallback(PyObject* new_callback); void setCallback(PyObject* new_callback);

View file

@ -268,8 +268,12 @@ PyGetSetDef UICaption::getsetters[] = {
//{"w", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "width of the rectangle", (void*)2}, //{"w", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "width of the rectangle", (void*)2},
//{"h", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "height of the rectangle", (void*)3}, //{"h", (getter)PyUIFrame_get_float_member, (setter)PyUIFrame_set_float_member, "height of the rectangle", (void*)3},
{"outline", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Thickness of the border", (void*)4}, {"outline", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Thickness of the border", (void*)4},
{"fill_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Fill color of the text", (void*)0}, {"fill_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member,
{"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1}, "Fill color of the text. Returns a copy; modifying components requires reassignment. "
"For animation, use 'fill_color.r', 'fill_color.g', etc.", (void*)0},
{"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member,
"Outline color of the text. Returns a copy; modifying components requires reassignment. "
"For animation, use 'outline_color.r', 'outline_color.g', etc.", (void*)1},
//{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL}, //{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL},
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
{"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5}, {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5},

View file

@ -434,8 +434,12 @@ PyGetSetDef UIFrame::getsetters[] = {
{"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "width of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 2)}, {"w", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "width of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 2)},
{"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "height of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 3)}, {"h", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "height of the rectangle", (void*)((intptr_t)PyObjectsEnum::UIFRAME << 8 | 3)},
{"outline", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Thickness of the border", (void*)4}, {"outline", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Thickness of the border", (void*)4},
{"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Fill color of the rectangle", (void*)0}, {"fill_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member,
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1}, "Fill color of the rectangle. Returns a copy; modifying components requires reassignment. "
"For animation, use 'fill_color.r', 'fill_color.g', etc.", (void*)0},
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member,
"Outline color of the rectangle. Returns a copy; modifying components requires reassignment. "
"For animation, use 'outline_color.r', 'outline_color.g', etc.", (void*)1},
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
{"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, {"on_click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click,
MCRF_PROPERTY(on_click, MCRF_PROPERTY(on_click,

View file

@ -2059,7 +2059,9 @@ PyGetSetDef UIGrid::getsetters[] = {
), (void*)PyObjectsEnum::UIGRID}, ), (void*)PyObjectsEnum::UIGRID},
{"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5
{"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color, "Background fill color of the grid", NULL}, {"fill_color", (getter)UIGrid::get_fill_color, (setter)UIGrid::set_fill_color,
"Background fill color of the grid. Returns a copy; modifying components requires reassignment. "
"For animation, use 'fill_color.r', 'fill_color.g', etc.", NULL},
{"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective, {"perspective", (getter)UIGrid::get_perspective, (setter)UIGrid::set_perspective,
"Entity whose perspective to use for FOV rendering (None for omniscient view). " "Entity whose perspective to use for FOV rendering (None for omniscient view). "
"Setting an entity automatically enables perspective mode.", NULL}, "Setting an entity automatically enables perspective mode.", NULL},

View file

@ -12,26 +12,44 @@ Transition = Union[str, None]
# Classes # Classes
class Color: class Color:
"""SFML Color Object for RGBA colors.""" """RGBA color representation.
Note:
When accessing colors from UI elements (e.g., frame.fill_color),
you receive a COPY of the color. Modifying it doesn't affect the
original. To change a component:
# This does NOT work:
frame.fill_color.r = 255 # Modifies a temporary copy
# Do this instead:
c = frame.fill_color
c.r = 255
frame.fill_color = c
# Or use Animation for sub-properties:
anim = mcrfpy.Animation('fill_color.r', 255, 0.5, 'linear')
anim.start(frame)
"""
r: int r: int
g: int g: int
b: int b: int
a: int a: int
@overload @overload
def __init__(self) -> None: ... def __init__(self) -> None: ...
@overload @overload
def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ... def __init__(self, r: int, g: int, b: int, a: int = 255) -> None: ...
def from_hex(self, hex_string: str) -> 'Color': def from_hex(self, hex_string: str) -> 'Color':
"""Create color from hex string (e.g., '#FF0000' or 'FF0000').""" """Create color from hex string (e.g., '#FF0000' or 'FF0000')."""
... ...
def to_hex(self) -> str: def to_hex(self) -> str:
"""Convert color to hex string format.""" """Convert color to hex string format."""
... ...
def lerp(self, other: 'Color', t: float) -> 'Color': def lerp(self, other: 'Color', t: float) -> 'Color':
"""Linear interpolation between two colors.""" """Linear interpolation between two colors."""
... ...
@ -534,31 +552,118 @@ class Window:
... ...
class Animation: class Animation:
"""Animation object for animating UI properties.""" """Animation for interpolating UI properties over time.
target: Any Create an animation targeting a specific property, then call start() on a
property: str UI element to begin the animation. The AnimationManager handles updates
duration: float automatically.
easing: str
loop: bool Example:
on_complete: Optional[Callable] # Move a frame to x=500 over 2 seconds with easing
anim = mcrfpy.Animation('x', 500.0, 2.0, 'easeInOut')
def __init__(self, target: Any, property: str, start_value: Any, end_value: Any, anim.start(my_frame)
duration: float, easing: str = 'linear', loop: bool = False,
on_complete: Optional[Callable] = None) -> None: ... # Animate color with completion callback
def on_done(anim, target):
def start(self) -> None: print('Fade complete!')
"""Start the animation.""" fade = mcrfpy.Animation('fill_color.a', 0, 1.0, callback=on_done)
fade.start(my_sprite)
"""
@property
def property(self) -> str:
"""Target property name being animated (read-only)."""
... ...
@property
def duration(self) -> float:
"""Animation duration in seconds (read-only)."""
...
@property
def elapsed(self) -> float:
"""Time elapsed since animation started in seconds (read-only)."""
...
@property
def is_complete(self) -> bool:
"""Whether the animation has finished (read-only)."""
...
@property
def is_delta(self) -> bool:
"""Whether animation uses delta/additive mode (read-only)."""
...
def __init__(self,
property: str,
target: Union[float, int, Tuple[float, float], Tuple[int, int, int], Tuple[int, int, int, int], List[int], str],
duration: float,
easing: str = 'linear',
delta: bool = False,
callback: Optional[Callable[['Animation', Any], None]] = None) -> None:
"""Create an animation for a UI property.
Args:
property: Property name to animate. Common properties:
- Position/Size: 'x', 'y', 'w', 'h', 'pos', 'size'
- Appearance: 'fill_color', 'outline_color', 'opacity'
- Sprite: 'sprite_index', 'scale'
- Grid: 'center', 'zoom'
- Sub-properties: 'fill_color.r', 'fill_color.g', etc.
target: Target value. Type depends on property:
- float: For x, y, w, h, scale, opacity, zoom
- int: For sprite_index
- (r, g, b) or (r, g, b, a): For colors
- (x, y): For pos, size, center
- [int, ...]: For sprite animation sequences
- str: For text animation
duration: Animation duration in seconds.
easing: Easing function. Options: 'linear', 'easeIn', 'easeOut',
'easeInOut', 'easeInQuad', 'easeOutQuad', 'easeInOutQuad',
'easeInCubic', 'easeOutCubic', 'easeInOutCubic',
'easeInElastic', 'easeOutElastic', 'easeInOutElastic',
'easeInBounce', 'easeOutBounce', 'easeInOutBounce', and more.
delta: If True, target value is added to start value.
callback: Function(animation, target) called on completion.
"""
...
def start(self, target: UIElement, conflict_mode: str = 'replace') -> None:
"""Start the animation on a UI element.
Args:
target: The UI element to animate (Frame, Caption, Sprite, Grid, or Entity)
conflict_mode: How to handle if property is already animating:
- 'replace': Stop existing animation, start new one (default)
- 'queue': Wait for existing animation to complete
- 'error': Raise RuntimeError if property is busy
"""
...
def update(self, dt: float) -> bool: def update(self, dt: float) -> bool:
"""Update animation, returns True if still running.""" """Update animation by time delta. Returns True if still running.
Note: Normally called automatically by AnimationManager.
"""
... ...
def get_current_value(self) -> Any: def get_current_value(self) -> Any:
"""Get the current interpolated value.""" """Get the current interpolated value."""
... ...
def complete(self) -> None:
"""Complete the animation immediately, jumping to final value."""
...
def hasValidTarget(self) -> bool:
"""Check if the animation target still exists."""
...
def __repr__(self) -> str:
"""Return string representation showing property, duration, and status."""
...
# Module-level attributes # Module-level attributes
__version__: str __version__: str

View file

@ -97,7 +97,7 @@ def handle_key(key, state):
benchmark.on_key = handle_key benchmark.on_key = handle_key
# Update entity positions # Update entity positions
def update_entities(ms): def update_entities(timer, ms):
dt = ms / 1000.0 # Convert to seconds dt = ms / 1000.0 # Convert to seconds
for entity in entities: for entity in entities:
@ -119,13 +119,13 @@ def update_entities(ms):
entity.y = new_y entity.y = new_y
# Run movement update every frame (16ms) # Run movement update every frame (16ms)
mcrfpy.setTimer("movement", update_entities, 16) movement_timer = mcrfpy.Timer("movement", update_entities, 16)
# Benchmark statistics # Benchmark statistics
frame_count = 0 frame_count = 0
start_time = None start_time = None
def benchmark_timer(ms): def benchmark_callback(timer, ms):
global frame_count, start_time global frame_count, start_time
if start_time is None: if start_time is None:
@ -152,4 +152,4 @@ def benchmark_timer(ms):
print("=" * 60) print("=" * 60)
# Don't exit - let user review # Don't exit - let user review
mcrfpy.setTimer("benchmark", benchmark_timer, 100) benchmark_timer = mcrfpy.Timer("benchmark", benchmark_callback, 100)

View file

@ -31,7 +31,7 @@ frame_count = 0
metrics_samples = [] metrics_samples = []
def collect_metrics(runtime): def collect_metrics(timer, runtime):
"""Timer callback to collect metrics each frame.""" """Timer callback to collect metrics each frame."""
global frame_count, metrics_samples global frame_count, metrics_samples
@ -65,9 +65,9 @@ def collect_metrics(runtime):
def finish_scenario(): def finish_scenario():
"""Calculate statistics and store results for current scenario.""" """Calculate statistics and store results for current scenario."""
global results, current_scenario, metrics_samples global results, current_scenario, metrics_samples, benchmark_timer
mcrfpy.delTimer("benchmark_collect") benchmark_timer.stop()
if not metrics_samples: if not metrics_samples:
print(f" WARNING: No samples collected for {current_scenario}") print(f" WARNING: No samples collected for {current_scenario}")
@ -149,7 +149,8 @@ def run_next_scenario():
scenarios[next_idx][1]() scenarios[next_idx][1]()
# Start collection timer (runs every frame) # Start collection timer (runs every frame)
mcrfpy.setTimer("benchmark_collect", collect_metrics, 1) global benchmark_timer
benchmark_timer = mcrfpy.Timer("benchmark_collect", collect_metrics, 1)
# ============================================================================ # ============================================================================

View file

@ -427,7 +427,7 @@ def print_analysis():
print(f" Note: This overhead is acceptable given query speedups") print(f" Note: This overhead is acceptable given query speedups")
def run_benchmarks(runtime=None): def run_benchmarks(timer=None, runtime=None):
"""Main benchmark runner.""" """Main benchmark runner."""
global results global results
@ -458,4 +458,4 @@ if __name__ == "__main__":
if "--headless" in sys.argv or True: # Always run immediately for benchmarks if "--headless" in sys.argv or True: # Always run immediately for benchmarks
run_benchmarks() run_benchmarks()
else: else:
mcrfpy.setTimer("run_bench", run_benchmarks, 100) bench_timer = mcrfpy.Timer("run_bench", run_benchmarks, 100, once=True)

View file

@ -34,7 +34,7 @@ frame_count = 0
test_results = {} # Store filenames for each test test_results = {} # Store filenames for each test
def run_test_phase(runtime): def run_test_phase(timer, runtime):
"""Run through warmup and measurement phases.""" """Run through warmup and measurement phases."""
global frame_count global frame_count
@ -51,7 +51,7 @@ def run_test_phase(runtime):
test_results[current_test] = filename test_results[current_test] = filename
print(f" {current_test}: saved to {filename}") print(f" {current_test}: saved to {filename}")
mcrfpy.delTimer("test_phase") timer.stop()
run_next_test() run_next_test()
@ -90,7 +90,8 @@ def run_next_test():
print(f"\n[{next_idx + 1}/{len(tests)}] Running: {current_test}") print(f"\n[{next_idx + 1}/{len(tests)}] Running: {current_test}")
tests[next_idx][1]() tests[next_idx][1]()
mcrfpy.setTimer("test_phase", run_test_phase, 1) global test_phase_timer
test_phase_timer = mcrfpy.Timer("test_phase", run_test_phase, 1)
# ============================================================================ # ============================================================================
@ -130,14 +131,15 @@ def setup_base_layer_modified():
# Timer to modify one cell per frame (triggers dirty flag each frame) # Timer to modify one cell per frame (triggers dirty flag each frame)
mod_counter = [0] mod_counter = [0]
def modify_cell(runtime): def modify_cell(timer, runtime):
x = mod_counter[0] % GRID_SIZE x = mod_counter[0] % GRID_SIZE
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
layer.set(x, y, mcrfpy.Color(255, 0, 0, 255)) layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
mod_counter[0] += 1 mod_counter[0] += 1
test_base_mod.activate() test_base_mod.activate()
mcrfpy.setTimer("modify", modify_cell, 1) global modify_timer
modify_timer = mcrfpy.Timer("modify", modify_cell, 1)
def setup_color_layer_static(): def setup_color_layer_static():
@ -170,14 +172,15 @@ def setup_color_layer_modified():
# Timer to modify one cell per frame - triggers re-render # Timer to modify one cell per frame - triggers re-render
mod_counter = [0] mod_counter = [0]
def modify_cell(runtime): def modify_cell(timer, runtime):
x = mod_counter[0] % GRID_SIZE x = mod_counter[0] % GRID_SIZE
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
layer.set(x, y, mcrfpy.Color(255, 0, 0, 255)) layer.set(x, y, mcrfpy.Color(255, 0, 0, 255))
mod_counter[0] += 1 mod_counter[0] += 1
test_color_mod.activate() test_color_mod.activate()
mcrfpy.setTimer("modify", modify_cell, 1) global modify_timer
modify_timer = mcrfpy.Timer("modify", modify_cell, 1)
def setup_tile_layer_static(): def setup_tile_layer_static():
@ -222,7 +225,7 @@ def setup_tile_layer_modified():
# Timer to modify one cell per frame # Timer to modify one cell per frame
mod_counter = [0] mod_counter = [0]
def modify_cell(runtime): def modify_cell(timer, runtime):
if layer: if layer:
x = mod_counter[0] % GRID_SIZE x = mod_counter[0] % GRID_SIZE
y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE y = (mod_counter[0] // GRID_SIZE) % GRID_SIZE
@ -230,7 +233,8 @@ def setup_tile_layer_modified():
mod_counter[0] += 1 mod_counter[0] += 1
test_tile_mod.activate() test_tile_mod.activate()
mcrfpy.setTimer("modify", modify_cell, 1) global modify_timer
modify_timer = mcrfpy.Timer("modify", modify_cell, 1)
def setup_multi_layer_static(): def setup_multi_layer_static():

View file

@ -31,7 +31,7 @@ class StressTestRunner:
def add_test(self, name, setup_fn, description=""): def add_test(self, name, setup_fn, description=""):
self.tests.append({'name': name, 'setup': setup_fn, 'description': description}) self.tests.append({'name': name, 'setup': setup_fn, 'description': description})
def tick(self, runtime): def tick(self, timer, runtime):
"""Single timer callback that manages all test flow""" """Single timer callback that manages all test flow"""
self.frames_counted += 1 self.frames_counted += 1
@ -103,7 +103,7 @@ class StressTestRunner:
self.results[test['name']] = {'error': str(e)} self.results[test['name']] = {'error': str(e)}
def finish_suite(self): def finish_suite(self):
mcrfpy.delTimer("tick") self.tick_timer.stop()
print("\n" + "="*50) print("\n" + "="*50)
print("STRESS TEST COMPLETE") print("STRESS TEST COMPLETE")
@ -137,7 +137,7 @@ class StressTestRunner:
ui = init.children ui = init.children
ui.append(mcrfpy.Frame(pos=(0,0), size=(10,10))) # Required for timer to fire ui.append(mcrfpy.Frame(pos=(0,0), size=(10,10))) # Required for timer to fire
init.activate() init.activate()
mcrfpy.setTimer("tick", self.tick, TIMER_INTERVAL_MS) self.tick_timer = mcrfpy.Timer("tick", self.tick, TIMER_INTERVAL_MS)
# ============================================================================= # =============================================================================

View file

@ -6,7 +6,7 @@ import mcrfpy
import sys import sys
import time import time
def run_test(runtime): def run_test(timer, runtime):
print("=" * 60) print("=" * 60)
print("FOV Isolation Test - Is TCOD slow, or is it the Python wrapper?") print("FOV Isolation Test - Is TCOD slow, or is it the Python wrapper?")
print("=" * 60) print("=" * 60)
@ -96,4 +96,4 @@ def run_test(runtime):
init = mcrfpy.Scene("init") init = mcrfpy.Scene("init")
init.activate() init.activate()
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -134,7 +134,7 @@ init = mcrfpy.Scene("init")
init.activate() init.activate()
# Use a timer to let the engine initialize # Use a timer to let the engine initialize
def run_benchmark(runtime): def run_benchmark(timer, runtime):
main() main()
mcrfpy.setTimer("bench", run_benchmark, 100) bench_timer = mcrfpy.Timer("bench", run_benchmark, 100, once=True)

View file

@ -114,7 +114,7 @@ class DemoRunner:
self.current_index = 0 self.current_index = 0
self.render_wait = 0 self.render_wait = 0
def screenshot_cycle(runtime): def screenshot_cycle(timer, runtime):
if self.render_wait == 0: if self.render_wait == 0:
# Set scene and wait for render # Set scene and wait for render
if self.current_index >= len(self.screens): if self.current_index >= len(self.screens):
@ -139,7 +139,7 @@ class DemoRunner:
print("Done!") print("Done!")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("screenshot", screenshot_cycle, 50) self.screenshot_timer = mcrfpy.Timer("screenshot", screenshot_cycle, 50)
def run_interactive(self): def run_interactive(self):
"""Run in interactive mode with menu.""" """Run in interactive mode with menu."""

View file

@ -126,9 +126,10 @@ def setup_scene():
patrol_demo.on_key = on_keypress patrol_demo.on_key = on_keypress
# Start patrol timer # Start patrol timer
mcrfpy.setTimer("patrol", patrol_step, move_timer_ms) global patrol_timer
patrol_timer = mcrfpy.Timer("patrol", patrol_step, move_timer_ms)
def patrol_step(runtime): def patrol_step(timer, runtime):
"""Move entity one step toward current waypoint""" """Move entity one step toward current waypoint"""
global current_waypoint, patrol_paused global current_waypoint, patrol_paused

View file

@ -784,12 +784,12 @@ def run_demo():
demo_state = create_demo_scene() demo_state = create_demo_scene()
# Set up exit timer for headless testing # Set up exit timer for headless testing
def check_exit(dt): def check_exit(timer, dt):
# In headless mode, exit after a short delay # In headless mode, exit after a short delay
# In interactive mode, this won't trigger # In interactive mode, this won't trigger
pass pass
# mcrfpy.setTimer("demo_check", check_exit, 100) # check_exit_timer = mcrfpy.Timer("demo_check", check_exit, 100)
# Run if executed directly # Run if executed directly
@ -801,8 +801,8 @@ if __name__ == "__main__":
# If --screenshot flag, take a screenshot and exit # If --screenshot flag, take a screenshot and exit
if "--screenshot" in sys.argv or len(sys.argv) > 1: if "--screenshot" in sys.argv or len(sys.argv) > 1:
def take_screenshot(dt): def take_screenshot(timer, dt):
automation.screenshot("focus_demo_screenshot.png") automation.screenshot("focus_demo_screenshot.png")
print("Screenshot saved: focus_demo_screenshot.png") print("Screenshot saved: focus_demo_screenshot.png")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("screenshot", take_screenshot, 200) screenshot_timer = mcrfpy.Timer("screenshot", take_screenshot, 200, once=True)

View file

@ -135,7 +135,7 @@ class GeometryDemoRunner:
self.current_index = 0 self.current_index = 0
self.render_wait = 0 self.render_wait = 0
def screenshot_cycle(runtime): def screenshot_cycle(timer, runtime):
if self.render_wait == 0: if self.render_wait == 0:
if self.current_index >= len(self.screens): if self.current_index >= len(self.screens):
print("Done!") print("Done!")
@ -162,7 +162,7 @@ class GeometryDemoRunner:
print("Done!") print("Done!")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("screenshot", screenshot_cycle, 100) self.screenshot_timer = mcrfpy.Timer("screenshot", screenshot_cycle, 100)
def run_interactive(self): def run_interactive(self):
"""Run in interactive mode with menu.""" """Run in interactive mode with menu."""

View file

@ -46,17 +46,19 @@ class GeometryDemoScreen:
def cleanup(self): def cleanup(self):
"""Clean up timers when leaving screen.""" """Clean up timers when leaving screen."""
for timer_name in self.timers: for timer in self.timers:
try: try:
mcrfpy.delTimer(timer_name) timer.stop()
except: except:
pass pass
def restart_timers(self): def restart_timers(self):
"""Re-register timers after cleanup.""" """Re-register timers after cleanup."""
self.timers = [] # Clear old timer references
for name, callback, interval in self._timer_configs: for name, callback, interval in self._timer_configs:
try: try:
mcrfpy.setTimer(name, callback, interval) timer = mcrfpy.Timer(name, callback, interval)
self.timers.append(timer)
except Exception as e: except Exception as e:
print(f"Timer restart failed: {e}") print(f"Timer restart failed: {e}")
@ -111,6 +113,6 @@ class GeometryDemoScreen:
if callback is None: if callback is None:
print(f"Warning: Timer '{name}' callback is None, skipping") print(f"Warning: Timer '{name}' callback is None, skipping")
return return
mcrfpy.setTimer(name, callback, interval) timer = mcrfpy.Timer(name, callback, interval)
self.timers.append(name) self.timers.append(timer)
self._timer_configs.append((name, callback, interval)) self._timer_configs.append((name, callback, interval))

View file

@ -269,7 +269,7 @@ class PathfindingAnimatedDemo(GeometryDemoScreen):
self.dist_label.fill_color = mcrfpy.Color(150, 150, 150) self.dist_label.fill_color = mcrfpy.Color(150, 150, 150)
self.ui.append(self.dist_label) self.ui.append(self.dist_label)
def _tick(self, runtime): def _tick(self, timer, runtime):
"""Advance one turn.""" """Advance one turn."""
self.current_time += 1 self.current_time += 1
self.time_label.text = f"Turn: {self.current_time}" self.time_label.text = f"Turn: {self.current_time}"

View file

@ -255,7 +255,7 @@ class SolarSystemDemo(GeometryDemoScreen):
self.ui.append(moon_path) self.ui.append(moon_path)
self.orbit_rings[moon.name + "_path"] = moon_path self.orbit_rings[moon.name + "_path"] = moon_path
def _tick(self, runtime): def _tick(self, timer, runtime):
"""Advance time by one turn and update planet positions.""" """Advance time by one turn and update planet positions."""
self.current_time += 1 self.current_time += 1

View file

@ -266,7 +266,7 @@ def handle_keypress(scene_name, keycode):
sys.exit(0) sys.exit(0)
# Timer callback for animation # Timer callback for animation
def update_animation(dt): def update_animation(timer, dt):
"""Update animation state""" """Update animation state"""
animate_movement(dt / 1000.0) # Convert ms to seconds animate_movement(dt / 1000.0) # Convert ms to seconds
@ -335,7 +335,7 @@ for i, entity in enumerate(entities):
dijkstra_enhanced.on_key = handle_keypress dijkstra_enhanced.on_key = handle_keypress
# Set up animation timer (60 FPS) # Set up animation timer (60 FPS)
mcrfpy.setTimer("animation", update_animation, 16) animation_timer = mcrfpy.Timer("animation", update_animation, 16)
# Show the scene # Show the scene
dijkstra_enhanced.activate() dijkstra_enhanced.activate()

View file

@ -88,11 +88,11 @@ def test_dijkstra(grid, entities):
return results return results
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to run tests and take screenshot""" """Timer callback to run tests and take screenshot"""
# Run pathfinding tests # Run pathfinding tests
results = test_dijkstra(grid, entities) results = test_dijkstra(grid, entities)
# Update display with results # Update display with results
y_pos = 380 y_pos = 380
for result in results: for result in results:
@ -100,9 +100,9 @@ def run_test(runtime):
caption.fill_color = mcrfpy.Color(200, 200, 200) caption.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(caption) ui.append(caption)
y_pos += 20 y_pos += 20
# Take screenshot # Take screenshot (one-shot timer)
mcrfpy.setTimer("screenshot", lambda rt: take_screenshot(), 500) screenshot_timer = mcrfpy.Timer("screenshot", lambda t, rt: take_screenshot(), 500, once=True)
def take_screenshot(): def take_screenshot():
"""Take screenshot and exit""" """Take screenshot and exit"""
@ -140,7 +140,7 @@ ui.append(legend)
# Set scene # Set scene
dijkstra_test.activate() dijkstra_test.activate()
# Run test after scene loads # Run test after scene loads (one-shot timer)
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)
print("Running Dijkstra tests...") print("Running Dijkstra tests...")

View file

@ -9,7 +9,7 @@ This test verifies that:
import mcrfpy import mcrfpy
import sys import sys
def timer_that_raises(runtime): def timer_that_raises(timer, runtime):
"""A timer callback that raises an exception""" """A timer callback that raises an exception"""
raise ValueError("Intentional test exception") raise ValueError("Intentional test exception")
@ -17,8 +17,8 @@ def timer_that_raises(runtime):
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule the timer - it will fire after 50ms # Schedule the timer - it will fire after 50ms (one-shot timer)
mcrfpy.setTimer("raise_exception", timer_that_raises, 50) exception_timer = mcrfpy.Timer("raise_exception", timer_that_raises, 50, once=True)
# This test expects: # This test expects:
# - Default behavior: exit with code 1 after first exception # - Default behavior: exit with code 1 after first exception

View file

@ -158,7 +158,7 @@ def test_edge_cases():
print(" Edge cases: PASS") print(" Edge cases: PASS")
return True return True
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to run tests after scene is active""" """Timer callback to run tests after scene is active"""
results = [] results = []
@ -185,4 +185,4 @@ if __name__ == "__main__":
test.activate() test.activate()
# Run tests after scene is active # Run tests after scene is active
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -14,7 +14,7 @@ import mcrfpy
import sys import sys
import time import time
def run_test(runtime): def run_test(timer, runtime):
print("=" * 60) print("=" * 60)
print("Issue #146 Regression Test: compute_fov() returns None") print("Issue #146 Regression Test: compute_fov() returns None")
print("=" * 60) print("=" * 60)
@ -111,4 +111,4 @@ def run_test(runtime):
# Initialize and run # Initialize and run
init = mcrfpy.Scene("init") init = mcrfpy.Scene("init")
init.activate() init.activate()
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -11,7 +11,7 @@ Tests:
import mcrfpy import mcrfpy
import sys import sys
def run_test(runtime): def run_test(timer, runtime):
print("=" * 60) print("=" * 60)
print("Issue #147 Regression Test: Dynamic Layer System for Grid") print("Issue #147 Regression Test: Dynamic Layer System for Grid")
print("=" * 60) print("=" * 60)
@ -190,4 +190,4 @@ def run_test(runtime):
# Initialize and run # Initialize and run
init = mcrfpy.Scene("init") init = mcrfpy.Scene("init")
init.activate() init.activate()
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -14,7 +14,7 @@ import mcrfpy
import sys import sys
import time import time
def run_test(runtime): def run_test(timer, runtime):
print("=" * 60) print("=" * 60)
print("Issue #148 Regression Test: Layer Dirty Flags and Caching") print("Issue #148 Regression Test: Layer Dirty Flags and Caching")
print("=" * 60) print("=" * 60)
@ -154,4 +154,4 @@ def run_test(runtime):
# Initialize and run # Initialize and run
init = mcrfpy.Scene("init") init = mcrfpy.Scene("init")
init.activate() init.activate()
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -17,7 +17,7 @@ class CustomEntity(mcrfpy.Entity):
def custom_method(self): def custom_method(self):
return "Custom method called" return "Custom method called"
def run_test(runtime): def run_test(timer, runtime):
"""Test that derived entity classes maintain their type in collections""" """Test that derived entity classes maintain their type in collections"""
try: try:
# Create a grid # Create a grid
@ -85,4 +85,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -149,7 +149,7 @@ def test_color_properties():
return tests_passed == tests_total return tests_passed == tests_total
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to run the test""" """Timer callback to run the test"""
try: try:
success = test_color_properties() success = test_color_properties()
@ -167,4 +167,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -183,7 +183,7 @@ def test_property_introspection():
return tests_passed, tests_total return tests_passed, tests_total
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to run the test""" """Timer callback to run the test"""
try: try:
print("=== Testing Texture and Font Properties (Issue #99) ===\n") print("=== Testing Texture and Font Properties (Issue #99) ===\n")
@ -221,4 +221,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -7,7 +7,7 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def run_test(runtime): def run_test(timer, runtime):
"""Test RenderTexture resizing""" """Test RenderTexture resizing"""
print("Testing Issue #9: RenderTexture resize (minimal)") print("Testing Issue #9: RenderTexture resize (minimal)")
@ -64,4 +64,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test # Schedule test
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -209,7 +209,7 @@ def test_rendertexture_resize():
print(f"\nScreenshots saved to /tmp/issue_9_*.png") print(f"\nScreenshots saved to /tmp/issue_9_*.png")
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to run the test""" """Timer callback to run the test"""
try: try:
test_rendertexture_resize() test_rendertexture_resize()
@ -226,4 +226,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -9,7 +9,7 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def run_test(runtime): def run_test(timer, runtime):
"""Test that UIGrid properly handles resizing""" """Test that UIGrid properly handles resizing"""
try: try:
# Create a grid with initial size # Create a grid with initial size
@ -86,4 +86,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -64,7 +64,7 @@ def demonstrate_solution():
} }
""") """)
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback""" """Timer callback"""
try: try:
demonstrate_solution() demonstrate_solution()
@ -74,10 +74,10 @@ def run_test(runtime):
print(f"\nError: {e}") print(f"\nError: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()
sys.exit(0) sys.exit(0)
# Set up scene and run # Set up scene and run
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -4,43 +4,44 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
from datetime import datetime from datetime import datetime
def run_automation_tests(): def run_automation_tests(timer, runtime):
"""This runs AFTER the game loop has started and rendered frames""" """This runs AFTER the game loop has started and rendered frames"""
print("\n=== Automation Test Running (1 second after start) ===") print("\n=== Automation Test Running (1 second after start) ===")
# NOW we can take screenshots that will show content! # NOW we can take screenshots that will show content!
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"WORKING_screenshot_{timestamp}.png" filename = f"WORKING_screenshot_{timestamp}.png"
# Take screenshot - this should now show our red frame # Take screenshot - this should now show our red frame
result = automation.screenshot(filename) result = automation.screenshot(filename)
print(f"Screenshot taken: {filename} - Result: {result}") print(f"Screenshot taken: {filename} - Result: {result}")
# Test clicking on the frame # Test clicking on the frame
automation.click(200, 200) # Click in center of red frame automation.click(200, 200) # Click in center of red frame
# Test keyboard input # Test keyboard input
automation.typewrite("Hello from timer callback!") automation.typewrite("Hello from timer callback!")
# Take another screenshot to show any changes # Take another screenshot to show any changes
filename2 = f"WORKING_screenshot_after_click_{timestamp}.png" filename2 = f"WORKING_screenshot_after_click_{timestamp}.png"
automation.screenshot(filename2) automation.screenshot(filename2)
print(f"Second screenshot: {filename2}") print(f"Second screenshot: {filename2}")
print("Test completed successfully!") print("Test completed successfully!")
print("\nThis works because:") print("\nThis works because:")
print("1. The game loop has been running for 1 second") print("1. The game loop has been running for 1 second")
print("2. The scene has been rendered multiple times") print("2. The scene has been rendered multiple times")
print("3. The RenderTexture now contains actual rendered content") print("3. The RenderTexture now contains actual rendered content")
# Cancel this timer so it doesn't repeat # Cancel this timer so it doesn't repeat
mcrfpy.delTimer("automation_test") timer.stop()
# Optional: exit after a moment # Optional: exit after a moment
def exit_game(): def exit_game(t, r):
print("Exiting...") print("Exiting...")
mcrfpy.exit() mcrfpy.exit()
mcrfpy.setTimer("exit", exit_game, 500) # Exit 500ms later global exit_timer
exit_timer = mcrfpy.Timer("exit", exit_game, 500, once=True)
# This code runs during --exec script execution # This code runs during --exec script execution
print("=== Setting Up Test Scene ===") print("=== Setting Up Test Scene ===")
@ -73,7 +74,7 @@ frame.on_click = frame_clicked
print("Scene setup complete. Setting timer for automation tests...") print("Scene setup complete. Setting timer for automation tests...")
# THIS IS THE KEY: Set timer to run AFTER the game loop starts # THIS IS THE KEY: Set timer to run AFTER the game loop starts
mcrfpy.setTimer("automation_test", run_automation_tests, 1000) automation_test_timer = mcrfpy.Timer("automation_test", run_automation_tests, 1000, once=True)
print("Timer set. Game loop will start after this script completes.") print("Timer set. Game loop will start after this script completes.")
print("Automation tests will run 1 second later when content is visible.") print("Automation tests will run 1 second later when content is visible.")

View file

@ -1,70 +1,126 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Test for mcrfpy.setTimer() and delTimer() methods""" """Test for mcrfpy.Timer class - replaces old setTimer/delTimer tests (#173)"""
import mcrfpy import mcrfpy
import sys import sys
def test_timers(): def test_timers():
"""Test timer API methods""" """Test Timer class API"""
print("Testing mcrfpy timer methods...") print("Testing mcrfpy.Timer class...")
# Test 1: Create a simple timer # Test 1: Create a simple timer
try: try:
call_count = [0] call_count = [0]
def simple_callback(runtime): def simple_callback(timer, runtime):
call_count[0] += 1 call_count[0] += 1
print(f"Timer callback called, count={call_count[0]}, runtime={runtime}") print(f"Timer callback called, count={call_count[0]}, runtime={runtime}")
mcrfpy.setTimer("test_timer", simple_callback, 100) timer = mcrfpy.Timer("test_timer", simple_callback, 100)
print("✓ setTimer() called successfully") print("✓ Timer() created successfully")
print(f" Timer repr: {timer}")
except Exception as e: except Exception as e:
print(f"setTimer() failed: {e}") print(f"Timer() failed: {e}")
print("FAIL") print("FAIL")
return return
# Test 2: Delete the timer # Test 2: Stop the timer
try: try:
mcrfpy.delTimer("test_timer") timer.stop()
print("✓ delTimer() called successfully") print("✓ timer.stop() called successfully")
assert timer.stopped == True, "Timer should be stopped"
print(f" Timer after stop: {timer}")
except Exception as e: except Exception as e:
print(f"✗ delTimer() failed: {e}") print(f"timer.stop() failed: {e}")
print("FAIL") print("FAIL")
return return
# Test 3: Delete non-existent timer (should not crash) # Test 3: Restart the timer
try: try:
mcrfpy.delTimer("nonexistent_timer") timer.start()
print("✓ delTimer() accepts non-existent timer names") print("✓ timer.start() called successfully")
assert timer.stopped == False, "Timer should not be stopped"
assert timer.active == True, "Timer should be active"
timer.stop() # Clean up
except Exception as e: except Exception as e:
print(f"✗ delTimer() failed on non-existent timer: {e}") print(f"timer.start() failed: {e}")
print("FAIL") print("FAIL")
return return
# Test 4: Create multiple timers # Test 4: Create timer with start=False
try: try:
def callback1(rt): pass def callback2(timer, runtime): pass
def callback2(rt): pass timer2 = mcrfpy.Timer("timer2", callback2, 500, start=False)
def callback3(rt): pass assert timer2.stopped == True, "Timer with start=False should be stopped"
print("✓ Timer with start=False created in stopped state")
mcrfpy.setTimer("timer1", callback1, 500) timer2.start()
mcrfpy.setTimer("timer2", callback2, 750) assert timer2.active == True, "Timer should be active after start()"
mcrfpy.setTimer("timer3", callback3, 250) timer2.stop()
except Exception as e:
print(f"✗ Timer with start=False failed: {e}")
print("FAIL")
return
# Test 5: Create multiple timers
try:
def callback3(t, rt): pass
t1 = mcrfpy.Timer("multi1", callback3, 500)
t2 = mcrfpy.Timer("multi2", callback3, 750)
t3 = mcrfpy.Timer("multi3", callback3, 250)
print("✓ Multiple timers created successfully") print("✓ Multiple timers created successfully")
# Clean up # Clean up
mcrfpy.delTimer("timer1") t1.stop()
mcrfpy.delTimer("timer2") t2.stop()
mcrfpy.delTimer("timer3") t3.stop()
print("✓ Multiple timers deleted successfully") print("✓ Multiple timers stopped successfully")
except Exception as e: except Exception as e:
print(f"✗ Multiple timer test failed: {e}") print(f"✗ Multiple timer test failed: {e}")
print("FAIL") print("FAIL")
return return
print("\nAll timer API tests passed") # Test 6: mcrfpy.timers collection
try:
# Create a timer that's running
running_timer = mcrfpy.Timer("running_test", callback3, 1000)
timers = mcrfpy.timers
assert isinstance(timers, tuple), "mcrfpy.timers should be a tuple"
print(f"✓ mcrfpy.timers returns tuple with {len(timers)} timer(s)")
# Clean up
running_timer.stop()
except Exception as e:
print(f"✗ mcrfpy.timers test failed: {e}")
print("FAIL")
return
# Test 7: active property is read-write
try:
active_timer = mcrfpy.Timer("active_test", callback3, 1000)
assert active_timer.active == True, "New timer should be active"
active_timer.active = False # Should pause
assert active_timer.paused == True, "Timer should be paused after active=False"
active_timer.active = True # Should resume
assert active_timer.active == True, "Timer should be active after active=True"
active_timer.stop()
active_timer.active = True # Should restart from stopped
assert active_timer.active == True, "Timer should restart from stopped via active=True"
active_timer.stop()
print("✓ active property is read-write")
except Exception as e:
print(f"✗ active property test failed: {e}")
print("FAIL")
return
print("\nAll Timer API tests passed")
print("PASS") print("PASS")
# Run the test # Run the test
test_timers() test_timers()
# Exit cleanly # Exit cleanly
sys.exit(0) sys.exit(0)

View file

@ -5,7 +5,7 @@ import sys
import os import os
import json import json
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to test benchmark logging""" """Timer callback to test benchmark logging"""
# Stop the benchmark and get filename # Stop the benchmark and get filename
try: try:
@ -132,4 +132,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test completion after ~100ms (to capture some frames) # Schedule test completion after ~100ms (to capture some frames)
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)

View file

@ -68,13 +68,13 @@ print("\nTest 7: Path after potential sync")
path4 = grid.compute_astar_path(0, 0, 5, 5) path4 = grid.compute_astar_path(0, 0, 5, 5)
print(f" A* path: {path4}") print(f" A* path: {path4}")
def timer_cb(dt): def timer_cb(timer, runtime):
sys.exit(0) sys.exit(0)
# Quick UI setup # Quick UI setup
ui = debug.children ui = debug.children
ui.append(grid) ui.append(grid)
debug.activate() debug.activate()
mcrfpy.setTimer("exit", timer_cb, 100) exit_timer = mcrfpy.Timer("exit", timer_cb, 100, once=True)
print("\nStarting timer...") print("\nStarting timer...")

View file

@ -404,48 +404,50 @@ screenshots = [
("combined_example", "ui_combined_example.png") ("combined_example", "ui_combined_example.png")
] ]
def take_screenshots(runtime): def take_screenshots(timer, runtime):
"""Timer callback to take screenshots sequentially""" """Timer callback to take screenshots sequentially"""
global current_screenshot global current_screenshot
if current_screenshot >= len(screenshots): if current_screenshot >= len(screenshots):
print("\nAll screenshots captured successfully!") print("\nAll screenshots captured successfully!")
print(f"Screenshots saved to: {output_dir}/") print(f"Screenshots saved to: {output_dir}/")
mcrfpy.exit() mcrfpy.exit()
return return
scene_name, filename = screenshots[current_screenshot] scene_name, filename = screenshots[current_screenshot]
# Switch to the scene # Switch to the scene
mcrfpy.current_scene = scene_name mcrfpy.current_scene = scene_name
# Take screenshot after a short delay to ensure rendering # Take screenshot after a short delay to ensure rendering
def capture(): def capture(t, r):
global current_screenshot global current_screenshot
full_path = f"{output_dir}/{filename}" full_path = f"{output_dir}/{filename}"
result = automation.screenshot(full_path) result = automation.screenshot(full_path)
print(f"Screenshot {current_screenshot + 1}/{len(screenshots)}: {filename} - {'Success' if result else 'Failed'}") print(f"Screenshot {current_screenshot + 1}/{len(screenshots)}: {filename} - {'Success' if result else 'Failed'}")
current_screenshot += 1 current_screenshot += 1
# Schedule next screenshot # Schedule next screenshot
mcrfpy.setTimer("next_screenshot", take_screenshots, 200) global next_screenshot_timer
next_screenshot_timer = mcrfpy.Timer("next_screenshot", take_screenshots, 200, once=True)
# Give scene time to render # Give scene time to render
mcrfpy.setTimer("capture", lambda r: capture(), 100) global capture_timer
capture_timer = mcrfpy.Timer("capture", capture, 100, once=True)
# Start with the first scene # Start with the first scene
caption_example.activate() caption_example.activate()
# Start the screenshot process # Start the screenshot process
print(f"\nStarting screenshot capture of {len(screenshots)} scenes...") print(f"\nStarting screenshot capture of {len(screenshots)} scenes...")
mcrfpy.setTimer("start", take_screenshots, 500) start_timer = mcrfpy.Timer("start", take_screenshots, 500, once=True)
# Safety timeout # Safety timeout
def safety_exit(runtime): def safety_exit(timer, runtime):
print("\nERROR: Safety timeout reached! Exiting...") print("\nERROR: Safety timeout reached! Exiting...")
mcrfpy.exit() mcrfpy.exit()
mcrfpy.setTimer("safety", safety_exit, 30000) safety_timer = mcrfpy.Timer("safety", safety_exit, 30000, once=True)
print("Setup complete. Game loop starting...") print("Setup complete. Game loop starting...")

View file

@ -5,13 +5,13 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def capture_grid(runtime): def capture_grid(timer, runtime):
"""Capture grid example after render loop starts""" """Capture grid example after render loop starts"""
# Take screenshot # Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_grid_example.png") automation.screenshot("mcrogueface.github.io/images/ui_grid_example.png")
print("Grid screenshot saved!") print("Grid screenshot saved!")
# Exit after capturing # Exit after capturing
sys.exit(0) sys.exit(0)
@ -112,4 +112,4 @@ ui.append(info)
grid.activate() grid.activate()
# Set timer to capture after rendering starts # Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_grid, 100) capture_timer = mcrfpy.Timer("capture", capture_grid, 100, once=True)

View file

@ -5,13 +5,13 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def capture_sprites(runtime): def capture_sprites(timer, runtime):
"""Capture sprite examples after render loop starts""" """Capture sprite examples after render loop starts"""
# Take screenshot # Take screenshot
automation.screenshot("mcrogueface.github.io/images/ui_sprite_example.png") automation.screenshot("mcrogueface.github.io/images/ui_sprite_example.png")
print("Sprite screenshot saved!") print("Sprite screenshot saved!")
# Exit after capturing # Exit after capturing
sys.exit(0) sys.exit(0)
@ -157,4 +157,4 @@ ui.append(scale_label)
sprites.activate() sprites.activate()
# Set timer to capture after rendering starts # Set timer to capture after rendering starts
mcrfpy.setTimer("capture", capture_sprites, 100) capture_timer = mcrfpy.Timer("capture", capture_sprites, 100, once=True)

View file

@ -3,7 +3,7 @@
Test for keypressScene() validation - should reject non-callable arguments Test for keypressScene() validation - should reject non-callable arguments
""" """
def test_keypress_validation(timer_name): def test_keypress_validation(timer, runtime):
"""Test that keypressScene validates its argument is callable""" """Test that keypressScene validates its argument is callable"""
import mcrfpy import mcrfpy
import sys import sys
@ -90,4 +90,4 @@ def test_keypress_validation(timer_name):
# Execute the test after a short delay # Execute the test after a short delay
import mcrfpy import mcrfpy
mcrfpy.setTimer("test", test_keypress_validation, 100) test_timer = mcrfpy.Timer("test", test_keypress_validation, 100, once=True)

View file

@ -6,17 +6,17 @@ from mcrfpy import automation
import sys import sys
import time import time
def take_screenshot(runtime): def take_screenshot(timer, runtime):
"""Take screenshot after render starts""" """Take screenshot after render starts"""
print(f"Timer callback fired at runtime: {runtime}") print(f"Timer callback fired at runtime: {runtime}")
# Try different paths # Try different paths
paths = [ paths = [
"test_screenshot.png", "test_screenshot.png",
"./test_screenshot.png", "./test_screenshot.png",
"mcrogueface.github.io/images/test_screenshot.png" "mcrogueface.github.io/images/test_screenshot.png"
] ]
for path in paths: for path in paths:
try: try:
print(f"Trying to save to: {path}") print(f"Trying to save to: {path}")
@ -24,7 +24,7 @@ def take_screenshot(runtime):
print(f"Success: {path}") print(f"Success: {path}")
except Exception as e: except Exception as e:
print(f"Failed {path}: {e}") print(f"Failed {path}: {e}")
sys.exit(0) sys.exit(0)
# Create minimal scene # Create minimal scene
@ -41,5 +41,5 @@ test.activate()
# Use timer to ensure rendering has started # Use timer to ensure rendering has started
print("Setting timer...") print("Setting timer...")
mcrfpy.setTimer("screenshot", take_screenshot, 500) # Wait 0.5 seconds mcrfpy.Timer("screenshot", take_screenshot, 500, once=True) # Wait 0.5 seconds
print("Timer set, entering game loop...") print("Timer set, entering game loop...")

View file

@ -6,18 +6,18 @@ from mcrfpy import automation
# Counter to track timer calls # Counter to track timer calls
call_count = 0 call_count = 0
def take_screenshot_and_exit(): def take_screenshot_and_exit(timer, runtime):
"""Timer callback that takes screenshot then exits""" """Timer callback that takes screenshot then exits"""
global call_count global call_count
call_count += 1 call_count += 1
print(f"\nTimer callback fired! (call #{call_count})") print(f"\nTimer callback fired! (call #{call_count})")
# Take screenshot # Take screenshot
filename = f"timer_screenshot_test_{call_count}.png" filename = f"timer_screenshot_test_{call_count}.png"
result = automation.screenshot(filename) result = automation.screenshot(filename)
print(f"Screenshot result: {result} -> {filename}") print(f"Screenshot result: {result} -> {filename}")
# Exit after first call # Exit after first call
if call_count >= 1: if call_count >= 1:
print("Exiting game...") print("Exiting game...")
@ -35,6 +35,6 @@ frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200),
ui.append(frame) ui.append(frame)
print("Setting timer to fire in 100ms...") print("Setting timer to fire in 100ms...")
mcrfpy.setTimer("screenshot_timer", take_screenshot_and_exit, 100) mcrfpy.Timer("screenshot_timer", take_screenshot_and_exit, 100, once=True)
print("Setup complete. Game loop starting...") print("Setup complete. Game loop starting...")

View file

@ -6,6 +6,7 @@ import sys
# Global state to track callback # Global state to track callback
callback_count = 0 callback_count = 0
callback_demo = None # Will be set in setup_and_run
def my_callback(anim, target): def my_callback(anim, target):
"""Simple callback that prints when animation completes""" """Simple callback that prints when animation completes"""
@ -16,47 +17,48 @@ def my_callback(anim, target):
def setup_and_run(): def setup_and_run():
"""Set up scene and run animation with callback""" """Set up scene and run animation with callback"""
global callback_demo
# Create scene # Create scene
callback_demo = mcrfpy.Scene("callback_demo") callback_demo = mcrfpy.Scene("callback_demo")
callback_demo.activate() callback_demo.activate()
# Create a frame to animate # Create a frame to animate
frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0)) frame = mcrfpy.Frame((100, 100), (200, 200), fill_color=(255, 0, 0))
ui = callback_demo.children ui = callback_demo.children
ui.append(frame) ui.append(frame)
# Create animation with callback # Create animation with callback
print("Starting animation with callback...") print("Starting animation with callback...")
anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback) anim = mcrfpy.Animation("x", 400.0, 1.0, "easeInOutQuad", callback=my_callback)
anim.start(frame) anim.start(frame)
# Schedule check after animation should complete
mcrfpy.setTimer("check", check_result, 1500)
def check_result(runtime): # Schedule check after animation should complete
mcrfpy.Timer("check", check_result, 1500, once=True)
def check_result(timer, runtime):
"""Check if callback fired correctly""" """Check if callback fired correctly"""
global callback_count global callback_count, callback_demo
if callback_count == 1: if callback_count == 1:
print("SUCCESS: Callback fired exactly once!") print("SUCCESS: Callback fired exactly once!")
# Test 2: Animation without callback # Test 2: Animation without callback
print("\nTesting animation without callback...") print("\nTesting animation without callback...")
ui = callback_demo.children ui = callback_demo.children
frame = ui[0] frame = ui[0]
anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear") anim2 = mcrfpy.Animation("y", 300.0, 0.5, "linear")
anim2.start(frame) anim2.start(frame)
mcrfpy.setTimer("final", final_check, 700) mcrfpy.Timer("final", final_check, 700, once=True)
else: else:
print(f"FAIL: Expected 1 callback, got {callback_count}") print(f"FAIL: Expected 1 callback, got {callback_count}")
sys.exit(1) sys.exit(1)
def final_check(runtime): def final_check(timer, runtime):
"""Final check - callback count should still be 1""" """Final check - callback count should still be 1"""
global callback_count global callback_count
if callback_count == 1: if callback_count == 1:
print("SUCCESS: No unexpected callbacks fired!") print("SUCCESS: No unexpected callbacks fired!")
print("\nAnimation callback feature working correctly!") print("\nAnimation callback feature working correctly!")
@ -68,4 +70,4 @@ def final_check(runtime):
# Start the demo # Start the demo
print("Animation Callback Demo") print("Animation Callback Demo")
print("=" * 30) print("=" * 30)
setup_and_run() setup_and_run()

View file

@ -35,35 +35,36 @@ class PathAnimator:
if self.current_index >= len(self.path): if self.current_index >= len(self.path):
# Path complete # Path complete
self.animating = False self.animating = False
mcrfpy.delTimer(self.check_timer_name) if hasattr(self, '_check_timer'):
self._check_timer.stop()
if self.on_complete: if self.on_complete:
self.on_complete() self.on_complete()
return return
# Get target position # Get target position
target_x, target_y = self.path[self.current_index] target_x, target_y = self.path[self.current_index]
# Create animations # Create animations
self.anim_x = mcrfpy.Animation("x", float(target_x), self.step_duration, "easeInOut") self.anim_x = mcrfpy.Animation("x", float(target_x), self.step_duration, "easeInOut")
self.anim_y = mcrfpy.Animation("y", float(target_y), self.step_duration, "easeInOut") self.anim_y = mcrfpy.Animation("y", float(target_y), self.step_duration, "easeInOut")
# Start animations # Start animations
self.anim_x.start(self.entity) self.anim_x.start(self.entity)
self.anim_y.start(self.entity) self.anim_y.start(self.entity)
# Update visibility if entity has this method # Update visibility if entity has this method
if hasattr(self.entity, 'update_visibility'): if hasattr(self.entity, 'update_visibility'):
self.entity.update_visibility() self.entity.update_visibility()
# Set timer to check completion # Set timer to check completion
mcrfpy.setTimer(self.check_timer_name, self._check_completion, 50) self._check_timer = mcrfpy.Timer(self.check_timer_name, self._check_completion, 50)
def _check_completion(self, dt): def _check_completion(self, timer, runtime):
"""Check if current animation is complete""" """Check if current animation is complete"""
if hasattr(self.anim_x, 'is_complete') and self.anim_x.is_complete: if hasattr(self.anim_x, 'is_complete') and self.anim_x.is_complete:
# Move to next step # Move to next step
self.current_index += 1 self.current_index += 1
mcrfpy.delTimer(self.check_timer_name) timer.stop()
self._animate_next_step() self._animate_next_step()
# Create test scene # Create test scene
@ -165,7 +166,7 @@ def animate_both():
# Camera follow test # Camera follow test
camera_follow = False camera_follow = False
def update_camera(dt): def update_camera(timer, runtime):
"""Update camera to follow player if enabled""" """Update camera to follow player if enabled"""
if camera_follow and player_animator and player_animator.animating: if camera_follow and player_animator and player_animator.animating:
# Smooth camera follow # Smooth camera follow
@ -205,7 +206,7 @@ chain_test.activate()
chain_test.on_key = handle_input chain_test.on_key = handle_input
# Camera update timer # Camera update timer
mcrfpy.setTimer("cam_update", update_camera, 100) cam_update_timer = mcrfpy.Timer("cam_update", update_camera, 100)
print("Animation Chaining Test") print("Animation Chaining Test")
print("=======================") print("=======================")

View file

@ -38,25 +38,25 @@ class AnimationTracker:
# Track it # Track it
active_animations[self.name] = self active_animations[self.name] = self
# Set timer to check completion # Set timer to check completion
check_interval = 100 # ms check_interval = 100 # ms
mcrfpy.setTimer(f"check_{self.name}", self._check_complete, check_interval) self._check_timer = mcrfpy.Timer(f"check_{self.name}", self._check_complete, check_interval)
def _check_complete(self, dt): def _check_complete(self, timer, runtime):
"""Check if animation is complete""" """Check if animation is complete"""
if self.animation and hasattr(self.animation, 'is_complete') and self.animation.is_complete: if self.animation and hasattr(self.animation, 'is_complete') and self.animation.is_complete:
# Log completion # Log completion
log_entry = f"COMPLETE: {self.name}" log_entry = f"COMPLETE: {self.name}"
animation_log.append(log_entry) animation_log.append(log_entry)
print(log_entry) print(log_entry)
# Remove from active # Remove from active
if self.name in active_animations: if self.name in active_animations:
del active_animations[self.name] del active_animations[self.name]
# Stop checking # Stop checking
mcrfpy.delTimer(f"check_{self.name}") timer.stop()
# Create test scene # Create test scene
anim_debug = mcrfpy.Scene("anim_debug") anim_debug = mcrfpy.Scene("anim_debug")
@ -117,14 +117,15 @@ def test_rapid_fire():
# Start first animation # Start first animation
anim1 = AnimationTracker("rapid_1", entity, "x", 8.0, 2.0) anim1 = AnimationTracker("rapid_1", entity, "x", 8.0, 2.0)
anim1.start() anim1.start()
# Start another after 500ms (before first completes) # Start another after 500ms (before first completes)
def start_second(dt): def start_second(timer, runtime):
anim2 = AnimationTracker("rapid_2", entity, "x", 12.0, 1.0) anim2 = AnimationTracker("rapid_2", entity, "x", 12.0, 1.0)
anim2.start() anim2.start()
mcrfpy.delTimer("rapid_timer") timer.stop()
mcrfpy.setTimer("rapid_timer", start_second, 500) global rapid_timer
rapid_timer = mcrfpy.Timer("rapid_timer", start_second, 500, once=True)
def test_sequential(): def test_sequential():
"""Test proper sequential animations""" """Test proper sequential animations"""
@ -142,14 +143,14 @@ def test_sequential():
if index >= len(sequence): if index >= len(sequence):
print("Sequence complete!") print("Sequence complete!")
return return
name, prop, value, duration = sequence[index] name, prop, value, duration = sequence[index]
anim = AnimationTracker(name, entity, prop, value, duration) anim = AnimationTracker(name, entity, prop, value, duration)
anim.start() anim.start()
# Schedule next # Schedule next
delay = int(duration * 1000) + 100 # Add buffer delay = int(duration * 1000) + 100 # Add buffer
mcrfpy.setTimer(f"seq_timer_{index}", lambda dt: run_sequence(index + 1), delay) mcrfpy.Timer(f"seq_timer_{index}", lambda t, r: run_sequence(index + 1), delay, once=True)
run_sequence() run_sequence()
@ -163,19 +164,20 @@ def test_conflicting():
anim1.start() anim1.start()
# After 1 second, start conflicting animation to x=2 # After 1 second, start conflicting animation to x=2
def start_conflict(dt): def start_conflict(timer, runtime):
print("Starting conflicting animation!") print("Starting conflicting animation!")
anim2 = AnimationTracker("conflict_2", entity, "x", 2.0, 1.0) anim2 = AnimationTracker("conflict_2", entity, "x", 2.0, 1.0)
anim2.start() anim2.start()
mcrfpy.delTimer("conflict_timer") timer.stop()
mcrfpy.setTimer("conflict_timer", start_conflict, 1000) global conflict_timer
conflict_timer = mcrfpy.Timer("conflict_timer", start_conflict, 1000, once=True)
# Update display # Update display
def update_display(dt): def update_display(timer, runtime):
pos_display.text = f"Entity position: ({entity.x:.2f}, {entity.y:.2f})" pos_display.text = f"Entity position: ({entity.x:.2f}, {entity.y:.2f})"
active_display.text = f"Active animations: {len(active_animations)}" active_display.text = f"Active animations: {len(active_animations)}"
# Show active animation names # Show active animation names
if active_animations: if active_animations:
names = ", ".join(active_animations.keys()) names = ", ".join(active_animations.keys())
@ -217,7 +219,7 @@ def handle_input(key, state):
# Setup # Setup
anim_debug.activate() anim_debug.activate()
anim_debug.on_key = handle_input anim_debug.on_key = handle_input
mcrfpy.setTimer("update", update_display, 100) update_display_timer = mcrfpy.Timer("update", update_display, 100)
print("Animation Debug Tool") print("Animation Debug Tool")
print("====================") print("====================")

View file

@ -210,7 +210,7 @@ def test_8_replace_completes_old():
test_result("Replace completes old animation", False, str(e)) test_result("Replace completes old animation", False, str(e))
def run_all_tests(runtime): def run_all_tests(timer, runtime):
"""Run all property locking tests""" """Run all property locking tests"""
print("\nRunning Animation Property Locking Tests...") print("\nRunning Animation Property Locking Tests...")
print("-" * 50) print("-" * 50)
@ -246,4 +246,4 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Start tests after a brief delay to allow scene to initialize # Start tests after a brief delay to allow scene to initialize
mcrfpy.setTimer("start", run_all_tests, 100) mcrfpy.Timer("start", run_all_tests, 100, once=True)

View file

@ -93,32 +93,32 @@ def test_3_complete_animation():
def test_4_multiple_animations_timer(): def test_4_multiple_animations_timer():
"""Test creating multiple animations in timer callback""" """Test creating multiple animations in timer callback"""
success = False success = False
def create_animations(runtime): def create_animations(timer, runtime):
nonlocal success nonlocal success
try: try:
ui = test.children ui = test.children
frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100)) frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
ui.append(frame) ui.append(frame)
# Create multiple animations rapidly (this used to crash) # Create multiple animations rapidly (this used to crash)
for i in range(10): for i in range(10):
anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear") anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear")
anim.start(frame) anim.start(frame)
success = True success = True
except Exception as e: except Exception as e:
print(f"Timer animation error: {e}") print(f"Timer animation error: {e}")
finally: finally:
mcrfpy.setTimer("exit", lambda t: None, 100) mcrfpy.Timer("exit", lambda t, r: None, 100, once=True)
# Clear scene # Clear scene
ui = test.children ui = test.children
while len(ui) > 0: while len(ui) > 0:
ui.remove(len(ui) - 1) ui.remove(len(ui) - 1)
mcrfpy.setTimer("test", create_animations, 50) mcrfpy.Timer("test", create_animations, 50, once=True)
mcrfpy.setTimer("check", lambda t: test_result("Multiple animations in timer", success), 200) mcrfpy.Timer("check", lambda t, r: test_result("Multiple animations in timer", success), 200, once=True)
def test_5_scene_cleanup(): def test_5_scene_cleanup():
"""Test that changing scenes cleans up animations""" """Test that changing scenes cleans up animations"""
@ -168,38 +168,38 @@ def test_6_animation_after_clear():
except Exception as e: except Exception as e:
test_result("Animation after UI clear", False, str(e)) test_result("Animation after UI clear", False, str(e))
def run_all_tests(runtime): def run_all_tests(timer, runtime):
"""Run all RAII tests""" """Run all RAII tests"""
print("\nRunning RAII Animation Tests...") print("\nRunning RAII Animation Tests...")
print("-" * 40) print("-" * 40)
test_1_basic_animation() test_1_basic_animation()
test_2_remove_animated_object() test_2_remove_animated_object()
test_3_complete_animation() test_3_complete_animation()
test_4_multiple_animations_timer() test_4_multiple_animations_timer()
test_5_scene_cleanup() test_5_scene_cleanup()
test_6_animation_after_clear() test_6_animation_after_clear()
# Schedule result summary
mcrfpy.setTimer("results", print_results, 500)
def print_results(runtime): # Schedule result summary
mcrfpy.Timer("results", print_results, 500, once=True)
def print_results(timer, runtime):
"""Print test results""" """Print test results"""
print("\n" + "=" * 40) print("\n" + "=" * 40)
print(f"Tests passed: {tests_passed}") print(f"Tests passed: {tests_passed}")
print(f"Tests failed: {tests_failed}") print(f"Tests failed: {tests_failed}")
if tests_failed == 0: if tests_failed == 0:
print("\n All tests passed! RAII implementation is working correctly.") print("\n+ All tests passed! RAII implementation is working correctly.")
else: else:
print(f"\n {tests_failed} tests failed.") print(f"\nx {tests_failed} tests failed.")
print("\nFailed tests:") print("\nFailed tests:")
for name, passed, details in test_results: for name, passed, details in test_results:
if not passed: if not passed:
print(f" - {name}: {details}") print(f" - {name}: {details}")
# Exit # Exit
mcrfpy.setTimer("exit", lambda t: sys.exit(0 if tests_failed == 0 else 1), 500) mcrfpy.Timer("exit", lambda t, r: sys.exit(0 if tests_failed == 0 else 1), 500, once=True)
# Setup and run # Setup and run
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
@ -212,4 +212,4 @@ bg.fill_color = mcrfpy.Color(20, 20, 30)
ui.append(bg) ui.append(bg)
# Start tests # Start tests
mcrfpy.setTimer("start", run_all_tests, 100) start_timer = mcrfpy.Timer("start", run_all_tests, 100, once=True)

View file

@ -6,7 +6,7 @@ Test if the crash is related to removing animated objects
import mcrfpy import mcrfpy
import sys import sys
def clear_and_recreate(runtime): def clear_and_recreate(timer, runtime):
"""Clear UI and recreate - mimics demo switching""" """Clear UI and recreate - mimics demo switching"""
print(f"\nTimer called at {runtime}") print(f"\nTimer called at {runtime}")
@ -31,9 +31,10 @@ def clear_and_recreate(runtime):
anim.start(f) anim.start(f)
print("New objects created and animated") print("New objects created and animated")
# Schedule exit # Schedule exit
mcrfpy.setTimer("exit", lambda t: sys.exit(0), 2000) global exit_timer
exit_timer = mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 2000, once=True)
# Create initial scene # Create initial scene
print("Creating scene...") print("Creating scene...")
@ -60,6 +61,6 @@ for i in range(10):
print(f"Initial scene has {len(ui)} elements") print(f"Initial scene has {len(ui)} elements")
# Schedule the clear and recreate # Schedule the clear and recreate
mcrfpy.setTimer("switch", clear_and_recreate, 1000) switch_timer = mcrfpy.Timer("switch", clear_and_recreate, 1000, once=True)
print("\nEntering game loop...") print("\nEntering game loop...")

View file

@ -114,7 +114,7 @@ print(" - Empty paths returned for blocked destinations")
print(" - Diagonal movement supported") print(" - Diagonal movement supported")
# Quick visual test # Quick visual test
def visual_test(runtime): def visual_test(timer, runtime):
print("\nVisual test timer fired") print("\nVisual test timer fired")
sys.exit(0) sys.exit(0)
@ -125,6 +125,6 @@ grid.position = (50, 50)
grid.size = (400, 400) grid.size = (400, 400)
astar_test.activate() astar_test.activate()
mcrfpy.setTimer("visual", visual_test, 100) visual_test_timer = mcrfpy.Timer("visual", visual_test, 100, once=True)
print("\nStarting visual test...") print("\nStarting visual test...")

View file

@ -6,7 +6,7 @@ Test #94: Color helper methods - from_hex, to_hex, lerp
import mcrfpy import mcrfpy
import sys import sys
def test_color_helpers(runtime): def test_color_helpers(timer, runtime):
"""Test Color helper methods""" """Test Color helper methods"""
all_pass = True all_pass = True
@ -179,4 +179,4 @@ def test_color_helpers(runtime):
# Run test # Run test
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
mcrfpy.setTimer("test", test_color_helpers, 100) test_timer = mcrfpy.Timer("test", test_color_helpers, 100, once=True)

View file

@ -183,7 +183,7 @@ def test_multi_target_scenario():
cell.tilesprite = 83 # S for safe cell.tilesprite = 83 # S for safe
grid._color_layer.set(best_pos[0], best_pos[1], mcrfpy.Color(0, 255, 0)) grid._color_layer.set(best_pos[0], best_pos[1], mcrfpy.Color(0, 255, 0))
def run_test(runtime): def run_test(timer, runtime):
"""Timer callback to run tests after scene loads""" """Timer callback to run tests after scene loads"""
test_basic_dijkstra() test_basic_dijkstra()
test_libtcod_interface() test_libtcod_interface()
@ -221,7 +221,7 @@ title.fill_color = mcrfpy.Color(255, 255, 255)
ui.append(title) ui.append(title)
# Set timer to run tests # Set timer to run tests
mcrfpy.setTimer("test", run_test, 100) test_timer = mcrfpy.Timer("test", run_test, 100, once=True)
# Show scene # Show scene
dijkstra_test.activate() dijkstra_test.activate()

View file

@ -20,7 +20,7 @@ def test_method_docs():
'createSoundBuffer', 'loadMusic', 'setMusicVolume', 'setSoundVolume', 'createSoundBuffer', 'loadMusic', 'setMusicVolume', 'setSoundVolume',
'playSound', 'getMusicVolume', 'getSoundVolume', 'sceneUI', 'playSound', 'getMusicVolume', 'getSoundVolume', 'sceneUI',
'currentScene', 'setScene', 'createScene', 'keypressScene', 'currentScene', 'setScene', 'createScene', 'keypressScene',
'setTimer', 'delTimer', 'exit', 'setScale', 'find', 'findAll', 'exit', 'setScale', 'find', 'findAll',
'getMetrics' 'getMetrics'
] ]
@ -40,7 +40,7 @@ def test_class_docs():
"""Test class documentation.""" """Test class documentation."""
print("=== Class Documentation ===") print("=== Class Documentation ===")
classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity', 'Color', 'Vector', 'Texture', 'Font'] classes = ['Frame', 'Caption', 'Sprite', 'Grid', 'Entity', 'Color', 'Vector', 'Texture', 'Font', 'Timer']
for class_name in classes: for class_name in classes:
if hasattr(mcrfpy, class_name): if hasattr(mcrfpy, class_name):
@ -80,12 +80,12 @@ def test_method_signatures():
else: else:
print("✗ setScene signature incorrect or missing") print("✗ setScene signature incorrect or missing")
if hasattr(mcrfpy, 'setTimer'): if hasattr(mcrfpy, 'Timer'):
doc = mcrfpy.setTimer.__doc__ doc = mcrfpy.Timer.__doc__
if doc and 'setTimer(name: str, handler: callable, interval: int)' in doc: if doc and 'Timer' in doc:
print("✓ setTimer signature correct") print("+ Timer class documentation present")
else: else:
print("✗ setTimer signature incorrect or missing") print("x Timer class documentation missing")
if hasattr(mcrfpy, 'find'): if hasattr(mcrfpy, 'find'):
doc = mcrfpy.find.__doc__ doc = mcrfpy.find.__doc__

View file

@ -12,9 +12,9 @@ test.activate()
print("Scene created, no animations added") print("Scene created, no animations added")
print("Starting game loop in 100ms...") print("Starting game loop in 100ms...")
def check_alive(runtime): def check_alive(timer, runtime):
print(f"Timer fired at {runtime}ms - AnimationManager survived!") print(f"Timer fired at {runtime}ms - AnimationManager survived!")
mcrfpy.setTimer("exit", lambda t: mcrfpy.exit(), 100) mcrfpy.Timer("exit", lambda t, r: mcrfpy.exit(), 100, once=True)
mcrfpy.setTimer("check", check_alive, 1000) mcrfpy.Timer("check", check_alive, 1000, once=True)
print("If this crashes immediately, AnimationManager has an issue with empty state") print("If this crashes immediately, AnimationManager has an issue with empty state")

View file

@ -77,10 +77,10 @@ current_waypoint = 0
animating = False animating = False
waypoints = [(5,5), (10,5), (10,10), (5,10), (5,5)] waypoints = [(5,5), (10,5), (10,10), (5,10), (5,5)]
def update_position_display(dt): def update_position_display(timer, runtime):
"""Update position display every 200ms""" """Update position display every 200ms"""
pos_display.text = f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})" pos_display.text = f"Entity Position: ({entity.x:.2f}, {entity.y:.2f})"
# Check if entity is at expected position # Check if entity is at expected position
if animating and current_waypoint > 0: if animating and current_waypoint > 0:
target = waypoints[current_waypoint - 1] target = waypoints[current_waypoint - 1]
@ -124,9 +124,10 @@ def animate_to_next_waypoint():
print(f"Started animations: x to {float(target_x)}, y to {float(target_y)}, duration: {duration}s") print(f"Started animations: x to {float(target_x)}, y to {float(target_y)}, duration: {duration}s")
current_waypoint += 1 current_waypoint += 1
# Schedule next waypoint # Schedule next waypoint
mcrfpy.setTimer("next_waypoint", lambda dt: animate_to_next_waypoint(), int(duration * 1000 + 100)) global next_waypoint_timer
next_waypoint_timer = mcrfpy.Timer("next_waypoint", lambda t, r: animate_to_next_waypoint(), int(duration * 1000 + 100), once=True)
def start_animation(): def start_animation():
"""Start or restart the animation sequence""" """Start or restart the animation sequence"""
@ -186,7 +187,7 @@ test_anim.activate()
test_anim.on_key = handle_input test_anim.on_key = handle_input
# Start position update timer # Start position update timer
mcrfpy.setTimer("update_pos", update_position_display, 200) update_pos_timer = mcrfpy.Timer("update_pos", update_position_display, 200)
# No perspective (omniscient view) # No perspective (omniscient view)
grid.perspective = -1 grid.perspective = -1

View file

@ -72,7 +72,7 @@ status.fill_color = mcrfpy.Color(200, 200, 200)
ui.append(status) ui.append(status)
# Update display # Update display
def update_display(dt): def update_display(timer, runtime):
pos_info.text = f"Entity Grid Position: ({entity.x:.2f}, {entity.y:.2f})" pos_info.text = f"Entity Grid Position: ({entity.x:.2f}, {entity.y:.2f})"
# We can't access sprite position from Python, but in C++ it would show # We can't access sprite position from Python, but in C++ it would show
# the issue: sprite position would be (2, 2) instead of pixel coords # the issue: sprite position would be (2, 2) instead of pixel coords
@ -113,7 +113,7 @@ def handle_input(key, state):
# Setup # Setup
fix_demo.activate() fix_demo.activate()
fix_demo.on_key = handle_input fix_demo.on_key = handle_input
mcrfpy.setTimer("update", update_display, 100) update_timer = mcrfpy.Timer("update", update_display, 100)
print("Ready to demonstrate the issue.") print("Ready to demonstrate the issue.")
print() print()

View file

@ -8,9 +8,9 @@ import sys
# Module-level state to avoid closures # Module-level state to avoid closures
_test_state = {} _test_state = {}
def take_second_screenshot(runtime): def take_second_screenshot(timer, runtime):
"""Take final screenshot and exit""" """Take final screenshot and exit"""
mcrfpy.delTimer("screenshot2") timer.stop()
from mcrfpy import automation from mcrfpy import automation
automation.screenshot("frame_clipping_animated.png") automation.screenshot("frame_clipping_animated.png")
print("\nTest completed successfully!") print("\nTest completed successfully!")
@ -19,20 +19,21 @@ def take_second_screenshot(runtime):
print(" - frame_clipping_animated.png (with animation)") print(" - frame_clipping_animated.png (with animation)")
sys.exit(0) sys.exit(0)
def animate_frames(runtime): def animate_frames(timer, runtime):
"""Animate frames to demonstrate clipping""" """Animate frames to demonstrate clipping"""
mcrfpy.delTimer("animate") timer.stop()
scene = test.children scene = test.children
# Move child frames # Move child frames
parent1 = scene[0] parent1 = scene[0]
parent2 = scene[1] parent2 = scene[1]
parent1.children[1].x = 50 parent1.children[1].x = 50
parent2.children[1].x = 50 parent2.children[1].x = 50
mcrfpy.setTimer("screenshot2", take_second_screenshot, 500) global screenshot2_timer
screenshot2_timer = mcrfpy.Timer("screenshot2", take_second_screenshot, 500, once=True)
def test_clipping(runtime): def test_clipping(timer, runtime):
"""Test that clip_children property works correctly""" """Test that clip_children property works correctly"""
mcrfpy.delTimer("test_clipping") timer.stop()
print("Testing UIFrame clipping functionality...") print("Testing UIFrame clipping functionality...")
@ -115,7 +116,8 @@ def test_clipping(runtime):
print(f"PASS: clip_children correctly rejected non-boolean: {e}") print(f"PASS: clip_children correctly rejected non-boolean: {e}")
# Start animation after a short delay # Start animation after a short delay
mcrfpy.setTimer("animate", animate_frames, 100) global animate_timer
animate_timer = mcrfpy.Timer("animate", animate_frames, 100, once=True)
def handle_keypress(key, modifiers): def handle_keypress(key, modifiers):
if key == "c": if key == "c":
@ -129,5 +131,5 @@ print("Creating test scene...")
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
test.on_key = handle_keypress test.on_key = handle_keypress
mcrfpy.setTimer("test_clipping", test_clipping, 100) test_clipping_timer = mcrfpy.Timer("test_clipping", test_clipping, 100, once=True)
print("Test scheduled, running...") print("Test scheduled, running...")

View file

@ -5,9 +5,9 @@ import mcrfpy
from mcrfpy import Color, Frame, Caption, Vector from mcrfpy import Color, Frame, Caption, Vector
import sys import sys
def test_nested_clipping(runtime): def test_nested_clipping(timer, runtime):
"""Test nested frames with clipping""" """Test nested frames with clipping"""
mcrfpy.delTimer("test_nested_clipping") timer.stop()
print("Testing advanced UIFrame clipping with nested frames...") print("Testing advanced UIFrame clipping with nested frames...")
@ -62,8 +62,8 @@ def test_nested_clipping(runtime):
print(f"Inner frame size: {inner.w}x{inner.h}") print(f"Inner frame size: {inner.w}x{inner.h}")
# Dynamically resize frames to test RenderTexture recreation # Dynamically resize frames to test RenderTexture recreation
def resize_test(runtime): def resize_test(timer, runtime):
mcrfpy.delTimer("resize_test") timer.stop()
print("Resizing frames to test RenderTexture recreation...") print("Resizing frames to test RenderTexture recreation...")
outer.w = 450 outer.w = 450
outer.h = 350 outer.h = 350
@ -71,12 +71,13 @@ def test_nested_clipping(runtime):
inner.h = 250 inner.h = 250
print(f"New outer frame size: {outer.w}x{outer.h}") print(f"New outer frame size: {outer.w}x{outer.h}")
print(f"New inner frame size: {inner.w}x{inner.h}") print(f"New inner frame size: {inner.w}x{inner.h}")
# Take screenshot after resize # Take screenshot after resize
mcrfpy.setTimer("screenshot_resize", take_resize_screenshot, 500) global screenshot_resize_timer
screenshot_resize_timer = mcrfpy.Timer("screenshot_resize", take_resize_screenshot, 500, once=True)
def take_resize_screenshot(runtime):
mcrfpy.delTimer("screenshot_resize") def take_resize_screenshot(timer, runtime):
timer.stop()
from mcrfpy import automation from mcrfpy import automation
automation.screenshot("frame_clipping_resized.png") automation.screenshot("frame_clipping_resized.png")
print("\nAdvanced test completed!") print("\nAdvanced test completed!")
@ -88,9 +89,10 @@ def test_nested_clipping(runtime):
from mcrfpy import automation from mcrfpy import automation
automation.screenshot("frame_clipping_nested.png") automation.screenshot("frame_clipping_nested.png")
print("Initial screenshot saved: frame_clipping_nested.png") print("Initial screenshot saved: frame_clipping_nested.png")
# Schedule resize test # Schedule resize test
mcrfpy.setTimer("resize_test", resize_test, 1000) global resize_test_timer
resize_test_timer = mcrfpy.Timer("resize_test", resize_test, 1000, once=True)
# Main execution # Main execution
print("Creating advanced test scene...") print("Creating advanced test scene...")
@ -98,6 +100,6 @@ test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule the test # Schedule the test
mcrfpy.setTimer("test_nested_clipping", test_nested_clipping, 100) test_nested_clipping_timer = mcrfpy.Timer("test_nested_clipping", test_nested_clipping, 100, once=True)
print("Advanced test scheduled, running...") print("Advanced test scheduled, running...")

View file

@ -42,25 +42,25 @@ def test_grid_background():
# Activate the scene # Activate the scene
test.activate() test.activate()
def run_tests(dt): def run_tests(timer, runtime):
"""Run background color tests""" """Run background color tests"""
mcrfpy.delTimer("run_tests") timer.stop()
print("\nTest 1: Default background color") print("\nTest 1: Default background color")
default_color = grid.background_color default_color = grid.background_color
print(f"Default: R={default_color.r}, G={default_color.g}, B={default_color.b}, A={default_color.a}") print(f"Default: R={default_color.r}, G={default_color.g}, B={default_color.b}, A={default_color.a}")
color_display.text = f"R:{default_color.r} G:{default_color.g} B:{default_color.b}" color_display.text = f"R:{default_color.r} G:{default_color.g} B:{default_color.b}"
def test_set_color(dt): def test_set_color(timer, runtime):
mcrfpy.delTimer("test_set") timer.stop()
print("\nTest 2: Set background to blue") print("\nTest 2: Set background to blue")
grid.background_color = mcrfpy.Color(20, 40, 100) grid.background_color = mcrfpy.Color(20, 40, 100)
new_color = grid.background_color new_color = grid.background_color
print(f" Set to: R={new_color.r}, G={new_color.g}, B={new_color.b}") print(f"+ Set to: R={new_color.r}, G={new_color.g}, B={new_color.b}")
color_display.text = f"R:{new_color.r} G:{new_color.g} B:{new_color.b}" color_display.text = f"R:{new_color.r} G:{new_color.g} B:{new_color.b}"
def test_animation(dt): def test_animation(timer, runtime):
mcrfpy.delTimer("test_anim") timer.stop()
print("\nTest 3: Manual color cycling") print("\nTest 3: Manual color cycling")
# Manually change color to test property is working # Manually change color to test property is working
colors = [ colors = [
@ -68,55 +68,55 @@ def test_grid_background():
mcrfpy.Color(20, 200, 20), # Green mcrfpy.Color(20, 200, 20), # Green
mcrfpy.Color(20, 20, 200), # Blue mcrfpy.Color(20, 20, 200), # Blue
] ]
color_index = [0] # Use list to allow modification in nested function color_index = [0] # Use list to allow modification in nested function
def cycle_red(dt): def cycle_red(t, r):
mcrfpy.delTimer("cycle_0") t.stop()
grid.background_color = colors[0] grid.background_color = colors[0]
c = grid.background_color c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f" Set to Red: R={c.r}, G={c.g}, B={c.b}") print(f"+ Set to Red: R={c.r}, G={c.g}, B={c.b}")
def cycle_green(dt): def cycle_green(t, r):
mcrfpy.delTimer("cycle_1") t.stop()
grid.background_color = colors[1] grid.background_color = colors[1]
c = grid.background_color c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f" Set to Green: R={c.r}, G={c.g}, B={c.b}") print(f"+ Set to Green: R={c.r}, G={c.g}, B={c.b}")
def cycle_blue(dt): def cycle_blue(t, r):
mcrfpy.delTimer("cycle_2") t.stop()
grid.background_color = colors[2] grid.background_color = colors[2]
c = grid.background_color c = grid.background_color
color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" color_display.text = f"R:{c.r} G:{c.g} B:{c.b}"
print(f" Set to Blue: R={c.r}, G={c.g}, B={c.b}") print(f"+ Set to Blue: R={c.r}, G={c.g}, B={c.b}")
# Cycle through colors # Cycle through colors
mcrfpy.setTimer("cycle_0", cycle_red, 100) mcrfpy.Timer("cycle_0", cycle_red, 100, once=True)
mcrfpy.setTimer("cycle_1", cycle_green, 400) mcrfpy.Timer("cycle_1", cycle_green, 400, once=True)
mcrfpy.setTimer("cycle_2", cycle_blue, 700) mcrfpy.Timer("cycle_2", cycle_blue, 700, once=True)
def test_complete(dt): def test_complete(timer, runtime):
mcrfpy.delTimer("complete") timer.stop()
print("\nTest 4: Final color check") print("\nTest 4: Final color check")
final_color = grid.background_color final_color = grid.background_color
print(f"Final: R={final_color.r}, G={final_color.g}, B={final_color.b}") print(f"Final: R={final_color.r}, G={final_color.g}, B={final_color.b}")
print("\n Grid background color tests completed!") print("\n+ Grid background color tests completed!")
print("- Default background color works") print("- Default background color works")
print("- Setting background color works") print("- Setting background color works")
print("- Color cycling works") print("- Color cycling works")
sys.exit(0) sys.exit(0)
# Schedule tests # Schedule tests
mcrfpy.setTimer("test_set", test_set_color, 1000) mcrfpy.Timer("test_set", test_set_color, 1000, once=True)
mcrfpy.setTimer("test_anim", test_animation, 2000) mcrfpy.Timer("test_anim", test_animation, 2000, once=True)
mcrfpy.setTimer("complete", test_complete, 4500) mcrfpy.Timer("complete", test_complete, 4500, once=True)
# Start tests # Start tests
mcrfpy.setTimer("run_tests", run_tests, 100) mcrfpy.Timer("run_tests", run_tests, 100, once=True)
if __name__ == "__main__": if __name__ == "__main__":
test_grid_background() test_grid_background()

View file

@ -68,9 +68,7 @@ def test_cell_hover():
automation.moveTo(150, 150) automation.moveTo(150, 150)
automation.moveTo(200, 200) automation.moveTo(200, 200)
def check_hover(runtime): def check_hover(timer, runtime):
mcrfpy.delTimer("check_hover")
print(f" Enter events: {len(enter_events)}, Exit events: {len(exit_events)}") print(f" Enter events: {len(enter_events)}, Exit events: {len(exit_events)}")
print(f" Hovered cell: {grid.hovered_cell}") print(f" Hovered cell: {grid.hovered_cell}")
@ -82,7 +80,7 @@ def test_cell_hover():
# Continue to click test # Continue to click test
test_cell_click() test_cell_click()
mcrfpy.setTimer("check_hover", check_hover, 200) mcrfpy.Timer("check_hover", check_hover, 200, once=True)
def test_cell_click(): def test_cell_click():
@ -105,9 +103,7 @@ def test_cell_click():
automation.click(200, 200) automation.click(200, 200)
def check_click(runtime): def check_click(timer, runtime):
mcrfpy.delTimer("check_click")
print(f" Click events: {len(click_events)}") print(f" Click events: {len(click_events)}")
if len(click_events) >= 1: if len(click_events) >= 1:
@ -118,7 +114,7 @@ def test_cell_click():
print("\n=== All grid cell event tests passed! ===") print("\n=== All grid cell event tests passed! ===")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("check_click", check_click, 200) mcrfpy.Timer("check_click", check_click, 200, once=True)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -4,18 +4,18 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def take_screenshot(runtime): def take_screenshot(timer, runtime):
"""Take screenshot after render completes""" """Take screenshot after render completes"""
mcrfpy.delTimer("screenshot") timer.stop()
automation.screenshot("test_grid_children_result.png") automation.screenshot("test_grid_children_result.png")
print("Screenshot saved to test_grid_children_result.png") print("Screenshot saved to test_grid_children_result.png")
print("PASS - Grid.children test completed") print("PASS - Grid.children test completed")
sys.exit(0) sys.exit(0)
def run_test(runtime): def run_test(timer, runtime):
"""Main test - runs after scene is set up""" """Main test - runs after scene is set up"""
mcrfpy.delTimer("test") timer.stop()
# Get the scene UI # Get the scene UI
ui = test.children ui = test.children
@ -119,11 +119,11 @@ def run_test(runtime):
print(f"\nFinal children count: {len(grid.children)}") print(f"\nFinal children count: {len(grid.children)}")
# Schedule screenshot for next frame # Schedule screenshot for next frame
mcrfpy.setTimer("screenshot", take_screenshot, 100) mcrfpy.Timer("screenshot", take_screenshot, 100, once=True)
# Create a test scene # Create a test scene
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 50) mcrfpy.Timer("test", run_test, 50, once=True)

View file

@ -36,9 +36,7 @@ def test_headless_click():
automation.click(150, 150) automation.click(150, 150)
# Give time for events to process # Give time for events to process
def check_results(runtime): def check_results(timer, runtime):
mcrfpy.delTimer("check_click") # Clean up timer
if len(start_clicks) >= 1: if len(start_clicks) >= 1:
print(f" - Click received: {len(start_clicks)} click(s)") print(f" - Click received: {len(start_clicks)} click(s)")
# Verify position # Verify position
@ -53,7 +51,7 @@ def test_headless_click():
print(f" - No clicks received: FAIL") print(f" - No clicks received: FAIL")
sys.exit(1) sys.exit(1)
mcrfpy.setTimer("check_click", check_results, 200) mcrfpy.Timer("check_click", check_results, 200, once=True)
def test_click_miss(): def test_click_miss():
@ -84,9 +82,7 @@ def test_click_miss():
print(" Clicking outside frame at (50, 50)...") print(" Clicking outside frame at (50, 50)...")
automation.click(50, 50) automation.click(50, 50)
def check_miss_results(runtime): def check_miss_results(timer, runtime):
mcrfpy.delTimer("check_miss") # Clean up timer
if miss_count[0] == 0: if miss_count[0] == 0:
print(" - No click on miss: PASS") print(" - No click on miss: PASS")
# Now run the main click test # Now run the main click test
@ -95,7 +91,7 @@ def test_click_miss():
print(f" - Unexpected {miss_count[0]} click(s): FAIL") print(f" - Unexpected {miss_count[0]} click(s): FAIL")
sys.exit(1) sys.exit(1)
mcrfpy.setTimer("check_miss", check_miss_results, 200) mcrfpy.Timer("check_miss", check_miss_results, 200, once=True)
def test_position_tracking(): def test_position_tracking():

View file

@ -15,12 +15,12 @@ frame = mcrfpy.Frame(pos=(100, 100), size=(200, 200))
frame.fill_color = mcrfpy.Color(255, 100, 100, 255) frame.fill_color = mcrfpy.Color(255, 100, 100, 255)
ui.append(frame) ui.append(frame)
def test_mode(runtime): def test_mode(timer, runtime):
try: try:
# Try to take a screenshot - this should work in both modes # Try to take a screenshot - this should work in both modes
automation.screenshot("test_screenshot.png") automation.screenshot("test_screenshot.png")
print("PASS: Screenshot capability available") print("PASS: Screenshot capability available")
# Check if we can interact with the window # Check if we can interact with the window
try: try:
# In headless mode, this should still work but via the headless renderer # In headless mode, this should still work but via the headless renderer
@ -28,12 +28,12 @@ def test_mode(runtime):
print("PASS: Click automation available") print("PASS: Click automation available")
except Exception as e: except Exception as e:
print(f"Click failed: {e}") print(f"Click failed: {e}")
except Exception as e: except Exception as e:
print(f"Screenshot failed: {e}") print(f"Screenshot failed: {e}")
print("Test complete") print("Test complete")
sys.exit(0) sys.exit(0)
# Run test after render loop starts # Run test after render loop starts
mcrfpy.setTimer("test", test_mode, 100) test_timer = mcrfpy.Timer("test", test_mode, 100, once=True)

View file

@ -22,8 +22,8 @@ ui.append(caption)
print("Script started. Window should appear unless --headless was specified.") print("Script started. Window should appear unless --headless was specified.")
# Exit after 2 seconds # Exit after 2 seconds
def exit_test(runtime): def exit_test(timer, runtime):
print("Test complete. Exiting.") print("Test complete. Exiting.")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("exit", exit_test, 2000) exit_timer = mcrfpy.Timer("exit", exit_test, 2000, once=True)

View file

@ -5,13 +5,17 @@ import mcrfpy
import sys import sys
import time import time
def test_metrics(runtime): # Track success across callbacks
success = True
def test_metrics(timer, runtime):
"""Test the metrics after timer starts""" """Test the metrics after timer starts"""
global success
print("\nRunning metrics test...") print("\nRunning metrics test...")
# Get metrics # Get metrics
metrics = mcrfpy.getMetrics() metrics = mcrfpy.getMetrics()
print("\nPerformance Metrics:") print("\nPerformance Metrics:")
print(f" Frame Time: {metrics['frame_time']:.2f} ms") print(f" Frame Time: {metrics['frame_time']:.2f} ms")
print(f" Avg Frame Time: {metrics['avg_frame_time']:.2f} ms") print(f" Avg Frame Time: {metrics['avg_frame_time']:.2f} ms")
@ -21,17 +25,16 @@ def test_metrics(runtime):
print(f" Visible Elements: {metrics['visible_elements']}") print(f" Visible Elements: {metrics['visible_elements']}")
print(f" Current Frame: {metrics['current_frame']}") print(f" Current Frame: {metrics['current_frame']}")
print(f" Runtime: {metrics['runtime']:.2f} seconds") print(f" Runtime: {metrics['runtime']:.2f} seconds")
# Test that metrics are reasonable # Test that metrics are reasonable
success = True
# Frame time should be positive # Frame time should be positive
if metrics['frame_time'] <= 0: if metrics['frame_time'] <= 0:
print(" FAIL: Frame time should be positive") print(" FAIL: Frame time should be positive")
success = False success = False
else: else:
print(" PASS: Frame time is positive") print(" PASS: Frame time is positive")
# FPS should be reasonable (between 1 and 20000 in headless mode) # FPS should be reasonable (between 1 and 20000 in headless mode)
# In headless mode, FPS can be very high since there's no vsync # In headless mode, FPS can be very high since there's no vsync
if metrics['fps'] < 1 or metrics['fps'] > 20000: if metrics['fps'] < 1 or metrics['fps'] > 20000:
@ -39,72 +42,76 @@ def test_metrics(runtime):
success = False success = False
else: else:
print(f" PASS: FPS {metrics['fps']} is reasonable") print(f" PASS: FPS {metrics['fps']} is reasonable")
# UI elements count (may be 0 if scene hasn't rendered yet) # UI elements count (may be 0 if scene hasn't rendered yet)
if metrics['ui_elements'] < 0: if metrics['ui_elements'] < 0:
print(f" FAIL: UI elements count {metrics['ui_elements']} is negative") print(f" FAIL: UI elements count {metrics['ui_elements']} is negative")
success = False success = False
else: else:
print(f" PASS: UI element count {metrics['ui_elements']} is valid") print(f" PASS: UI element count {metrics['ui_elements']} is valid")
# Visible elements should be <= total elements # Visible elements should be <= total elements
if metrics['visible_elements'] > metrics['ui_elements']: if metrics['visible_elements'] > metrics['ui_elements']:
print(" FAIL: Visible elements > total elements") print(" FAIL: Visible elements > total elements")
success = False success = False
else: else:
print(" PASS: Visible element count is valid") print(" PASS: Visible element count is valid")
# Current frame should be > 0 # Current frame should be > 0
if metrics['current_frame'] <= 0: if metrics['current_frame'] <= 0:
print(" FAIL: Current frame should be > 0") print(" FAIL: Current frame should be > 0")
success = False success = False
else: else:
print(" PASS: Current frame is positive") print(" PASS: Current frame is positive")
# Runtime should be > 0 # Runtime should be > 0
if metrics['runtime'] <= 0: if metrics['runtime'] <= 0:
print(" FAIL: Runtime should be > 0") print(" FAIL: Runtime should be > 0")
success = False success = False
else: else:
print(" PASS: Runtime is positive") print(" PASS: Runtime is positive")
# Test metrics update over multiple frames # Test metrics update over multiple frames
print("\n\nTesting metrics over multiple frames...") print("\n\nTesting metrics over multiple frames...")
# Store initial metrics for comparison
initial_frame = metrics['current_frame']
initial_runtime = metrics['runtime']
# Schedule another check after 100ms # Schedule another check after 100ms
def check_later(runtime2): def check_later(timer2, runtime2):
global success
metrics2 = mcrfpy.getMetrics() metrics2 = mcrfpy.getMetrics()
print(f"\nMetrics after 100ms:") print(f"\nMetrics after 100ms:")
print(f" Frame Time: {metrics2['frame_time']:.2f} ms") print(f" Frame Time: {metrics2['frame_time']:.2f} ms")
print(f" Avg Frame Time: {metrics2['avg_frame_time']:.2f} ms") print(f" Avg Frame Time: {metrics2['avg_frame_time']:.2f} ms")
print(f" FPS: {metrics2['fps']}") print(f" FPS: {metrics2['fps']}")
print(f" Current Frame: {metrics2['current_frame']}") print(f" Current Frame: {metrics2['current_frame']}")
# Frame count should have increased # Frame count should have increased
if metrics2['current_frame'] > metrics['current_frame']: if metrics2['current_frame'] > initial_frame:
print(" PASS: Frame count increased") print(" PASS: Frame count increased")
else: else:
print(" FAIL: Frame count did not increase") print(" FAIL: Frame count did not increase")
nonlocal success
success = False success = False
# Runtime should have increased # Runtime should have increased
if metrics2['runtime'] > metrics['runtime']: if metrics2['runtime'] > initial_runtime:
print(" PASS: Runtime increased") print(" PASS: Runtime increased")
else: else:
print(" FAIL: Runtime did not increase") print(" FAIL: Runtime did not increase")
success = False success = False
print("\n" + "="*50) print("\n" + "="*50)
if success: if success:
print("ALL METRICS TESTS PASSED!") print("ALL METRICS TESTS PASSED!")
else: else:
print("SOME METRICS TESTS FAILED!") print("SOME METRICS TESTS FAILED!")
sys.exit(0 if success else 1) sys.exit(0 if success else 1)
mcrfpy.setTimer("check_later", check_later, 100) mcrfpy.Timer("check_later", check_later, 100, once=True)
# Set up test scene # Set up test scene
print("Setting up metrics test scene...") print("Setting up metrics test scene...")
@ -136,4 +143,4 @@ ui.append(grid)
print(f"Created {len(ui)} UI elements (1 invisible)") print(f"Created {len(ui)} UI elements (1 invisible)")
# Schedule test to run after render loop starts # Schedule test to run after render loop starts
mcrfpy.setTimer("test", test_metrics, 50) mcrfpy.Timer("test", test_metrics, 50, once=True)

View file

@ -153,7 +153,7 @@ def test_enter_exit_simulation():
automation.moveTo(50, 50) automation.moveTo(50, 50)
# Give time for callbacks to execute # Give time for callbacks to execute
def check_results(runtime): def check_results(timer, runtime):
global enter_count, exit_count global enter_count, exit_count
if enter_count >= 1 and exit_count >= 1: if enter_count >= 1 and exit_count >= 1:
@ -166,7 +166,7 @@ def test_enter_exit_simulation():
print("\n=== Basic Mouse Enter/Exit tests passed! ===") print("\n=== Basic Mouse Enter/Exit tests passed! ===")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("check", check_results, 200) mcrfpy.Timer("check", check_results, 200, once=True)
def run_basic_tests(): def run_basic_tests():

View file

@ -7,7 +7,7 @@ This verifies the fix for requiring arguments even with safe default constructor
import mcrfpy import mcrfpy
import sys import sys
def test_ui_constructors(runtime): def test_ui_constructors(timer, runtime):
"""Test that UI classes can be instantiated without arguments""" """Test that UI classes can be instantiated without arguments"""
print("Testing UI class instantiation without arguments...") print("Testing UI class instantiation without arguments...")
@ -88,4 +88,4 @@ def test_ui_constructors(runtime):
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
# Schedule the test to run after game initialization # Schedule the test to run after game initialization
mcrfpy.setTimer("test", test_ui_constructors, 100) test_timer = mcrfpy.Timer("test", test_ui_constructors, 100, once=True)

View file

@ -57,9 +57,7 @@ def test_on_move_fires():
automation.moveTo(200, 200) automation.moveTo(200, 200)
automation.moveTo(250, 250) automation.moveTo(250, 250)
def check_results(runtime): def check_results(timer, runtime):
mcrfpy.delTimer("check_move")
if move_count[0] >= 2: if move_count[0] >= 2:
print(f" - on_move fired {move_count[0]} times: PASS") print(f" - on_move fired {move_count[0]} times: PASS")
print(f" Positions: {positions[:5]}...") print(f" Positions: {positions[:5]}...")
@ -71,7 +69,7 @@ def test_on_move_fires():
print("\n=== on_move basic tests passed! ===") print("\n=== on_move basic tests passed! ===")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("check_move", check_results, 200) mcrfpy.Timer("check_move", check_results, 200, once=True)
def test_on_move_not_outside(): def test_on_move_not_outside():
@ -99,9 +97,7 @@ def test_on_move_not_outside():
automation.moveTo(60, 60) automation.moveTo(60, 60)
automation.moveTo(70, 70) automation.moveTo(70, 70)
def check_results(runtime): def check_results(timer, runtime):
mcrfpy.delTimer("check_outside")
if move_count[0] == 0: if move_count[0] == 0:
print(" - No on_move outside bounds: PASS") print(" - No on_move outside bounds: PASS")
# Chain to the firing test # Chain to the firing test
@ -110,7 +106,7 @@ def test_on_move_not_outside():
print(f" - Unexpected {move_count[0]} move(s) outside bounds: FAIL") print(f" - Unexpected {move_count[0]} move(s) outside bounds: FAIL")
sys.exit(1) sys.exit(1)
mcrfpy.setTimer("check_outside", check_results, 200) mcrfpy.Timer("check_outside", check_results, 200, once=True)
def test_all_types_have_on_move(): def test_all_types_have_on_move():

View file

@ -63,7 +63,7 @@ for y in range(5):
print("\nIf colors are changing in data but not visually, it may be a rendering issue.") print("\nIf colors are changing in data but not visually, it may be a rendering issue.")
# Quick visual test # Quick visual test
def check_visual(runtime): def check_visual(timer, runtime):
print("\nTimer fired - checking if scene is rendering...") print("\nTimer fired - checking if scene is rendering...")
# Take screenshot to see actual rendering # Take screenshot to see actual rendering
try: try:
@ -81,6 +81,6 @@ grid.position = (50, 50)
grid.size = (250, 250) grid.size = (250, 250)
test.activate() test.activate()
mcrfpy.setTimer("check", check_visual, 500) check_timer = mcrfpy.Timer("check", check_visual, 500, once=True)
print("\nStarting render test...") print("\nStarting render test...")

View file

@ -48,11 +48,11 @@ print("\n✓ Pathfinding integration working correctly!")
print("Enhanced demos are ready for interactive use.") print("Enhanced demos are ready for interactive use.")
# Quick animation test # Quick animation test
def test_timer(dt): def test_timer(timer, runtime):
print(f"Timer callback received: dt={dt}ms") print(f"Timer callback received: runtime={runtime}ms")
sys.exit(0) sys.exit(0)
# Set a quick timer to test animation system # Set a quick timer to test animation system
mcrfpy.setTimer("test", test_timer, 100) timer = mcrfpy.Timer("test", test_timer, 100, once=True)
print("\nTesting timer system for animations...") print("\nTesting timer system for animations...")

View file

@ -3,8 +3,8 @@
import mcrfpy import mcrfpy
import sys import sys
def test_properties(runtime): def test_properties(timer, runtime):
mcrfpy.delTimer("test_properties") timer.stop()
print("\n=== Testing Properties ===") print("\n=== Testing Properties ===")
@ -54,4 +54,4 @@ def test_properties(runtime):
sys.exit(0) sys.exit(0)
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
mcrfpy.setTimer("test_properties", test_properties, 100) test_properties_timer = mcrfpy.Timer("test_properties", test_properties, 100, once=True)

View file

@ -21,7 +21,7 @@ def test(condition, message):
test_results.append(f"{message}") test_results.append(f"{message}")
test_passed = False test_passed = False
def run_tests(runtime): def run_tests(timer, runtime):
"""Timer callback to run tests after game loop starts""" """Timer callback to run tests after game loop starts"""
global test_passed global test_passed
@ -146,6 +146,6 @@ test_scene = mcrfpy.Scene("test_scene")
test_scene.activate() test_scene.activate()
# Schedule tests to run after game loop starts # Schedule tests to run after game loop starts
mcrfpy.setTimer("test", run_tests, 100) test_timer = mcrfpy.Timer("test", run_tests, 100, once=True)
print("Python object cache test initialized. Running tests...") print("Python object cache test initialized. Running tests...")

View file

@ -186,7 +186,7 @@ for s in (red_scene, blue_scene, green_scene, menu_scene):
# Option to run automatic test # Option to run automatic test
if len(sys.argv) > 1 and sys.argv[1] == "--auto": if len(sys.argv) > 1 and sys.argv[1] == "--auto":
mcrfpy.setTimer("auto_test", test_automatic_transitions, 1000) mcrfpy.Timer("auto_test", lambda t, r: test_automatic_transitions(r), 1000, once=True)
else: else:
print("\nManual test mode. Use keyboard controls shown on screen.") print("\nManual test mode. Use keyboard controls shown on screen.")
print("Run with --auto flag for automatic transition demo.") print("Run with --auto flag for automatic transition demo.")

View file

@ -7,8 +7,8 @@ def cb(a, t):
print("CB") print("CB")
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
e = mcrfpy.Entity((0, 0), texture=None, sprite_index=0) e = mcrfpy.Entity((0, 0), texture=None, sprite_index=0)
a = mcrfpy.Animation("x", 1.0, 0.1, "linear", callback=cb) a = mcrfpy.Animation("x", 1.0, 0.1, "linear", callback=cb)
a.start(e) a.start(e)
mcrfpy.setTimer("exit", lambda r: sys.exit(0), 200) mcrfpy.Timer("exit", lambda t, r: sys.exit(0), 200, once=True)

View file

@ -3,8 +3,8 @@
import mcrfpy import mcrfpy
import sys import sys
def simple_test(runtime): def simple_test(timer, runtime):
mcrfpy.delTimer("simple_test") timer.stop()
try: try:
# Test basic functionality # Test basic functionality
@ -27,4 +27,4 @@ def simple_test(runtime):
sys.exit(0) sys.exit(0)
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
mcrfpy.setTimer("simple_test", simple_test, 100) simple_test_timer = mcrfpy.Timer("simple_test", simple_test, 100, once=True)

View file

@ -63,13 +63,13 @@ def run_tests():
print("Test 5: Timer fires after step() advances past interval") print("Test 5: Timer fires after step() advances past interval")
timer_fired = [False] # Use list for mutable closure timer_fired = [False] # Use list for mutable closure
def on_timer(runtime): def on_timer(timer, runtime):
"""Timer callback - receives runtime in ms""" """Timer callback - receives timer object and runtime in ms"""
timer_fired[0] = True timer_fired[0] = True
print(f" Timer fired at simulation time={runtime}ms") print(f" Timer fired at simulation time={runtime}ms")
# Set a timer for 500ms # Set a timer for 500ms
mcrfpy.setTimer("test_timer", on_timer, 500) test_timer = mcrfpy.Timer("test_timer", on_timer, 500)
# Step 600ms - timer should fire (500ms interval + some buffer) # Step 600ms - timer should fire (500ms interval + some buffer)
dt = mcrfpy.step(0.6) dt = mcrfpy.step(0.6)
@ -88,7 +88,7 @@ def run_tests():
print(" Skipping timer test in windowed mode") print(" Skipping timer test in windowed mode")
# Clean up # Clean up
mcrfpy.delTimer("test_timer") test_timer.stop()
print() print()
# Test 6: Error handling - invalid argument type # Test 6: Error handling - invalid argument type

View file

@ -16,7 +16,7 @@ def create_demo():
# Create scene # Create scene
text_demo = mcrfpy.Scene("text_demo") text_demo = mcrfpy.Scene("text_demo")
scene = text_demo.children scene = text_demo.children
# Background # Background
bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600)) bg = mcrfpy.Frame(pos=(0, 0), size=(800, 600))
bg.fill_color = mcrfpy.Color(40, 40, 40, 255) bg.fill_color = mcrfpy.Color(40, 40, 40, 255)
@ -26,55 +26,55 @@ def create_demo():
title = mcrfpy.Caption(pos=(20, 20), text="Text Input Widget Demo") title = mcrfpy.Caption(pos=(20, 20), text="Text Input Widget Demo")
title.fill_color = mcrfpy.Color(255, 255, 255, 255) title.fill_color = mcrfpy.Color(255, 255, 255, 255)
scene.append(title) scene.append(title)
# Focus manager # Focus manager
focus_mgr = FocusManager() focus_mgr = FocusManager()
# Create inputs # Create inputs
inputs = [] inputs = []
# Name input # Name input
name_input = TextInput(50, 100, 300, label="Name:", placeholder="Enter your name") name_input = TextInput(50, 100, 300, label="Name:", placeholder="Enter your name")
name_input._focus_manager = focus_mgr name_input._focus_manager = focus_mgr
focus_mgr.register(name_input) focus_mgr.register(name_input)
name_input.add_to_scene(scene) name_input.add_to_scene(scene)
inputs.append(name_input) inputs.append(name_input)
# Email input # Email input
email_input = TextInput(50, 160, 300, label="Email:", placeholder="user@example.com") email_input = TextInput(50, 160, 300, label="Email:", placeholder="user@example.com")
email_input._focus_manager = focus_mgr email_input._focus_manager = focus_mgr
focus_mgr.register(email_input) focus_mgr.register(email_input)
email_input.add_to_scene(scene) email_input.add_to_scene(scene)
inputs.append(email_input) inputs.append(email_input)
# Tags input # Tags input
tags_input = TextInput(50, 220, 400, label="Tags:", placeholder="comma, separated, tags") tags_input = TextInput(50, 220, 400, label="Tags:", placeholder="comma, separated, tags")
tags_input._focus_manager = focus_mgr tags_input._focus_manager = focus_mgr
focus_mgr.register(tags_input) focus_mgr.register(tags_input)
tags_input.add_to_scene(scene) tags_input.add_to_scene(scene)
inputs.append(tags_input) inputs.append(tags_input)
# Comment input # Comment input
comment_input = TextInput(50, 280, 500, height=30, label="Comment:", placeholder="Add a comment...") comment_input = TextInput(50, 280, 500, height=30, label="Comment:", placeholder="Add a comment...")
comment_input._focus_manager = focus_mgr comment_input._focus_manager = focus_mgr
focus_mgr.register(comment_input) focus_mgr.register(comment_input)
comment_input.add_to_scene(scene) comment_input.add_to_scene(scene)
inputs.append(comment_input) inputs.append(comment_input)
# Status display # Status display
status = mcrfpy.Caption(pos=(50, 360), text="Ready for input...") status = mcrfpy.Caption(pos=(50, 360), text="Ready for input...")
status.fill_color = mcrfpy.Color(150, 255, 150, 255) status.fill_color = mcrfpy.Color(150, 255, 150, 255)
scene.append(status) scene.append(status)
# Update handler # Update handler
def update_status(text=None): def update_status(text=None):
values = [inp.get_text() for inp in inputs] values = [inp.get_text() for inp in inputs]
status.text = f"Data: {values[0]} | {values[1]} | {values[2]} | {values[3]}" status.text = f"Data: {values[0]} | {values[1]} | {values[2]} | {values[3]}"
# Set change handlers # Set change handlers
for inp in inputs: for inp in inputs:
inp.on_change = update_status inp.on_change = update_status
# Keyboard handler # Keyboard handler
def handle_keys(scene_name, key): def handle_keys(scene_name, key):
if not focus_mgr.handle_key(key): if not focus_mgr.handle_key(key):
@ -85,12 +85,12 @@ def create_demo():
for i, inp in enumerate(inputs): for i, inp in enumerate(inputs):
print(f" Field {i+1}: '{inp.get_text()}'") print(f" Field {i+1}: '{inp.get_text()}'")
sys.exit(0) sys.exit(0)
text_demo.on_key = "text_demo", handle_keys text_demo.on_key = "text_demo", handle_keys
text_demo.activate() text_demo.activate()
# Run demo test # Run demo test
def run_test(timer_name): def run_test(timer, runtime):
print("\n=== Text Input Widget Test ===") print("\n=== Text Input Widget Test ===")
print("Features:") print("Features:")
print("- Click to focus fields") print("- Click to focus fields")
@ -102,9 +102,9 @@ def create_demo():
print("- Visual focus indication") print("- Visual focus indication")
print("- Press Escape to exit") print("- Press Escape to exit")
print("\nTry it out!") print("\nTry it out!")
mcrfpy.setTimer("info", run_test, 100) info_timer = mcrfpy.Timer("info", run_test, 100, once=True)
if __name__ == "__main__": if __name__ == "__main__":
create_demo() create_demo()

View file

@ -1,34 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Test timer callback arguments Test timer callback arguments with new Timer API (#173)
""" """
import mcrfpy import mcrfpy
import sys import sys
call_count = 0 call_count = 0
def old_style_callback(arg): def new_style_callback(timer, runtime):
"""Old style callback - should receive just runtime""" """New style callback - receives timer object and runtime"""
global call_count global call_count
call_count += 1 call_count += 1
print(f"Old style callback called with: {arg} (type: {type(arg)})") print(f"Callback called with: timer={timer} (type: {type(timer)}), runtime={runtime} (type: {type(runtime)})")
if hasattr(timer, 'once'):
print(f"Got Timer object! once={timer.once}")
if call_count >= 2: if call_count >= 2:
print("PASS")
sys.exit(0) sys.exit(0)
def new_style_callback(arg1, arg2=None):
"""New style callback - should receive timer object and runtime"""
print(f"New style callback called with: arg1={arg1} (type: {type(arg1)}), arg2={arg2} (type: {type(arg2) if arg2 else 'None'})")
if hasattr(arg1, 'once'):
print(f"Got Timer object! once={arg1.once}")
sys.exit(0)
# Set up the scene # Set up the scene
test_scene = mcrfpy.Scene("test_scene") test_scene = mcrfpy.Scene("test_scene")
test_scene.activate() test_scene.activate()
print("Testing old style timer with setTimer...") print("Testing new Timer callback signature (timer, runtime)...")
mcrfpy.setTimer("old_timer", old_style_callback, 100) timer = mcrfpy.Timer("test_timer", new_style_callback, 100)
print(f"Timer created: {timer}")
print("\nTesting new style timer with Timer object...")
timer = mcrfpy.Timer("new_timer", new_style_callback, 200)
print(f"Timer created: {timer}")

View file

@ -1,26 +1,28 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Test legacy timer API still works Test Timer API works correctly (#173)
Replaces old legacy setTimer test
""" """
import mcrfpy import mcrfpy
import sys import sys
count = 0 count = 0
def timer_callback(runtime): def timer_callback(timer, runtime):
global count global count
count += 1 count += 1
print(f"Timer fired! Count: {count}, Runtime: {runtime}") print(f"Timer fired! Count: {count}, Runtime: {runtime}")
if count >= 3: if count >= 3:
print("Test passed - timer fired 3 times") print("Test passed - timer fired 3 times")
print("PASS")
sys.exit(0) sys.exit(0)
# Set up the scene # Set up the scene
test_scene = mcrfpy.Scene("test_scene") test_scene = mcrfpy.Scene("test_scene")
test_scene.activate() test_scene.activate()
# Create a timer the old way # Create a timer with new API
mcrfpy.setTimer("test_timer", timer_callback, 100) timer = mcrfpy.Timer("test_timer", timer_callback, 100)
print("Legacy timer test starting...") print("Timer test starting...")

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Test the new mcrfpy.Timer object with pause/resume/cancel functionality Test the new mcrfpy.Timer object with pause/resume/stop functionality
Updated for new Timer API (#173)
""" """
import mcrfpy import mcrfpy
import sys import sys
@ -25,13 +26,13 @@ def cancel_test_callback(timer, runtime):
cancel_test_count += 1 cancel_test_count += 1
print(f"Cancel test timer: {cancel_test_count} - This should only print once!") print(f"Cancel test timer: {cancel_test_count} - This should only print once!")
def run_tests(runtime): def run_tests(timer, runtime):
"""Main test function that runs after game loop starts""" """Main test function that runs after game loop starts"""
# Delete the timer that called us to prevent re-running # Stop the timer that called us to prevent re-running
mcrfpy.delTimer("run_tests") timer.stop()
print("\n=== Testing mcrfpy.Timer object ===\n") print("\n=== Testing mcrfpy.Timer object ===\n")
# Test 1: Create a basic timer # Test 1: Create a basic timer
print("Test 1: Creating Timer object") print("Test 1: Creating Timer object")
timer1 = mcrfpy.Timer("test_timer", timer_callback, 500) timer1 = mcrfpy.Timer("test_timer", timer_callback, 500)
@ -39,103 +40,102 @@ def run_tests(runtime):
print(f" Interval: {timer1.interval}ms") print(f" Interval: {timer1.interval}ms")
print(f" Active: {timer1.active}") print(f" Active: {timer1.active}")
print(f" Paused: {timer1.paused}") print(f" Paused: {timer1.paused}")
# Test 2: Test pause/resume # Test 2: Test pause/resume
print("\nTest 2: Testing pause/resume functionality") print("\nTest 2: Testing pause/resume functionality")
timer2 = mcrfpy.Timer("pause_test", pause_test_callback, 200) timer2 = mcrfpy.Timer("pause_test", pause_test_callback, 200)
# Schedule pause after 250ms # Schedule pause after 250ms
def pause_timer2(runtime): def pause_timer2(t, rt):
print(" Pausing timer2...") print(" Pausing timer2...")
timer2.pause() timer2.pause()
print(f" Timer2 paused: {timer2.paused}") print(f" Timer2 paused: {timer2.paused}")
print(f" Timer2 active: {timer2.active}") print(f" Timer2 active: {timer2.active}")
# Schedule resume after another 400ms # Schedule resume after another 400ms
def resume_timer2(runtime): def resume_timer2(t2, rt2):
print(" Resuming timer2...") print(" Resuming timer2...")
timer2.resume() timer2.resume()
print(f" Timer2 paused: {timer2.paused}") print(f" Timer2 paused: {timer2.paused}")
print(f" Timer2 active: {timer2.active}") print(f" Timer2 active: {timer2.active}")
mcrfpy.setTimer("resume_timer2", resume_timer2, 400) mcrfpy.Timer("resume_timer2", resume_timer2, 400, once=True)
mcrfpy.setTimer("pause_timer2", pause_timer2, 250) mcrfpy.Timer("pause_timer2", pause_timer2, 250, once=True)
# Test 3: Test cancel # Test 3: Test cancel/stop
print("\nTest 3: Testing cancel functionality") print("\nTest 3: Testing stop functionality")
timer3 = mcrfpy.Timer("cancel_test", cancel_test_callback, 300) timer3 = mcrfpy.Timer("cancel_test", cancel_test_callback, 300)
# Cancel after 350ms (should fire once) # Cancel after 350ms (should fire once)
def cancel_timer3(runtime): def cancel_timer3(t, rt):
mcrfpy.delTimer("cancel_timer3") # Make this a one-shot timer print(" Stopping timer3...")
print(" Canceling timer3...") timer3.stop()
timer3.cancel() print(" Timer3 stopped")
print(" Timer3 canceled")
mcrfpy.Timer("cancel_timer3", cancel_timer3, 350, once=True)
mcrfpy.setTimer("cancel_timer3", cancel_timer3, 350)
# Test 4: Test interval modification # Test 4: Test interval modification
print("\nTest 4: Testing interval modification") print("\nTest 4: Testing interval modification")
def interval_test(timer, runtime): def interval_test(timer, runtime):
print(f" Interval test fired at {runtime}ms") print(f" Interval test fired at {runtime}ms")
timer4 = mcrfpy.Timer("interval_test", interval_test, 1000) timer4 = mcrfpy.Timer("interval_test", interval_test, 1000)
print(f" Original interval: {timer4.interval}ms") print(f" Original interval: {timer4.interval}ms")
timer4.interval = 500 timer4.interval = 500
print(f" Modified interval: {timer4.interval}ms") print(f" Modified interval: {timer4.interval}ms")
# Test 5: Test remaining time # Test 5: Test remaining time
print("\nTest 5: Testing remaining time") print("\nTest 5: Testing remaining time")
def check_remaining(runtime): def check_remaining(t, rt):
if timer1.active: if timer1.active:
print(f" Timer1 remaining: {timer1.remaining}ms") print(f" Timer1 remaining: {timer1.remaining}ms")
if timer2.active or timer2.paused: if timer2.active or timer2.paused:
print(f" Timer2 remaining: {timer2.remaining}ms (paused: {timer2.paused})") print(f" Timer2 remaining: {timer2.remaining}ms (paused: {timer2.paused})")
mcrfpy.setTimer("check_remaining", check_remaining, 150) remaining_timer = mcrfpy.Timer("check_remaining", check_remaining, 150)
# Test 6: Test restart # Test 6: Test restart
print("\nTest 6: Testing restart functionality") print("\nTest 6: Testing restart functionality")
restart_count = [0] restart_count = [0]
def restart_test(timer, runtime): def restart_test(timer, runtime):
restart_count[0] += 1 restart_count[0] += 1
print(f" Restart test: {restart_count[0]}") print(f" Restart test: {restart_count[0]}")
if restart_count[0] == 2: if restart_count[0] == 2:
print(" Restarting timer...") print(" Restarting timer...")
timer.restart() timer.restart()
timer5 = mcrfpy.Timer("restart_test", restart_test, 400) timer5 = mcrfpy.Timer("restart_test", restart_test, 400)
# Final verification after 2 seconds # Final verification after 2 seconds
def final_check(runtime): def final_check(t, rt):
print("\n=== Final Results ===") print("\n=== Final Results ===")
print(f"Timer1 call count: {call_count} (expected: ~4)") print(f"Timer1 call count: {call_count} (expected: ~4)")
print(f"Pause test count: {pause_test_count} (expected: ~6-7, with pause gap)") print(f"Pause test count: {pause_test_count} (expected: ~6-7, with pause gap)")
print(f"Cancel test count: {cancel_test_count} (expected: 1)") print(f"Cancel test count: {cancel_test_count} (expected: 1)")
print(f"Restart test count: {restart_count[0]} (expected: ~5 with restart)") print(f"Restart test count: {restart_count[0]} (expected: ~5 with restart)")
# Verify timer states # Verify timer states
try: try:
print(f"\nTimer1 active: {timer1.active}") print(f"\nTimer1 active: {timer1.active}")
print(f"Timer2 active: {timer2.active}") print(f"Timer2 active: {timer2.active}")
print(f"Timer3 active: {timer3.active} (should be False after cancel)") print(f"Timer3 active: {timer3.active} (should be False after stop)")
print(f"Timer4 active: {timer4.active}") print(f"Timer4 active: {timer4.active}")
print(f"Timer5 active: {timer5.active}") print(f"Timer5 active: {timer5.active}")
except: except:
print("Some timers may have been garbage collected") print("Some timers may have been garbage collected")
print("\n✓ All Timer object tests completed!") print("\n✓ All Timer object tests completed!")
sys.exit(0) sys.exit(0)
mcrfpy.setTimer("final_check", final_check, 2000) mcrfpy.Timer("final_check", final_check, 2000, once=True)
# Create a minimal scene # Create a minimal scene
timer_test = mcrfpy.Scene("timer_test") timer_test = mcrfpy.Scene("timer_test")
timer_test.activate() timer_test.activate()
# Start tests after game loop begins # Start tests after game loop begins
mcrfpy.setTimer("run_tests", run_tests, 100) mcrfpy.Timer("run_tests", run_tests, 100, once=True)
print("Timer object tests starting...") print("Timer object tests starting...")

View file

@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Test once=True timer functionality Test once=True timer functionality
Uses mcrfpy.step() to advance time in headless mode.
""" """
import mcrfpy import mcrfpy
import sys import sys
@ -18,20 +19,8 @@ def repeat_callback(timer, runtime):
repeat_count += 1 repeat_count += 1
print(f"Repeat timer fired! Count: {repeat_count}, Timer.once: {timer.once}") print(f"Repeat timer fired! Count: {repeat_count}, Timer.once: {timer.once}")
def check_results(runtime):
print(f"\nFinal results:")
print(f"Once timer fired {once_count} times (expected: 1)")
print(f"Repeat timer fired {repeat_count} times (expected: 3+)")
if once_count == 1 and repeat_count >= 3:
print("PASS: Once timer fired exactly once, repeat timer fired multiple times")
sys.exit(0)
else:
print("FAIL: Timer behavior incorrect")
sys.exit(1)
# Set up the scene # Set up the scene
test_scene = mcrfpy.Scene("test_scene") test_scene = mcrfpy.Scene("test_scene")
test_scene.activate() test_scene.activate()
# Create timers # Create timers
@ -43,5 +32,20 @@ print("\nCreating repeat timer with once=False (default)...")
repeat_timer = mcrfpy.Timer("repeat_timer", repeat_callback, 100) repeat_timer = mcrfpy.Timer("repeat_timer", repeat_callback, 100)
print(f"Timer: {repeat_timer}, once={repeat_timer.once}") print(f"Timer: {repeat_timer}, once={repeat_timer.once}")
# Check results after 500ms # Advance time using step() to let timers fire
mcrfpy.setTimer("check", check_results, 500) # Step 600ms total - once timer (100ms) fires once, repeat timer fires ~6 times
print("\nAdvancing time with step()...")
for i in range(6):
mcrfpy.step(0.1) # 100ms each
# Check results
print(f"\nFinal results:")
print(f"Once timer fired {once_count} times (expected: 1)")
print(f"Repeat timer fired {repeat_count} times (expected: 3+)")
if once_count == 1 and repeat_count >= 3:
print("PASS: Once timer fired exactly once, repeat timer fired multiple times")
sys.exit(0)
else:
print("FAIL: Timer behavior incorrect")
sys.exit(1)

View file

@ -4,18 +4,18 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def take_screenshot(runtime): def take_screenshot(timer, runtime):
"""Take screenshot after render completes""" """Take screenshot after render completes"""
mcrfpy.delTimer("screenshot") timer.stop()
automation.screenshot("test_uiarc_result.png") automation.screenshot("test_uiarc_result.png")
print("Screenshot saved to test_uiarc_result.png") print("Screenshot saved to test_uiarc_result.png")
print("PASS - UIArc test completed") print("PASS - UIArc test completed")
sys.exit(0) sys.exit(0)
def run_test(runtime): def run_test(timer, runtime):
"""Main test - runs after scene is set up""" """Main test - runs after scene is set up"""
mcrfpy.delTimer("test") timer.stop()
# Get the scene UI # Get the scene UI
ui = test.children ui = test.children
@ -127,11 +127,12 @@ def run_test(runtime):
print(f" Arc 10 (reverse): {a10}") print(f" Arc 10 (reverse): {a10}")
# Schedule screenshot for next frame # Schedule screenshot for next frame
mcrfpy.setTimer("screenshot", take_screenshot, 50) global screenshot_timer
screenshot_timer = mcrfpy.Timer("screenshot", take_screenshot, 50, once=True)
# Create a test scene # Create a test scene
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 50) test_timer = mcrfpy.Timer("test", run_test, 50, once=True)

View file

@ -6,7 +6,7 @@ from mcrfpy import automation
import sys import sys
import time import time
def run_visual_test(runtime): def run_visual_test(timer, runtime):
"""Timer callback to run visual tests and take screenshots.""" """Timer callback to run visual tests and take screenshots."""
print("\nRunning visual tests...") print("\nRunning visual tests...")
@ -89,9 +89,9 @@ def main():
ui.append(frame) ui.append(frame)
print("Scene setup complete. Scheduling visual tests...") print("Scene setup complete. Scheduling visual tests...")
# Schedule visual test to run after render loop starts # Schedule visual test to run after render loop starts
mcrfpy.setTimer("visual_test", run_visual_test, 100) visual_test_timer = mcrfpy.Timer("visual_test", run_visual_test, 100, once=True)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View file

@ -4,18 +4,18 @@ import mcrfpy
from mcrfpy import automation from mcrfpy import automation
import sys import sys
def take_screenshot(runtime): def take_screenshot(timer, runtime):
"""Take screenshot after render completes""" """Take screenshot after render completes"""
mcrfpy.delTimer("screenshot") timer.stop()
automation.screenshot("test_uicircle_result.png") automation.screenshot("test_uicircle_result.png")
print("Screenshot saved to test_uicircle_result.png") print("Screenshot saved to test_uicircle_result.png")
print("PASS - UICircle test completed") print("PASS - UICircle test completed")
sys.exit(0) sys.exit(0)
def run_test(runtime): def run_test(timer, runtime):
"""Main test - runs after scene is set up""" """Main test - runs after scene is set up"""
mcrfpy.delTimer("test") timer.stop()
# Get the scene UI # Get the scene UI
ui = test.children ui = test.children
@ -118,11 +118,12 @@ def run_test(runtime):
print(f" c1 moved from {old_center} to {new_center}") print(f" c1 moved from {old_center} to {new_center}")
# Schedule screenshot for next frame # Schedule screenshot for next frame
mcrfpy.setTimer("screenshot", take_screenshot, 50) global screenshot_timer
screenshot_timer = mcrfpy.Timer("screenshot", take_screenshot, 50, once=True)
# Create a test scene # Create a test scene
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
test.activate() test.activate()
# Schedule test to run after game loop starts # Schedule test to run after game loop starts
mcrfpy.setTimer("test", run_test, 50) test_timer = mcrfpy.Timer("test", run_test, 50, once=True)

View file

@ -6,7 +6,7 @@ Test UTF-8 encoding support
import mcrfpy import mcrfpy
import sys import sys
def test_utf8(runtime): def test_utf8(timer, runtime):
"""Test UTF-8 encoding in print statements""" """Test UTF-8 encoding in print statements"""
# Test various unicode characters # Test various unicode characters
@ -32,4 +32,4 @@ def test_utf8(runtime):
# Run test # Run test
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
mcrfpy.setTimer("test", test_utf8, 100) test_timer = mcrfpy.Timer("test", test_utf8, 100, once=True)

View file

@ -7,7 +7,7 @@ import mcrfpy
import sys import sys
import math import math
def test_vector_arithmetic(runtime): def test_vector_arithmetic(timer, runtime):
"""Test vector arithmetic operations""" """Test vector arithmetic operations"""
all_pass = True all_pass = True
@ -244,4 +244,4 @@ def test_vector_arithmetic(runtime):
# Run test # Run test
test = mcrfpy.Scene("test") test = mcrfpy.Scene("test")
mcrfpy.setTimer("test", test_vector_arithmetic, 100) test_timer = mcrfpy.Timer("test", test_vector_arithmetic, 100, once=True)

View file

@ -5,9 +5,9 @@ import mcrfpy
from mcrfpy import Window, Frame, Caption, Color, Vector from mcrfpy import Window, Frame, Caption, Color, Vector
import sys import sys
def test_viewport_modes(runtime): def test_viewport_modes(timer, runtime):
"""Test all three viewport scaling modes""" """Test all three viewport scaling modes"""
mcrfpy.delTimer("test_viewport") timer.stop()
print("Testing viewport scaling modes...") print("Testing viewport scaling modes...")
@ -82,47 +82,47 @@ def test_viewport_modes(runtime):
scene.append(instructions) scene.append(instructions)
# Test changing modes # Test changing modes
def test_mode_changes(runtime): def test_mode_changes(t, r):
mcrfpy.delTimer("test_modes") t.stop()
from mcrfpy import automation from mcrfpy import automation
print("\nTesting scaling modes:") print("\nTesting scaling modes:")
# Test center mode # Test center mode
window.scaling_mode = "center" window.scaling_mode = "center"
print(f"Set to center mode: {window.scaling_mode}") print(f"Set to center mode: {window.scaling_mode}")
mode_text.text = f"Mode: center (1:1 pixels)" mode_text.text = f"Mode: center (1:1 pixels)"
automation.screenshot("viewport_center_mode.png") automation.screenshot("viewport_center_mode.png")
# Schedule next mode test # Schedule next mode test
mcrfpy.setTimer("test_stretch", test_stretch_mode, 1000) mcrfpy.Timer("test_stretch", test_stretch_mode, 1000, once=True)
def test_stretch_mode(runtime): def test_stretch_mode(t, r):
mcrfpy.delTimer("test_stretch") t.stop()
from mcrfpy import automation from mcrfpy import automation
window.scaling_mode = "stretch" window.scaling_mode = "stretch"
print(f"Set to stretch mode: {window.scaling_mode}") print(f"Set to stretch mode: {window.scaling_mode}")
mode_text.text = f"Mode: stretch (fill window)" mode_text.text = f"Mode: stretch (fill window)"
automation.screenshot("viewport_stretch_mode.png") automation.screenshot("viewport_stretch_mode.png")
# Schedule next mode test # Schedule next mode test
mcrfpy.setTimer("test_fit", test_fit_mode, 1000) mcrfpy.Timer("test_fit", test_fit_mode, 1000, once=True)
def test_fit_mode(runtime): def test_fit_mode(t, r):
mcrfpy.delTimer("test_fit") t.stop()
from mcrfpy import automation from mcrfpy import automation
window.scaling_mode = "fit" window.scaling_mode = "fit"
print(f"Set to fit mode: {window.scaling_mode}") print(f"Set to fit mode: {window.scaling_mode}")
mode_text.text = f"Mode: fit (aspect ratio maintained)" mode_text.text = f"Mode: fit (aspect ratio maintained)"
automation.screenshot("viewport_fit_mode.png") automation.screenshot("viewport_fit_mode.png")
# Test different window sizes # Test different window sizes
mcrfpy.setTimer("test_resize", test_window_resize, 1000) mcrfpy.Timer("test_resize", test_window_resize, 1000, once=True)
def test_window_resize(runtime): def test_window_resize(t, r):
mcrfpy.delTimer("test_resize") t.stop()
from mcrfpy import automation from mcrfpy import automation
print("\nTesting window resize with fit mode:") print("\nTesting window resize with fit mode:")
@ -133,13 +133,13 @@ def test_viewport_modes(runtime):
print(f"Window resized to: {window.resolution}") print(f"Window resized to: {window.resolution}")
automation.screenshot("viewport_fit_wide.png") automation.screenshot("viewport_fit_wide.png")
# Make window taller # Make window taller
mcrfpy.setTimer("test_tall", test_tall_window, 1000) mcrfpy.Timer("test_tall", test_tall_window, 1000, once=True)
except RuntimeError as e: except RuntimeError as e:
print(f" Skipping window resize tests (headless mode): {e}") print(f" Skipping window resize tests (headless mode): {e}")
mcrfpy.setTimer("test_game_res", test_game_resolution, 100) mcrfpy.Timer("test_game_res", test_game_resolution, 100, once=True)
def test_tall_window(runtime): def test_tall_window(t, r):
mcrfpy.delTimer("test_tall") t.stop()
from mcrfpy import automation from mcrfpy import automation
try: try:
@ -150,10 +150,10 @@ def test_viewport_modes(runtime):
print(f" Skipping tall window test (headless mode): {e}") print(f" Skipping tall window test (headless mode): {e}")
# Test game resolution change # Test game resolution change
mcrfpy.setTimer("test_game_res", test_game_resolution, 1000) mcrfpy.Timer("test_game_res", test_game_resolution, 1000, once=True)
def test_game_resolution(runtime): def test_game_resolution(t, r):
mcrfpy.delTimer("test_game_res") t.stop()
print("\nTesting game resolution change:") print("\nTesting game resolution change:")
window.game_resolution = (800, 600) window.game_resolution = (800, 600)
@ -178,9 +178,9 @@ def test_viewport_modes(runtime):
window.scaling_mode = "fit" window.scaling_mode = "fit"
sys.exit(0) sys.exit(0)
# Start test sequence # Start test sequence
mcrfpy.setTimer("test_modes", test_mode_changes, 500) mcrfpy.Timer("test_modes", test_mode_changes, 500, once=True)
# Set up keyboard handler for manual testing # Set up keyboard handler for manual testing
def handle_keypress(key, state): def handle_keypress(key, state):
@ -240,7 +240,7 @@ test.activate()
test.on_key = handle_keypress test.on_key = handle_keypress
# Schedule the test # Schedule the test
mcrfpy.setTimer("test_viewport", test_viewport_modes, 100) test_viewport_timer = mcrfpy.Timer("test_viewport", test_viewport_modes, 100, once=True)
print("Viewport test running...") print("Viewport test running...")
print("Use number keys to switch modes, R to resize window, G to change game resolution") print("Use number keys to switch modes, R to resize window, G to change game resolution")

Some files were not shown because too many files have changed in this diff Show more