diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index de149be..8a4c619 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -457,11 +457,17 @@ void GameEngine::processEvent(const sf::Event& event) std::string name = currentScene()->action(actionCode); currentScene()->doAction(name, actionType); } - else if (currentScene()->key_callable && + else if (currentScene()->key_callable && !currentScene()->key_callable->isNone() && (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)) { + // Property-assigned handler (scene.on_key = callable) currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType); } + else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased) + { + // Try subclass on_key method if no property handler is set + McRFPy_API::triggerKeyEvent(ActionCode::key_str(event.key.code), actionType); + } } void GameEngine::sUserInput() diff --git a/src/GridChunk.cpp b/src/GridChunk.cpp index f1ff7f2..d98339b 100644 --- a/src/GridChunk.cpp +++ b/src/GridChunk.cpp @@ -33,13 +33,6 @@ void GridChunk::markDirty() { // #150 - Removed ensureTexture/renderToTexture - base layer rendering removed // GridChunk now only provides data storage for GridPoints -sf::FloatRect GridChunk::getWorldBounds(int cell_width, int cell_height) const { - return sf::FloatRect( - sf::Vector2f(world_x * cell_width, world_y * cell_height), - sf::Vector2f(width * cell_width, height * cell_height) - ); -} - bool GridChunk::isVisible(float left_edge, float top_edge, float right_edge, float bottom_edge) const { // Check if chunk's cell range overlaps with viewport's cell range @@ -147,26 +140,6 @@ const UIGridPoint& ChunkManager::at(int x, int y) const { return chunk->at(local_x, local_y); } -void ChunkManager::markAllDirty() { - for (auto& chunk : chunks) { - chunk->markDirty(); - } -} - -std::vector ChunkManager::getVisibleChunks(float left_edge, float top_edge, - float right_edge, float bottom_edge) { - std::vector visible; - visible.reserve(chunks.size()); // Pre-allocate for worst case - - for (auto& chunk : chunks) { - if (chunk->isVisible(left_edge, top_edge, right_edge, bottom_edge)) { - visible.push_back(chunk.get()); - } - } - - return visible; -} - void ChunkManager::resize(int new_grid_x, int new_grid_y) { // For now, simple rebuild - could be optimized to preserve data grid_x = new_grid_x; diff --git a/src/GridChunk.h b/src/GridChunk.h index 1360355..133b786 100644 --- a/src/GridChunk.h +++ b/src/GridChunk.h @@ -50,9 +50,6 @@ public: // Mark chunk as dirty void markDirty(); - // Get pixel bounds of this chunk in world coordinates - sf::FloatRect getWorldBounds(int cell_width, int cell_height) const; - // Check if chunk overlaps with viewport bool isVisible(float left_edge, float top_edge, float right_edge, float bottom_edge) const; @@ -90,13 +87,6 @@ public: UIGridPoint& at(int x, int y); const UIGridPoint& at(int x, int y) const; - // Mark all chunks dirty (for full rebuild) - void markAllDirty(); - - // Get chunks that overlap with viewport - std::vector getVisibleChunks(float left_edge, float top_edge, - float right_edge, float bottom_edge); - // Resize grid (rebuilds chunks) void resize(int new_grid_x, int new_grid_y); diff --git a/src/GridLayers.cpp b/src/GridLayers.cpp index 16f02c4..f6c6f85 100644 --- a/src/GridLayers.cpp +++ b/src/GridLayers.cpp @@ -57,11 +57,6 @@ int GridLayer::getChunkIndex(int cell_x, int cell_y) const { return cy * chunks_x + cx; } -void GridLayer::getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const { - chunk_x = cell_x / CHUNK_SIZE; - chunk_y = cell_y / CHUNK_SIZE; -} - void GridLayer::getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const { start_x = chunk_x * CHUNK_SIZE; start_y = chunk_y * CHUNK_SIZE; diff --git a/src/GridLayers.h b/src/GridLayers.h index 290162d..aeccdf5 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -56,9 +56,6 @@ public: // Get chunk index for a cell int getChunkIndex(int cell_x, int cell_y) const; - // Get chunk coordinates for a cell - void getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const; - // Get cell bounds for a chunk void getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const; diff --git a/src/McRFPy_API.h b/src/McRFPy_API.h index f6e7440..3c7408d 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -80,6 +80,7 @@ public: static void triggerSceneChange(const std::string& from_scene, const std::string& to_scene); static void updatePythonScenes(float dt); static void triggerResize(int width, int height); + static void triggerKeyEvent(const std::string& key, const std::string& action); // #151: Module-level scene property accessors static PyObject* api_get_current_scene(); diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp index 59cb51b..e388384 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -413,23 +413,28 @@ void PySceneClass::call_on_exit(PySceneObject* self) } } -void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action) +void PySceneClass::call_on_key(PySceneObject* self, const std::string& key, const std::string& action) { PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_keypress"); - if (method && PyCallable_Check(method)) { - PyObject* result = PyObject_CallFunction(method, "ss", key.c_str(), action.c_str()); + // Look for on_key attribute on the Python object + // This handles both: + // 1. Subclass methods: class MyScene(Scene): def on_key(self, k, s): ... + // 2. Instance attributes: ts.on_key = lambda k, s: ... (when subclass shadows property) + PyObject* attr = PyObject_GetAttrString((PyObject*)self, "on_key"); + if (attr && PyCallable_Check(attr) && attr != Py_None) { + // Call it - works for both bound methods and regular callables + PyObject* result = PyObject_CallFunction(attr, "ss", key.c_str(), action.c_str()); if (result) { Py_DECREF(result); } else { PyErr_Print(); } - Py_DECREF(method); + Py_DECREF(attr); } else { - // Clear AttributeError if method doesn't exist + // Not callable or is None - nothing to call PyErr_Clear(); - Py_XDECREF(method); + Py_XDECREF(attr); } PyGILState_Release(gstate); @@ -571,6 +576,18 @@ void McRFPy_API::triggerResize(int width, int height) } } +// Helper function to trigger key events on Python scene subclasses +void McRFPy_API::triggerKeyEvent(const std::string& key, const std::string& action) +{ + GameEngine* game = McRFPy_API::game; + if (!game) return; + + // Only notify the active scene if it has an on_key method (subclass) + if (python_scenes.count(game->scene) > 0) { + PySceneClass::call_on_key(python_scenes[game->scene], key, action); + } +} + // #151: Get the current scene as a Python Scene object PyObject* McRFPy_API::api_get_current_scene() { diff --git a/src/PySceneObject.h b/src/PySceneObject.h index 9fb7c23..9e9f189 100644 --- a/src/PySceneObject.h +++ b/src/PySceneObject.h @@ -36,7 +36,7 @@ public: // Lifecycle callbacks (called from C++) static void call_on_enter(PySceneObject* self); static void call_on_exit(PySceneObject* self); - static void call_on_keypress(PySceneObject* self, std::string key, std::string action); + static void call_on_key(PySceneObject* self, const std::string& key, const std::string& action); static void call_update(PySceneObject* self, float dt); static void call_on_resize(PySceneObject* self, int width, int height); @@ -75,7 +75,7 @@ namespace mcrfpydef { "Lifecycle Callbacks (override in subclass):\n" " on_enter(): Called when scene becomes active via activate().\n" " on_exit(): Called when scene is deactivated (another scene activates).\n" - " on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.\n" + " on_key(key: str, action: str): Called for keyboard events (subclass method).\n" " update(dt: float): Called every frame with delta time in seconds.\n" " on_resize(width: int, height: int): Called when window is resized.\n\n" "Example:\n" diff --git a/src/SpatialHash.cpp b/src/SpatialHash.cpp index 9902d94..0ca4b81 100644 --- a/src/SpatialHash.cpp +++ b/src/SpatialHash.cpp @@ -92,24 +92,6 @@ std::vector> SpatialHash::getBucketsInRadius(float x, float return result; } -std::vector> SpatialHash::getBucketsInRect(float x, float y, float width, float height) const -{ - std::vector> result; - - int min_bx = static_cast(std::floor(x / bucket_size)); - int max_bx = static_cast(std::floor((x + width) / bucket_size)); - int min_by = static_cast(std::floor(y / bucket_size)); - int max_by = static_cast(std::floor((y + height) / bucket_size)); - - for (int bx = min_bx; bx <= max_bx; ++bx) { - for (int by = min_by; by <= max_by; ++by) { - result.emplace_back(bx, by); - } - } - - return result; -} - std::vector> SpatialHash::queryRadius(float x, float y, float radius) const { std::vector> result; @@ -137,57 +119,7 @@ std::vector> SpatialHash::queryRadius(float x, float y return result; } -std::vector> SpatialHash::queryRect(float x, float y, float width, float height) const -{ - std::vector> result; - - auto bucket_coords = getBucketsInRect(x, y, width, height); - - for (const auto& coord : bucket_coords) { - auto it = buckets.find(coord); - if (it == buckets.end()) continue; - - for (const auto& wp : it->second) { - auto entity = wp.lock(); - if (!entity) continue; - - // Check if entity is within the rectangle - float ex = entity->position.x; - float ey = entity->position.y; - if (ex >= x && ex < x + width && ey >= y && ey < y + height) { - result.push_back(entity); - } - } - } - - return result; -} - void SpatialHash::clear() { buckets.clear(); } - -size_t SpatialHash::totalEntities() const -{ - size_t count = 0; - for (const auto& [coord, bucket] : buckets) { - for (const auto& wp : bucket) { - if (wp.lock()) { - ++count; - } - } - } - return count; -} - -void SpatialHash::cleanBucket(std::vector>& bucket) -{ - bucket.erase( - std::remove_if(bucket.begin(), bucket.end(), - [](const std::weak_ptr& wp) { - return wp.expired(); - }), - bucket.end() - ); -} diff --git a/src/SpatialHash.h b/src/SpatialHash.h index 8bc9828..c902cf6 100644 --- a/src/SpatialHash.h +++ b/src/SpatialHash.h @@ -37,15 +37,11 @@ public: // Returns entities whose positions are within the circular radius std::vector> queryRadius(float x, float y, float radius) const; - // Query all entities within a rectangular region - std::vector> queryRect(float x, float y, float width, float height) const; - // Clear all entities from the hash void clear(); // Get statistics for debugging size_t bucketCount() const { return buckets.size(); } - size_t totalEntities() const; private: int bucket_size; @@ -72,10 +68,4 @@ private: // Get all bucket coordinates that overlap with a radius query std::vector> getBucketsInRadius(float x, float y, float radius) const; - - // Get all bucket coordinates that overlap with a rectangle - std::vector> getBucketsInRect(float x, float y, float width, float height) const; - - // Clean expired weak_ptrs from a bucket - void cleanBucket(std::vector>& bucket); }; diff --git a/src/UIContainerBase.h b/src/UIContainerBase.h deleted file mode 100644 index 3dc0220..0000000 --- a/src/UIContainerBase.h +++ /dev/null @@ -1,82 +0,0 @@ -#pragma once -#include "UIDrawable.h" -#include -#include - -// Base class for UI containers that provides common click handling logic -class UIContainerBase { -protected: - // Transform a point from parent coordinates to this container's local coordinates - virtual sf::Vector2f toLocalCoordinates(sf::Vector2f point) const = 0; - - // Transform a point from this container's local coordinates to child coordinates - virtual sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const = 0; - - // Get the bounds of this container in parent coordinates - virtual sf::FloatRect getBounds() const = 0; - - // Check if a local point is within this container's bounds - virtual bool containsPoint(sf::Vector2f localPoint) const = 0; - - // Get click handler if this container has one - virtual UIDrawable* getClickHandler() = 0; - - // Get children to check for clicks (can be empty) - virtual std::vector getClickableChildren() = 0; - -public: - // Standard click handling algorithm for all containers - // Returns the deepest UIDrawable that has a click handler and contains the point - UIDrawable* handleClick(sf::Vector2f point) { - // Transform to local coordinates - sf::Vector2f localPoint = toLocalCoordinates(point); - - // Check if point is within our bounds - if (!containsPoint(localPoint)) { - return nullptr; - } - - // Check children in reverse z-order (top-most first) - // This ensures that elements rendered on top get first chance at clicks - auto children = getClickableChildren(); - - // TODO: Sort by z-index if not already sorted - // std::sort(children.begin(), children.end(), - // [](UIDrawable* a, UIDrawable* b) { return a->z_index > b->z_index; }); - - for (int i = children.size() - 1; i >= 0; --i) { - if (!children[i]->visible) continue; - - sf::Vector2f childPoint = toChildCoordinates(localPoint, i); - if (auto target = children[i]->click_at(childPoint)) { - // Child (or its descendant) handled the click - return target; - } - // If child didn't handle it, continue checking other children - // This allows click-through for elements without handlers - } - - // No child consumed the click - // Now check if WE have a click handler - return getClickHandler(); - } -}; - -// Helper for containers with simple box bounds -class RectangularContainer : public UIContainerBase { -protected: - sf::FloatRect bounds; - - sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override { - return point - sf::Vector2f(bounds.left, bounds.top); - } - - bool containsPoint(sf::Vector2f localPoint) const override { - return localPoint.x >= 0 && localPoint.y >= 0 && - localPoint.x < bounds.width && localPoint.y < bounds.height; - } - - sf::FloatRect getBounds() const override { - return bounds; - } -}; \ No newline at end of file diff --git a/tests/unit/scene_subclass_on_key_test.py b/tests/unit/scene_subclass_on_key_test.py new file mode 100644 index 0000000..829f188 --- /dev/null +++ b/tests/unit/scene_subclass_on_key_test.py @@ -0,0 +1,84 @@ +"""Test Scene subclass on_key method callback + +Verifies that: +1. Subclass on_key method is called for keyboard events +2. Property assignment (scene.on_key = callable) still works +3. Property assignment on subclass overrides the method +""" +import mcrfpy +from mcrfpy import automation +import sys + +# Test state +tests_passed = 0 +tests_failed = 0 + +def test_subclass_method(): + """Test that subclass on_key method receives keyboard events""" + global tests_passed, tests_failed + events = [] + + class TestScene(mcrfpy.Scene): + def on_key(self, key, state): + events.append((key, state)) + + ts = TestScene('test_method') + ts.activate() + automation.keyDown('a') + automation.keyUp('a') + + if len(events) >= 2: + print("PASS: test_subclass_method") + tests_passed += 1 + else: + print(f"FAIL: test_subclass_method - got {events}") + tests_failed += 1 + +def test_property_handler(): + """Test that property assignment works""" + global tests_passed, tests_failed + events = [] + + scene = mcrfpy.Scene('test_property') + scene.on_key = lambda k, s: events.append((k, s)) + scene.activate() + automation.keyDown('b') + automation.keyUp('b') + + if len(events) >= 2: + print("PASS: test_property_handler") + tests_passed += 1 + else: + print(f"FAIL: test_property_handler - got {events}") + tests_failed += 1 + +def test_property_overrides_method(): + """Test that property assignment on subclass overrides the method""" + global tests_passed, tests_failed + method_events = [] + property_events = [] + + class TestScene(mcrfpy.Scene): + def on_key(self, key, state): + method_events.append((key, state)) + + ts = TestScene('test_override') + ts.activate() + ts.on_key = lambda k, s: property_events.append((k, s)) + automation.keyDown('c') + automation.keyUp('c') + + if len(property_events) >= 2 and len(method_events) == 0: + print("PASS: test_property_overrides_method") + tests_passed += 1 + else: + print(f"FAIL: test_property_overrides_method - method={method_events}, property={property_events}") + tests_failed += 1 + +# Run tests +test_subclass_method() +test_property_handler() +test_property_overrides_method() + +print(f"\nResults: {tests_passed} passed, {tests_failed} failed") +sys.exit(0 if tests_failed == 0 else 1)