diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 8a4c619..de149be 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -457,17 +457,11 @@ void GameEngine::processEvent(const sf::Event& event) std::string name = currentScene()->action(actionCode); currentScene()->doAction(name, actionType); } - else if (currentScene()->key_callable && !currentScene()->key_callable->isNone() && + else if (currentScene()->key_callable && (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 d98339b..f1ff7f2 100644 --- a/src/GridChunk.cpp +++ b/src/GridChunk.cpp @@ -33,6 +33,13 @@ 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 @@ -140,6 +147,26 @@ 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 133b786..1360355 100644 --- a/src/GridChunk.h +++ b/src/GridChunk.h @@ -50,6 +50,9 @@ 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; @@ -87,6 +90,13 @@ 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 f6c6f85..16f02c4 100644 --- a/src/GridLayers.cpp +++ b/src/GridLayers.cpp @@ -57,6 +57,11 @@ 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 aeccdf5..290162d 100644 --- a/src/GridLayers.h +++ b/src/GridLayers.h @@ -56,6 +56,9 @@ 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 3c7408d..f6e7440 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -80,7 +80,6 @@ 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 e388384..59cb51b 100644 --- a/src/PySceneObject.cpp +++ b/src/PySceneObject.cpp @@ -413,28 +413,23 @@ void PySceneClass::call_on_exit(PySceneObject* self) } } -void PySceneClass::call_on_key(PySceneObject* self, const std::string& key, const std::string& action) +void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, std::string action) { PyGILState_STATE gstate = PyGILState_Ensure(); - // 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()); + 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()); if (result) { Py_DECREF(result); } else { PyErr_Print(); } - Py_DECREF(attr); + Py_DECREF(method); } else { - // Not callable or is None - nothing to call + // Clear AttributeError if method doesn't exist PyErr_Clear(); - Py_XDECREF(attr); + Py_XDECREF(method); } PyGILState_Release(gstate); @@ -576,18 +571,6 @@ 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 9e9f189..9fb7c23 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_key(PySceneObject* self, const std::string& key, const std::string& action); + static void call_on_keypress(PySceneObject* self, std::string key, 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_key(key: str, action: str): Called for keyboard events (subclass method).\n" + " on_keypress(key: str, action: str): Called for keyboard events. Alternative to on_key property.\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 0ca4b81..9902d94 100644 --- a/src/SpatialHash.cpp +++ b/src/SpatialHash.cpp @@ -92,6 +92,24 @@ 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; @@ -119,7 +137,57 @@ 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 c902cf6..8bc9828 100644 --- a/src/SpatialHash.h +++ b/src/SpatialHash.h @@ -37,11 +37,15 @@ 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; @@ -68,4 +72,10 @@ 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 new file mode 100644 index 0000000..3dc0220 --- /dev/null +++ b/src/UIContainerBase.h @@ -0,0 +1,82 @@ +#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 deleted file mode 100644 index 829f188..0000000 --- a/tests/unit/scene_subclass_on_key_test.py +++ /dev/null @@ -1,84 +0,0 @@ -"""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)