diff --git a/CMakeLists.txt b/CMakeLists.txt index 70a8d4d..40e4ff0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,9 +17,6 @@ option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF) # Playground mode - minimal scripts for web playground (REPL-focused) option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF) -# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games) -option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF) - # Emscripten builds: use SDL2 if specified, otherwise fall back to headless if(EMSCRIPTEN) if(MCRF_SDL2) @@ -289,8 +286,8 @@ if(EMSCRIPTEN) --preload-file=${CMAKE_SOURCE_DIR}/src/$,scripts_playground,scripts>@/scripts # Preload assets --preload-file=${CMAKE_SOURCE_DIR}/assets@/assets - # Use custom HTML shell - game shell (fullscreen) or playground shell (REPL) - --shell-file=${CMAKE_SOURCE_DIR}/src/$,shell_game.html,shell.html> + # Use custom HTML shell for crisp pixel rendering + --shell-file=${CMAKE_SOURCE_DIR}/src/shell.html # Pre-JS to fix browser zoom causing undefined values in events --pre-js=${CMAKE_SOURCE_DIR}/src/emscripten_pre.js ) @@ -299,19 +296,17 @@ if(EMSCRIPTEN) if(MCRF_SDL2) list(APPEND EMSCRIPTEN_LINK_OPTIONS -sUSE_SDL=2 - -sUSE_SDL_MIXER=2 -sFULL_ES2=1 -sMIN_WEBGL_VERSION=2 -sMAX_WEBGL_VERSION=2 -sUSE_FREETYPE=1 ) - # SDL2, SDL2_mixer, and FreeType flags are also needed at compile time for headers + # SDL2 and FreeType flags are also needed at compile time for headers target_compile_options(mcrogueface PRIVATE -sUSE_SDL=2 - -sUSE_SDL_MIXER=2 -sUSE_FREETYPE=1 ) - message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sUSE_SDL_MIXER=2 -sFULL_ES2=1 -sUSE_FREETYPE=1") + message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sFULL_ES2=1 -sUSE_FREETYPE=1") endif() target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS}) diff --git a/src/3d/Entity3D.cpp b/src/3d/Entity3D.cpp index 903dc58..fe15720 100644 --- a/src/3d/Entity3D.cpp +++ b/src/3d/Entity3D.cpp @@ -8,12 +8,7 @@ #include "PyVector.h" #include "PyColor.h" #include "PythonObjectCache.h" -#include "Animation.h" -#include "PyAnimation.h" -#include "PyEasing.h" -#include "McRFPy_API.h" #include -#include // Include appropriate GL headers based on backend #if defined(MCRF_SDL2) @@ -744,21 +739,6 @@ PyObject* Entity3D::repr(PyEntity3DObject* self) // Property getters/setters -PyObject* Entity3D::get_name(PyEntity3DObject* self, void* closure) -{ - return PyUnicode_FromString(self->data->getName().c_str()); -} - -int Entity3D::set_name(PyEntity3DObject* self, PyObject* value, void* closure) -{ - if (!PyUnicode_Check(value)) { - PyErr_SetString(PyExc_TypeError, "name must be a string"); - return -1; - } - self->data->setName(PyUnicode_AsUTF8(value)); - return 0; -} - PyObject* Entity3D::get_pos(PyEntity3DObject* self, void* closure) { return Py_BuildValue("(ii)", self->data->grid_x_, self->data->grid_z_); @@ -861,15 +841,9 @@ PyObject* Entity3D::get_viewport(PyEntity3DObject* self, void* closure) if (!vp) { Py_RETURN_NONE; } - - PyTypeObject* type = &mcrfpydef::PyViewport3DType; - PyViewport3DObject* obj = (PyViewport3DObject*)type->tp_alloc(type, 0); - if (!obj) return NULL; - - obj->data = vp; - obj->weakreflist = nullptr; - - return (PyObject*)obj; + // TODO: Return actual viewport Python object + // For now, return None + Py_RETURN_NONE; } PyObject* Entity3D::get_model(PyEntity3DObject* self, void* closure) @@ -1141,110 +1115,10 @@ PyObject* Entity3D::py_update_visibility(PyEntity3DObject* self, PyObject* args) PyObject* Entity3D::py_animate(PyEntity3DObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = {"property", "target", "duration", "easing", "delta", "callback", "conflict_mode", nullptr}; - - const char* property_name; - PyObject* target_value; - float duration; - PyObject* easing_arg = Py_None; - int delta = 0; - PyObject* callback = nullptr; - const char* conflict_mode_str = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|OpOs", const_cast(keywords), - &property_name, &target_value, &duration, - &easing_arg, &delta, &callback, &conflict_mode_str)) { - return NULL; - } - - // Validate property exists on this entity - if (!self->data->hasProperty(property_name)) { - PyErr_Format(PyExc_ValueError, - "Property '%s' is not valid for animation on Entity3D. " - "Valid properties: x, y, z, world_x, world_y, world_z, rotation, rot_y, " - "scale, scale_x, scale_y, scale_z, sprite_index, visible", - property_name); - return NULL; - } - - // Validate callback is callable if provided - if (callback && callback != Py_None && !PyCallable_Check(callback)) { - PyErr_SetString(PyExc_TypeError, "callback must be callable"); - return NULL; - } - - // Convert None to nullptr for C++ - if (callback == Py_None) { - callback = nullptr; - } - - // Convert Python target value to AnimationValue - // Entity3D only supports float and int properties - AnimationValue animValue; - - if (PyFloat_Check(target_value)) { - animValue = static_cast(PyFloat_AsDouble(target_value)); - } - else if (PyLong_Check(target_value)) { - animValue = static_cast(PyLong_AsLong(target_value)); - } - else { - PyErr_SetString(PyExc_TypeError, "Entity3D animations only support float or int target values"); - return NULL; - } - - // Get easing function from argument - EasingFunction easingFunc; - if (!PyEasing::from_arg(easing_arg, &easingFunc, nullptr)) { - return NULL; // Error already set by from_arg - } - - // Parse conflict mode - AnimationConflictMode conflict_mode = AnimationConflictMode::REPLACE; - if (conflict_mode_str) { - if (strcmp(conflict_mode_str, "replace") == 0) { - conflict_mode = AnimationConflictMode::REPLACE; - } else if (strcmp(conflict_mode_str, "queue") == 0) { - conflict_mode = AnimationConflictMode::QUEUE; - } else if (strcmp(conflict_mode_str, "error") == 0) { - conflict_mode = AnimationConflictMode::RAISE_ERROR; - } else { - PyErr_Format(PyExc_ValueError, - "Invalid conflict_mode '%s'. Must be 'replace', 'queue', or 'error'.", conflict_mode_str); - return NULL; - } - } - - // Create the Animation - auto animation = std::make_shared(property_name, animValue, duration, easingFunc, delta != 0, callback); - - // Start on this entity (uses startEntity3D) - animation->startEntity3D(self->data); - - // Add to AnimationManager - AnimationManager::getInstance().addAnimation(animation, conflict_mode); - - // Check if ERROR mode raised an exception - if (PyErr_Occurred()) { - return NULL; - } - - // Create and return a PyAnimation wrapper - PyTypeObject* animType = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Animation"); - if (!animType) { - PyErr_SetString(PyExc_RuntimeError, "Could not find Animation type"); - return NULL; - } - - PyAnimationObject* pyAnim = (PyAnimationObject*)animType->tp_alloc(animType, 0); - Py_DECREF(animType); - - if (!pyAnim) { - return NULL; - } - - pyAnim->data = animation; - return (PyObject*)pyAnim; + // TODO: Implement animation shorthand similar to UIEntity + // For now, return None + PyErr_SetString(PyExc_NotImplementedError, "Entity3D.animate() not yet implemented"); + return NULL; } PyObject* Entity3D::py_follow_path(PyEntity3DObject* self, PyObject* args) @@ -1306,16 +1180,8 @@ PyMethodDef Entity3D::methods[] = { "update_visibility()\n\n" "Recompute field of view from current position."}, {"animate", (PyCFunction)Entity3D::py_animate, METH_VARARGS | METH_KEYWORDS, - "animate(property, target, duration, easing=None, delta=False, callback=None, conflict_mode=None)\n\n" - "Animate a property over time.\n\n" - "Args:\n" - " property: Property name (x, y, z, rotation, scale, etc.)\n" - " target: Target value (float or int)\n" - " duration: Animation duration in seconds\n" - " easing: Easing function (Easing enum or None for linear)\n" - " delta: If True, target is relative to current value\n" - " callback: Called with (target, property, value) when complete\n" - " conflict_mode: 'replace', 'queue', or 'error'"}, + "animate(property, target, duration, easing=None, callback=None)\n\n" + "Animate a property over time. (Not yet implemented)"}, {"follow_path", (PyCFunction)Entity3D::py_follow_path, METH_VARARGS, "follow_path(path)\n\n" "Queue path positions for smooth movement.\n\n" @@ -1328,8 +1194,6 @@ PyMethodDef Entity3D::methods[] = { }; PyGetSetDef Entity3D::getsetters[] = { - {"name", (getter)Entity3D::get_name, (setter)Entity3D::set_name, - "Entity name (str). Used for find() lookup.", NULL}, {"pos", (getter)Entity3D::get_pos, (setter)Entity3D::set_pos, "Grid position (x, z). Setting triggers smooth movement.", NULL}, {"grid_pos", (getter)Entity3D::get_grid_pos, (setter)Entity3D::set_grid_pos, diff --git a/src/3d/Entity3D.h b/src/3d/Entity3D.h index f089e2c..ce1d69a 100644 --- a/src/3d/Entity3D.h +++ b/src/3d/Entity3D.h @@ -88,10 +88,6 @@ public: bool isVisible() const { return visible_; } void setVisible(bool v) { visible_ = v; } - // Name (for find() lookup) - const std::string& getName() const { return name_; } - void setName(const std::string& n) { name_ = n; } - // Color for placeholder cube rendering sf::Color getColor() const { return color_; } void setColor(const sf::Color& c) { color_ = c; } @@ -232,8 +228,6 @@ public: static PyObject* repr(PyEntity3DObject* self); // Property getters/setters - static PyObject* get_name(PyEntity3DObject* self, void* closure); - static int set_name(PyEntity3DObject* self, PyObject* value, void* closure); static PyObject* get_pos(PyEntity3DObject* self, void* closure); static int set_pos(PyEntity3DObject* self, PyObject* value, void* closure); static PyObject* get_world_pos(PyEntity3DObject* self, void* closure); @@ -303,7 +297,6 @@ private: vec3 scale_ = vec3(1.0f, 1.0f, 1.0f); // Appearance - std::string name_; // For find() lookup bool visible_ = true; sf::Color color_ = sf::Color(200, 100, 50); // Default orange int sprite_index_ = 0; diff --git a/src/3d/EntityCollection3D.cpp b/src/3d/EntityCollection3D.cpp index f38ad04..08eef9d 100644 --- a/src/3d/EntityCollection3D.cpp +++ b/src/3d/EntityCollection3D.cpp @@ -196,125 +196,6 @@ PyObject* EntityCollection3D::clear(PyEntityCollection3DObject* self, PyObject* Py_RETURN_NONE; } -PyObject* EntityCollection3D::pop(PyEntityCollection3DObject* self, PyObject* args, PyObject* kwds) -{ - static const char* kwlist[] = {"index", NULL}; - Py_ssize_t index = -1; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|n", const_cast(kwlist), &index)) { - return NULL; - } - - if (!self->data || self->data->empty()) { - PyErr_SetString(PyExc_IndexError, "pop from empty EntityCollection3D"); - return NULL; - } - - Py_ssize_t size = static_cast(self->data->size()); - if (index < 0) index += size; - if (index < 0 || index >= size) { - PyErr_SetString(PyExc_IndexError, "EntityCollection3D pop index out of range"); - return NULL; - } - - // Iterate to the index - auto it = self->data->begin(); - std::advance(it, index); - - auto entity = *it; - - // Clear viewport reference before removing - entity->setViewport(nullptr); - self->data->erase(it); - - // Create Python wrapper for the removed entity - auto type = &mcrfpydef::PyEntity3DType; - auto obj = (PyEntity3DObject*)type->tp_alloc(type, 0); - if (!obj) return NULL; - - new (&obj->data) std::shared_ptr(entity); - obj->weakreflist = nullptr; - - return (PyObject*)obj; -} - -PyObject* EntityCollection3D::extend(PyEntityCollection3DObject* self, PyObject* o) -{ - if (!self->data || !self->viewport) { - PyErr_SetString(PyExc_RuntimeError, "Collection has no data"); - return NULL; - } - - PyObject* iterator = PyObject_GetIter(o); - if (!iterator) { - return NULL; - } - - // First pass: validate all items are Entity3D - std::vector> to_add; - PyObject* item; - while ((item = PyIter_Next(iterator)) != NULL) { - if (!PyObject_IsInstance(item, (PyObject*)&mcrfpydef::PyEntity3DType)) { - Py_DECREF(item); - Py_DECREF(iterator); - PyErr_SetString(PyExc_TypeError, "extend() requires an iterable of Entity3D objects"); - return NULL; - } - auto entity_obj = (PyEntity3DObject*)item; - if (!entity_obj->data) { - Py_DECREF(item); - Py_DECREF(iterator); - PyErr_SetString(PyExc_ValueError, "Entity3D has no data"); - return NULL; - } - to_add.push_back(entity_obj->data); - Py_DECREF(item); - } - Py_DECREF(iterator); - - if (PyErr_Occurred()) { - return NULL; - } - - // Second pass: append all validated entities - for (auto& entity : to_add) { - self->data->push_back(entity); - entity->setViewport(self->viewport); - } - - Py_RETURN_NONE; -} - -PyObject* EntityCollection3D::find(PyEntityCollection3DObject* self, PyObject* args, PyObject* kwds) -{ - static const char* kwlist[] = {"name", NULL}; - const char* name = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast(kwlist), &name)) { - return NULL; - } - - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Collection has no data"); - return NULL; - } - - for (const auto& entity : *self->data) { - if (entity->getName() == name) { - auto type = &mcrfpydef::PyEntity3DType; - auto obj = (PyEntity3DObject*)type->tp_alloc(type, 0); - if (!obj) return NULL; - - new (&obj->data) std::shared_ptr(entity); - obj->weakreflist = nullptr; - - return (PyObject*)obj; - } - } - - Py_RETURN_NONE; -} - PyMethodDef EntityCollection3D::methods[] = { {"append", (PyCFunction)EntityCollection3D::append, METH_O, "append(entity)\n\n" @@ -325,15 +206,6 @@ PyMethodDef EntityCollection3D::methods[] = { {"clear", (PyCFunction)EntityCollection3D::clear, METH_NOARGS, "clear()\n\n" "Remove all entities from the collection."}, - {"pop", (PyCFunction)EntityCollection3D::pop, METH_VARARGS | METH_KEYWORDS, - "pop(index=-1) -> Entity3D\n\n" - "Remove and return Entity3D at index (default: last)."}, - {"extend", (PyCFunction)EntityCollection3D::extend, METH_O, - "extend(iterable)\n\n" - "Add all Entity3D objects from iterable to the collection."}, - {"find", (PyCFunction)EntityCollection3D::find, METH_VARARGS | METH_KEYWORDS, - "find(name) -> Entity3D or None\n\n" - "Find an Entity3D by name. Returns None if not found."}, {NULL} // Sentinel }; diff --git a/src/3d/EntityCollection3D.h b/src/3d/EntityCollection3D.h index b4d415a..f236de2 100644 --- a/src/3d/EntityCollection3D.h +++ b/src/3d/EntityCollection3D.h @@ -43,9 +43,6 @@ public: static PyObject* append(PyEntityCollection3DObject* self, PyObject* o); static PyObject* remove(PyEntityCollection3DObject* self, PyObject* o); static PyObject* clear(PyEntityCollection3DObject* self, PyObject* args); - static PyObject* pop(PyEntityCollection3DObject* self, PyObject* args, PyObject* kwds); - static PyObject* extend(PyEntityCollection3DObject* self, PyObject* o); - static PyObject* find(PyEntityCollection3DObject* self, PyObject* args, PyObject* kwds); static PyMethodDef methods[]; // Python type slots diff --git a/src/3d/Viewport3D.cpp b/src/3d/Viewport3D.cpp index 481111b..5800705 100644 --- a/src/3d/Viewport3D.cpp +++ b/src/3d/Viewport3D.cpp @@ -196,7 +196,7 @@ void Viewport3D::orbitCamera(float angle, float distance, float height) { camera_.setTarget(vec3(0, 0, 0)); } -vec3 Viewport3D::screenToWorld(float screenX, float screenY, float yPlane) { +vec3 Viewport3D::screenToWorld(float screenX, float screenY) { // Convert screen coordinates to normalized device coordinates (-1 to 1) // screenX/Y are relative to the viewport position float ndcX = (2.0f * screenX / size_.x) - 1.0f; @@ -217,10 +217,10 @@ vec3 Viewport3D::screenToWorld(float screenX, float screenY, float yPlane) { vec3 rayDir = vec3(rayWorld4.x, rayWorld4.y, rayWorld4.z).normalized(); vec3 rayOrigin = camera_.getPosition(); - // Intersect with Y=yPlane horizontal plane + // Intersect with Y=0 plane (ground level) // This is a simplification - for hilly terrain, you'd want ray-marching if (std::abs(rayDir.y) > 0.0001f) { - float t = (yPlane - rayOrigin.y) / rayDir.y; + float t = -rayOrigin.y / rayDir.y; if (t > 0) { return rayOrigin + rayDir * t; } @@ -2453,16 +2453,16 @@ static PyObject* Viewport3D_billboard_count(PyViewport3DObject* self, PyObject* // ============================================================================= static PyObject* Viewport3D_screen_to_world(PyViewport3DObject* self, PyObject* args, PyObject* kwds) { - static const char* kwlist[] = {"x", "y", "y_plane", NULL}; + static const char* kwlist[] = {"x", "y", NULL}; - float x = 0.0f, y = 0.0f, y_plane = 0.0f; + float x = 0.0f, y = 0.0f; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "ff|f", const_cast(kwlist), &x, &y, &y_plane)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ff", const_cast(kwlist), &x, &y)) { return NULL; } // Adjust for viewport position (user passes screen coords relative to viewport) - vec3 worldPos = self->data->screenToWorld(x, y, y_plane); + vec3 worldPos = self->data->screenToWorld(x, y); // Return None if no intersection (ray parallel to ground or invalid) if (worldPos.x < 0 && worldPos.y < 0 && worldPos.z < 0) { @@ -2833,14 +2833,13 @@ PyMethodDef Viewport3D_methods[] = { // Camera & Input methods (Milestone 8) {"screen_to_world", (PyCFunction)mcrf::Viewport3D_screen_to_world, METH_VARARGS | METH_KEYWORDS, - "screen_to_world(x, y, y_plane=0.0) -> tuple or None\n\n" + "screen_to_world(x, y) -> tuple or None\n\n" "Convert screen coordinates to world position via ray casting.\n\n" "Args:\n" " x: Screen X coordinate relative to viewport\n" - " y: Screen Y coordinate relative to viewport\n" - " y_plane: Y value of horizontal plane to intersect (default: 0.0)\n\n" + " y: Screen Y coordinate relative to viewport\n\n" "Returns:\n" - " (x, y, z) world position tuple, or None if no intersection with the plane"}, + " (x, y, z) world position tuple, or None if no intersection with ground plane"}, {"follow", (PyCFunction)mcrf::Viewport3D_follow, METH_VARARGS | METH_KEYWORDS, "follow(entity, distance=10, height=5, smoothing=1.0)\n\n" "Position camera to follow an entity.\n\n" diff --git a/src/3d/Viewport3D.h b/src/3d/Viewport3D.h index 63b1677..c38901f 100644 --- a/src/3d/Viewport3D.h +++ b/src/3d/Viewport3D.h @@ -89,9 +89,8 @@ public: /// Convert screen coordinates to world position via ray casting /// @param screenX X position relative to viewport /// @param screenY Y position relative to viewport - /// @param yPlane Y value of the horizontal plane to intersect (default: 0) - /// @return World position on the given Y plane, or (-1,-1,-1) if no intersection - vec3 screenToWorld(float screenX, float screenY, float yPlane = 0.0f); + /// @return World position on Y=0 plane, or (-1,-1,-1) if no intersection + vec3 screenToWorld(float screenX, float screenY); /// Position camera to follow an entity /// @param entity Entity to follow @@ -403,7 +402,7 @@ extern PyMethodDef Viewport3D_methods[]; namespace mcrfpydef { -inline PyTypeObject PyViewport3DType = { +static PyTypeObject PyViewport3DType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.Viewport3D", .tp_basicsize = sizeof(PyViewport3DObject), diff --git a/src/Animation.cpp b/src/Animation.cpp index e369556..b59c96e 100644 --- a/src/Animation.cpp +++ b/src/Animation.cpp @@ -1,7 +1,6 @@ #include "Animation.h" #include "UIDrawable.h" #include "UIEntity.h" -#include "3d/Entity3D.h" #include "PyAnimation.h" #include "McRFPy_API.h" #include "GameEngine.h" @@ -169,46 +168,8 @@ void Animation::startEntity(std::shared_ptr target) { } } -void Animation::startEntity3D(std::shared_ptr target) { - if (!target) return; - - entity3dTargetWeak = target; - elapsed = 0.0f; - callbackTriggered = false; - - // Capture the starting value from the entity - std::visit([this, target](const auto& val) { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - float value = 0.0f; - if (target->getProperty(targetProperty, value)) { - startValue = value; - } - } - else if constexpr (std::is_same_v) { - // For sprite_index/visible: capture via float and convert - float fvalue = 0.0f; - if (target->getProperty(targetProperty, fvalue)) { - startValue = static_cast(fvalue); - } - } - // Entity3D doesn't support other types - }, targetValue); - - // For zero-duration animations, apply final value immediately - if (duration <= 0.0f) { - AnimationValue finalValue = interpolate(1.0f); - applyValue(target.get(), finalValue); - if (pythonCallback && !callbackTriggered) { - triggerCallback(); - } - callbackTriggered = true; - } -} - bool Animation::hasValidTarget() const { - return !targetWeak.expired() || !entityTargetWeak.expired() || !entity3dTargetWeak.expired(); + return !targetWeak.expired() || !entityTargetWeak.expired(); } void Animation::clearCallback() { @@ -237,10 +198,6 @@ void Animation::complete() { AnimationValue finalValue = interpolate(1.0f); applyValue(entity.get(), finalValue); } - else if (auto entity3d = entity3dTargetWeak.lock()) { - AnimationValue finalValue = interpolate(1.0f); - applyValue(entity3d.get(), finalValue); - } } void Animation::stop() { @@ -258,10 +215,9 @@ bool Animation::update(float deltaTime) { // Try to lock weak_ptr to get shared_ptr std::shared_ptr target = targetWeak.lock(); std::shared_ptr entity = entityTargetWeak.lock(); - std::shared_ptr entity3d = entity3dTargetWeak.lock(); - // If all are null, target was destroyed - if (!target && !entity && !entity3d) { + // If both are null, target was destroyed + if (!target && !entity) { return false; // Remove this animation } @@ -275,8 +231,6 @@ bool Animation::update(float deltaTime) { applyValue(target.get(), finalValue); } else if (entity) { applyValue(entity.get(), finalValue); - } else if (entity3d) { - applyValue(entity3d.get(), finalValue); } // Trigger callback if (pythonCallback) { @@ -302,8 +256,6 @@ bool Animation::update(float deltaTime) { applyValue(target.get(), currentValue); } else if (entity) { applyValue(entity.get(), currentValue); - } else if (entity3d) { - applyValue(entity3d.get(), currentValue); } // Trigger callback when animation completes @@ -448,10 +400,10 @@ void Animation::applyValue(UIDrawable* target, const AnimationValue& value) { void Animation::applyValue(UIEntity* entity, const AnimationValue& value) { if (!entity) return; - + std::visit([this, entity](const auto& val) { using T = std::decay_t; - + if constexpr (std::is_same_v) { entity->setProperty(targetProperty, val); } @@ -462,22 +414,6 @@ void Animation::applyValue(UIEntity* entity, const AnimationValue& value) { }, value); } -void Animation::applyValue(mcrf::Entity3D* entity, const AnimationValue& value) { - if (!entity) return; - - std::visit([this, entity](const auto& val) { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - entity->setProperty(targetProperty, val); - } - else if constexpr (std::is_same_v) { - entity->setProperty(targetProperty, val); - } - // Entity3D doesn't support other types - }, value); -} - // #229 - Helper to convert UIDrawable target to Python object static PyObject* convertDrawableToPython(std::shared_ptr drawable) { if (!drawable) { @@ -624,37 +560,6 @@ static PyObject* convertEntityToPython(std::shared_ptr entity) { return (PyObject*)pyObj; } -// Helper to convert Entity3D target to Python object -static PyObject* convertEntity3DToPython(std::shared_ptr entity) { - if (!entity) { - Py_RETURN_NONE; - } - - // Use the entity's cached Python self pointer if available - if (entity->self) { - Py_INCREF(entity->self); - return entity->self; - } - - // Create a new wrapper - PyTypeObject* type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity3D"); - if (!type) { - Py_RETURN_NONE; - } - - auto pyObj = (PyEntity3DObject*)type->tp_alloc(type, 0); - Py_DECREF(type); - - if (!pyObj) { - Py_RETURN_NONE; - } - - pyObj->data = entity; - pyObj->weakreflist = NULL; - - return (PyObject*)pyObj; -} - // #229 - Helper to convert AnimationValue to Python object static PyObject* animationValueToPython(const AnimationValue& value) { return std::visit([](const auto& val) -> PyObject* { @@ -704,8 +609,6 @@ void Animation::triggerCallback() { targetObj = convertDrawableToPython(drawable); } else if (auto entity = entityTargetWeak.lock()) { targetObj = convertEntityToPython(entity); - } else if (auto entity3d = entity3dTargetWeak.lock()) { - targetObj = convertEntity3DToPython(entity3d); } // If target conversion failed, use None diff --git a/src/Animation.h b/src/Animation.h index 3fc4e0e..40b057f 100644 --- a/src/Animation.h +++ b/src/Animation.h @@ -11,7 +11,6 @@ // Forward declarations class UIDrawable; class UIEntity; -namespace mcrf { class Entity3D; } /** * ConflictMode - How to handle multiple animations on the same property (#120) @@ -59,9 +58,6 @@ public: // Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable) void startEntity(std::shared_ptr target); - - // Apply this animation to a 3D entity - void startEntity3D(std::shared_ptr target); // Complete the animation immediately (jump to final value) void complete(); @@ -94,7 +90,6 @@ public: void* getTargetPtr() const { if (auto sp = targetWeak.lock()) return sp.get(); if (auto sp = entityTargetWeak.lock()) return sp.get(); - if (auto sp = entity3dTargetWeak.lock()) return sp.get(); return nullptr; } @@ -111,7 +106,6 @@ private: // RAII: Use weak_ptr for safe target tracking std::weak_ptr targetWeak; std::weak_ptr entityTargetWeak; - std::weak_ptr entity3dTargetWeak; // Callback support PyObject* pythonCallback = nullptr; // Python callback function (we own a reference) @@ -127,7 +121,6 @@ private: // Helper to apply value to target void applyValue(UIDrawable* target, const AnimationValue& value); void applyValue(UIEntity* entity, const AnimationValue& value); - void applyValue(mcrf::Entity3D* entity, const AnimationValue& value); // Trigger callback when animation completes void triggerCallback(); diff --git a/src/UIDrawable.cpp b/src/UIDrawable.cpp index 346364a..996e1bf 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -1,5 +1,4 @@ #include "UIDrawable.h" -#include #include "UIFrame.h" #include "UICaption.h" #include "UISprite.h" @@ -429,8 +428,6 @@ void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) { if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) { render_texture = std::make_unique(); if (!render_texture->create(width, height)) { - std::cerr << "[McRogueFace] Warning: Failed to create RenderTexture (" - << width << "x" << height << ")" << std::endl; render_texture.reset(); use_render_texture = false; return; diff --git a/src/platform/SDL2Renderer.cpp b/src/platform/SDL2Renderer.cpp index 057ea6c..d73e175 100644 --- a/src/platform/SDL2Renderer.cpp +++ b/src/platform/SDL2Renderer.cpp @@ -17,11 +17,9 @@ #include // Emscripten's USE_SDL=2 port puts headers directly in include path #include -#include #include #else #include -#include #include // Desktop OpenGL - we'll use GL 2.1 compatible subset that matches GLES2 #define GL_GLEXT_PROTOTYPES @@ -134,22 +132,11 @@ bool SDL2Renderer::init() { if (initialized_) return true; // Initialize SDL2 if not already done - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_AUDIO) < 0) { + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { std::cerr << "SDL2Renderer: Failed to initialize SDL: " << SDL_GetError() << std::endl; return false; } - // Initialize SDL2_mixer for audio (non-fatal if it fails) - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) { - std::cerr << "SDL2Renderer: Failed to initialize audio: " << Mix_GetError() << std::endl; - std::cerr << "SDL2Renderer: Continuing without audio support" << std::endl; - } else { - Mix_AllocateChannels(16); - Mix_ChannelFinished(Sound::onChannelFinished); - audioInitialized_ = true; - std::cout << "SDL2Renderer: Audio initialized (16 channels, 44100 Hz)" << std::endl; - } - // Note: Shaders are initialized in initGL() after GL context is created // Set up initial projection matrix (identity) @@ -183,12 +170,6 @@ void SDL2Renderer::shutdown() { shapeProgram_ = spriteProgram_ = textProgram_ = 0; - // Close audio before SDL_Quit - if (audioInitialized_) { - Mix_CloseAudio(); - audioInitialized_ = false; - } - SDL_Quit(); initialized_ = false; } @@ -692,9 +673,6 @@ void RenderWindow::setSize(const Vector2u& size) { if (sdlWindow_) { SDL_SetWindowSize(static_cast(sdlWindow_), size.x, size.y); glViewport(0, 0, size.x, size.y); -#ifdef __EMSCRIPTEN__ - emscripten_set_canvas_element_size("#canvas", size.x, size.y); -#endif } } diff --git a/src/platform/SDL2Renderer.h b/src/platform/SDL2Renderer.h index 71690bd..f18656a 100644 --- a/src/platform/SDL2Renderer.h +++ b/src/platform/SDL2Renderer.h @@ -44,7 +44,6 @@ public: void shutdown(); bool isInitialized() const { return initialized_; } bool isGLInitialized() const { return glInitialized_; } - bool isAudioInitialized() const { return audioInitialized_; } // Built-in shader programs enum class ShaderType { @@ -101,7 +100,6 @@ private: bool initialized_ = false; bool glInitialized_ = false; - bool audioInitialized_ = false; // Built-in shader programs unsigned int shapeProgram_ = 0; diff --git a/src/platform/SDL2Types.h b/src/platform/SDL2Types.h index ef8fbec..d5f9bdf 100644 --- a/src/platform/SDL2Types.h +++ b/src/platform/SDL2Types.h @@ -25,7 +25,6 @@ #include #include #include -#include // SDL2 headers - conditionally included when actually implementing // For now, forward declare what we need @@ -34,13 +33,6 @@ #include #endif -// SDL2_mixer for audio (always needed in SDL2 builds for SoundBuffer/Sound/Music types) -#ifdef __EMSCRIPTEN__ -#include -#else -#include -#endif - namespace sf { // Forward declarations (needed for RenderWindow) @@ -930,195 +922,34 @@ public: }; // ============================================================================= -// Audio (SDL2_mixer backed) +// Audio Stubs (SDL2_mixer could implement these later) // ============================================================================= class SoundBuffer { - Mix_Chunk* chunk_ = nullptr; - Time duration_; - public: SoundBuffer() = default; - ~SoundBuffer() { - if (chunk_) { - Mix_FreeChunk(chunk_); - chunk_ = nullptr; - } - } - - // No copy (Mix_Chunk ownership) - SoundBuffer(const SoundBuffer&) = delete; - SoundBuffer& operator=(const SoundBuffer&) = delete; - - // Move - SoundBuffer(SoundBuffer&& other) noexcept - : chunk_(other.chunk_), duration_(other.duration_) { - other.chunk_ = nullptr; - } - SoundBuffer& operator=(SoundBuffer&& other) noexcept { - if (this != &other) { - if (chunk_) Mix_FreeChunk(chunk_); - chunk_ = other.chunk_; - duration_ = other.duration_; - other.chunk_ = nullptr; - } - return *this; - } - - bool loadFromFile(const std::string& filename) { - if (chunk_) { Mix_FreeChunk(chunk_); chunk_ = nullptr; } - chunk_ = Mix_LoadWAV(filename.c_str()); - if (!chunk_) return false; - computeDuration(); - return true; - } - - bool loadFromMemory(const void* data, size_t sizeInBytes) { - if (chunk_) { Mix_FreeChunk(chunk_); chunk_ = nullptr; } - SDL_RWops* rw = SDL_RWFromConstMem(data, static_cast(sizeInBytes)); - if (!rw) return false; - chunk_ = Mix_LoadWAV_RW(rw, 1); // 1 = free RWops after load - if (!chunk_) return false; - computeDuration(); - return true; - } - - Time getDuration() const { return duration_; } - Mix_Chunk* getChunk() const { return chunk_; } - -private: - void computeDuration() { - if (!chunk_) { duration_ = Time(); return; } - int freq = 0, channels = 0; - Uint16 format = 0; - Mix_QuerySpec(&freq, &format, &channels); - if (freq == 0 || channels == 0) { duration_ = Time(); return; } - // Compute bytes per sample based on format - int bytesPerSample = 2; // Default 16-bit - if (format == AUDIO_U8 || format == AUDIO_S8) bytesPerSample = 1; - else if (format == AUDIO_S32LSB || format == AUDIO_S32MSB) bytesPerSample = 4; - else if (format == AUDIO_F32LSB || format == AUDIO_F32MSB) bytesPerSample = 4; - int totalSamples = chunk_->alen / (bytesPerSample * channels); - float secs = static_cast(totalSamples) / static_cast(freq); - duration_ = seconds(secs); - } + bool loadFromFile(const std::string& filename) { return true; } // Stub + bool loadFromMemory(const void* data, size_t sizeInBytes) { return true; } // Stub + Time getDuration() const { return Time(); } }; -// Forward declare Sound for channel tracking -class Sound; - -// Channel tracking: maps SDL_mixer channel indices to Sound* owners -// Defined as inline to keep header-only and avoid multiple definition issues -inline Sound* g_channelOwners[16] = {}; - class Sound { public: enum Status { Stopped, Paused, Playing }; Sound() = default; - Sound(const SoundBuffer& buffer) : chunk_(buffer.getChunk()) {} + Sound(const SoundBuffer& buffer) {} - ~Sound() { - // Release our channel claim - if (channel_ >= 0 && channel_ < 16) { - if (g_channelOwners[channel_] == this) { - Mix_HaltChannel(channel_); - g_channelOwners[channel_] = nullptr; - } - channel_ = -1; - } - } + void setBuffer(const SoundBuffer& buffer) {} + void play() {} + void pause() {} + void stop() {} - // No copy (channel ownership) - Sound(const Sound&) = delete; - Sound& operator=(const Sound&) = delete; - - // Move - Sound(Sound&& other) noexcept - : chunk_(other.chunk_), channel_(other.channel_), - volume_(other.volume_), loop_(other.loop_) { - if (channel_ >= 0 && channel_ < 16) { - g_channelOwners[channel_] = this; - } - other.channel_ = -1; - other.chunk_ = nullptr; - } - Sound& operator=(Sound&& other) noexcept { - if (this != &other) { - stop(); - chunk_ = other.chunk_; - channel_ = other.channel_; - volume_ = other.volume_; - loop_ = other.loop_; - if (channel_ >= 0 && channel_ < 16) { - g_channelOwners[channel_] = this; - } - other.channel_ = -1; - other.chunk_ = nullptr; - } - return *this; - } - - void setBuffer(const SoundBuffer& buffer) { chunk_ = buffer.getChunk(); } - - void play() { - if (!chunk_) return; - channel_ = Mix_PlayChannel(-1, chunk_, loop_ ? -1 : 0); - if (channel_ >= 0 && channel_ < 16) { - // Clear any previous owner on this channel - if (g_channelOwners[channel_] && g_channelOwners[channel_] != this) { - g_channelOwners[channel_]->channel_ = -1; - } - g_channelOwners[channel_] = this; - Mix_Volume(channel_, static_cast(volume_ * 128.f / 100.f)); - } - } - - void pause() { - if (channel_ >= 0) Mix_Pause(channel_); - } - - void stop() { - if (channel_ >= 0) { - Mix_HaltChannel(channel_); - if (channel_ < 16 && g_channelOwners[channel_] == this) { - g_channelOwners[channel_] = nullptr; - } - channel_ = -1; - } - } - - Status getStatus() const { - if (channel_ < 0) return Stopped; - if (Mix_Paused(channel_)) return Paused; - if (Mix_Playing(channel_)) return Playing; - return Stopped; - } - - void setVolume(float vol) { - volume_ = std::clamp(vol, 0.f, 100.f); - if (channel_ >= 0) { - Mix_Volume(channel_, static_cast(volume_ * 128.f / 100.f)); - } - } - float getVolume() const { return volume_; } - - void setLoop(bool loop) { loop_ = loop; } - bool getLoop() const { return loop_; } - - // Called by Mix_ChannelFinished callback - static void onChannelFinished(int channel) { - if (channel >= 0 && channel < 16 && g_channelOwners[channel]) { - g_channelOwners[channel]->channel_ = -1; - g_channelOwners[channel] = nullptr; - } - } - -private: - Mix_Chunk* chunk_ = nullptr; // Borrowed from SoundBuffer - int channel_ = -1; - float volume_ = 100.f; - bool loop_ = false; + Status getStatus() const { return Stopped; } + void setVolume(float volume) {} + float getVolume() const { return 100.0f; } + void setLoop(bool loop) {} + bool getLoop() const { return false; } }; class Music { @@ -1126,83 +957,20 @@ public: enum Status { Stopped, Paused, Playing }; Music() = default; - ~Music() { - if (music_) { - Mix_FreeMusic(music_); - music_ = nullptr; - } - } + bool openFromFile(const std::string& filename) { return true; } // Stub - // No copy (global music channel) - Music(const Music&) = delete; - Music& operator=(const Music&) = delete; + void play() {} + void pause() {} + void stop() {} - // Move - Music(Music&& other) noexcept - : music_(other.music_), volume_(other.volume_), loop_(other.loop_) { - other.music_ = nullptr; - } - Music& operator=(Music&& other) noexcept { - if (this != &other) { - if (music_) Mix_FreeMusic(music_); - music_ = other.music_; - volume_ = other.volume_; - loop_ = other.loop_; - other.music_ = nullptr; - } - return *this; - } - - bool openFromFile(const std::string& filename) { - if (music_) { Mix_FreeMusic(music_); music_ = nullptr; } - music_ = Mix_LoadMUS(filename.c_str()); - return music_ != nullptr; - } - - void play() { - if (!music_) return; - Mix_PlayMusic(music_, loop_ ? -1 : 0); - Mix_VolumeMusic(static_cast(volume_ * 128.f / 100.f)); - } - - void pause() { - Mix_PauseMusic(); - } - - void stop() { - Mix_HaltMusic(); - } - - Status getStatus() const { - if (Mix_PausedMusic()) return Paused; - if (Mix_PlayingMusic()) return Playing; - return Stopped; - } - - void setVolume(float vol) { - volume_ = std::clamp(vol, 0.f, 100.f); - Mix_VolumeMusic(static_cast(volume_ * 128.f / 100.f)); - } - float getVolume() const { return volume_; } - - void setLoop(bool loop) { loop_ = loop; } - bool getLoop() const { return loop_; } - - // Duration not available in Emscripten's SDL_mixer 2.0.2 + Status getStatus() const { return Stopped; } + void setVolume(float volume) {} + float getVolume() const { return 100.0f; } + void setLoop(bool loop) {} + bool getLoop() const { return false; } Time getDuration() const { return Time(); } - - // Playing offset getter not available in Emscripten's SDL_mixer 2.0.2 Time getPlayingOffset() const { return Time(); } - - // Setter works for OGG via Mix_SetMusicPosition - void setPlayingOffset(Time offset) { - if (music_) Mix_SetMusicPosition(static_cast(offset.asSeconds())); - } - -private: - Mix_Music* music_ = nullptr; - float volume_ = 100.f; - bool loop_ = false; + void setPlayingOffset(Time offset) {} }; // ============================================================================= diff --git a/tests/unit/test_entity3d_animate.py b/tests/unit/test_entity3d_animate.py deleted file mode 100644 index df23945..0000000 --- a/tests/unit/test_entity3d_animate.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Test Entity3D.animate() (issue #242)""" -import mcrfpy -import sys - -errors = [] - -vp = mcrfpy.Viewport3D(pos=(0,0), size=(100,100)) -vp.set_grid_size(16, 16) - -# Test 1: Basic x animation -e = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -vp.entities.append(e) -start_x = e.world_pos[0] -anim = e.animate('x', 10.0, 1.0, 'linear') -if anim is None: - errors.append("animate() should return Animation object") -for _ in range(5): - mcrfpy.step(0.25) -if abs(e.world_pos[0] - 10.0) > 0.5: - errors.append(f"x animation: expected ~10.0, got {e.world_pos[0]}") - -# Test 2: Scale animation -e2 = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -vp.entities.append(e2) -e2.animate('scale', 5.0, 0.5, 'linear') -for _ in range(3): - mcrfpy.step(0.25) -if abs(e2.scale - 5.0) > 0.5: - errors.append(f"scale animation: expected ~5.0, got {e2.scale}") - -# Test 3: Rotation animation -e3 = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -vp.entities.append(e3) -e3.animate('rotation', 90.0, 0.5, 'easeInOut') -for _ in range(3): - mcrfpy.step(0.25) -if abs(e3.rotation - 90.0) > 0.5: - errors.append(f"rotation animation: expected ~90.0, got {e3.rotation}") - -# Test 4: Delta animation -e4 = mcrfpy.Entity3D(pos=(3,3), scale=1.0) -vp.entities.append(e4) -start_x = e4.world_pos[0] -e4.animate('x', 2.0, 0.5, 'linear', delta=True) -for _ in range(3): - mcrfpy.step(0.25) -expected = start_x + 2.0 -if abs(e4.world_pos[0] - expected) > 0.5: - errors.append(f"delta animation: expected ~{expected}, got {e4.world_pos[0]}") - -# Test 5: Invalid property raises ValueError -try: - e.animate('nonexistent', 1.0, 1.0, 'linear') - errors.append("Invalid property should raise ValueError") -except ValueError: - pass - -# Test 6: Invalid target type raises TypeError -try: - e.animate('x', "not_a_number", 1.0, 'linear') - errors.append("String target should raise TypeError") -except TypeError: - pass - -# Test 7: Callback -callback_called = [False] -def on_complete(target, prop, value): - callback_called[0] = True - -e5 = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -vp.entities.append(e5) -e5.animate('x', 5.0, 0.25, 'linear', callback=on_complete) -for _ in range(3): - mcrfpy.step(0.15) -if not callback_called[0]: - errors.append("Animation callback was not called") - -# Test 8: Easing enum -e6 = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -vp.entities.append(e6) -try: - e6.animate('x', 5.0, 1.0, mcrfpy.Easing.EASE_IN_OUT) -except Exception as ex: - errors.append(f"Easing enum should work: {ex}") -for _ in range(5): - mcrfpy.step(0.25) - -if errors: - for err in errors: - print(f"FAIL: {err}", file=sys.stderr) - sys.exit(1) -else: - print("PASS: Entity3D.animate() (issue #242)", file=sys.stderr) - sys.exit(0) diff --git a/tests/unit/test_entity3d_viewport.py b/tests/unit/test_entity3d_viewport.py deleted file mode 100644 index 4369b04..0000000 --- a/tests/unit/test_entity3d_viewport.py +++ /dev/null @@ -1,44 +0,0 @@ -"""Test Entity3D.viewport property (issue #244)""" -import mcrfpy -import sys - -errors = [] - -# Test 1: Detached entity returns None -e = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -if e.viewport is not None: - errors.append("Detached entity viewport should be None") - -# Test 2: Attached entity returns Viewport3D -vp = mcrfpy.Viewport3D(pos=(0,0), size=(100,100)) -vp.set_grid_size(16, 16) -e2 = mcrfpy.Entity3D(pos=(5,5), scale=1.0) -vp.entities.append(e2) -v = e2.viewport -if v is None: - errors.append("Attached entity viewport should not be None") -elif type(v).__name__ != 'Viewport3D': - errors.append(f"Expected Viewport3D, got {type(v).__name__}") - -# Test 3: Viewport properties are accessible -if v is not None: - try: - _ = v.w - _ = v.h - except Exception as ex: - errors.append(f"Viewport properties not accessible: {ex}") - -# Test 4: Multiple entities share same viewport -e3 = mcrfpy.Entity3D(pos=(3,3), scale=1.0) -vp.entities.append(e3) -v2 = e3.viewport -if v2 is None: - errors.append("Second entity viewport should not be None") - -if errors: - for err in errors: - print(f"FAIL: {err}", file=sys.stderr) - sys.exit(1) -else: - print("PASS: Entity3D.viewport (issue #244)", file=sys.stderr) - sys.exit(0) diff --git a/tests/unit/test_entity_collection3d_methods.py b/tests/unit/test_entity_collection3d_methods.py deleted file mode 100644 index b9d6d90..0000000 --- a/tests/unit/test_entity_collection3d_methods.py +++ /dev/null @@ -1,97 +0,0 @@ -"""Test EntityCollection3D pop/find/extend (issue #243)""" -import mcrfpy -import sys - -errors = [] - -vp = mcrfpy.Viewport3D(pos=(0,0), size=(100,100)) -vp.set_grid_size(16, 16) - -# Setup: add 5 named entities -for i in range(5): - e = mcrfpy.Entity3D(pos=(i, i), scale=1.0) - e.name = f"entity_{i}" - vp.entities.append(e) - -# Test find - existing -found = vp.entities.find("entity_2") -if found is None: - errors.append("find('entity_2') returned None") -elif found.name != "entity_2": - errors.append(f"find returned wrong entity: {found.name}") - -# Test find - missing -missing = vp.entities.find("nonexistent") -if missing is not None: - errors.append("find('nonexistent') should return None") - -# Test pop() - default (last element) -count_before = len(vp.entities) -popped = vp.entities.pop() -if popped.name != "entity_4": - errors.append(f"pop() should return last, got {popped.name}") -if len(vp.entities) != count_before - 1: - errors.append(f"Length should decrease after pop") - -# Test pop(0) - first element -popped0 = vp.entities.pop(0) -if popped0.name != "entity_0": - errors.append(f"pop(0) should return first, got {popped0.name}") - -# Test pop(1) - middle element -popped1 = vp.entities.pop(1) -if popped1.name != "entity_2": - errors.append(f"pop(1) should return index 1, got {popped1.name}") - -# Current state: [entity_1, entity_3] -if len(vp.entities) != 2: - errors.append(f"Expected 2 remaining, got {len(vp.entities)}") - -# Test pop - out of range -try: - vp.entities.pop(99) - errors.append("pop(99) should raise IndexError") -except IndexError: - pass - -# Test extend -new_entities = [] -for i in range(3): - e = mcrfpy.Entity3D(pos=(10+i, 10+i), scale=1.0) - e.name = f"new_{i}" - new_entities.append(e) -vp.entities.extend(new_entities) -if len(vp.entities) != 5: - errors.append(f"After extend, expected 5, got {len(vp.entities)}") - -# Verify extended entities are findable -if vp.entities.find("new_1") is None: - errors.append("Extended entity not findable") - -# Test extend with invalid type -try: - vp.entities.extend([42]) - errors.append("extend with non-Entity3D should raise TypeError") -except TypeError: - pass - -# Test extend with non-iterable -try: - vp.entities.extend(42) - errors.append("extend with non-iterable should raise TypeError") -except TypeError: - pass - -# Test name property -e_named = mcrfpy.Entity3D(pos=(0,0), scale=1.0) -e_named.name = "test_name" -if e_named.name != "test_name": - errors.append(f"name property: expected 'test_name', got '{e_named.name}'") - -if errors: - for err in errors: - print(f"FAIL: {err}", file=sys.stderr) - sys.exit(1) -else: - print("PASS: EntityCollection3D pop/find/extend (issue #243)", file=sys.stderr) - sys.exit(0) diff --git a/tests/unit/test_screen_to_world_yplane.py b/tests/unit/test_screen_to_world_yplane.py deleted file mode 100644 index 98d8ca9..0000000 --- a/tests/unit/test_screen_to_world_yplane.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Test screen_to_world y_plane parameter (issue #245)""" -import mcrfpy -import sys - -errors = [] - -vp = mcrfpy.Viewport3D(pos=(0,0), size=(320,240)) -vp.set_grid_size(16, 16) -# Position camera above the Y=0 plane -vp.camera_pos = (0, 5, 5) -vp.camera_target = (0, 0, 0) - -# Test 1: Default y_plane=0 works -r1 = vp.screen_to_world(160, 120) -if r1 is None: - errors.append("screen_to_world with default y_plane returned None") - -# Test 2: Explicit y_plane=0 matches default -r2 = vp.screen_to_world(160, 120, y_plane=0.0) -if r2 is None: - errors.append("screen_to_world with y_plane=0 returned None") -elif r1 is not None: - for i in range(3): - if abs(r1[i] - r2[i]) > 0.001: - errors.append(f"Default and explicit y_plane=0 differ: {r1} vs {r2}") - break - -# Test 3: Different y_plane gives different result -r3 = vp.screen_to_world(160, 120, y_plane=2.0) -if r3 is None: - errors.append("screen_to_world with y_plane=2 returned None") -elif r1 is not None: - if r1 == r3: - errors.append("y_plane=0 and y_plane=2 should give different results") - -# Test 4: y component matches y_plane -if r3 is not None: - if abs(r3[1] - 2.0) > 0.001: - errors.append(f"y component should be 2.0 for y_plane=2.0, got {r3[1]}") - -# Test 5: Positional argument also works -r4 = vp.screen_to_world(160, 120, 3.0) -if r4 is None: - errors.append("Positional y_plane argument returned None") -elif abs(r4[1] - 3.0) > 0.001: - errors.append(f"y component should be 3.0, got {r4[1]}") - -if errors: - for err in errors: - print(f"FAIL: {err}", file=sys.stderr) - sys.exit(1) -else: - print("PASS: screen_to_world y_plane (issue #245)", file=sys.stderr) - sys.exit(0)