Compare commits
3 commits
2c320effc6
...
1d11b020b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d11b020b0 | |||
| b6eb70748a | |||
| ae27e7deee |
12 changed files with 118 additions and 215 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<GridChunk*> ChunkManager::getVisibleChunks(float left_edge, float top_edge,
|
||||
float right_edge, float bottom_edge) {
|
||||
std::vector<GridChunk*> 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;
|
||||
|
|
|
|||
|
|
@ -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<GridChunk*> 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -92,24 +92,6 @@ std::vector<std::pair<int, int>> SpatialHash::getBucketsInRadius(float x, float
|
|||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::pair<int, int>> SpatialHash::getBucketsInRect(float x, float y, float width, float height) const
|
||||
{
|
||||
std::vector<std::pair<int, int>> result;
|
||||
|
||||
int min_bx = static_cast<int>(std::floor(x / bucket_size));
|
||||
int max_bx = static_cast<int>(std::floor((x + width) / bucket_size));
|
||||
int min_by = static_cast<int>(std::floor(y / bucket_size));
|
||||
int max_by = static_cast<int>(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<std::shared_ptr<UIEntity>> SpatialHash::queryRadius(float x, float y, float radius) const
|
||||
{
|
||||
std::vector<std::shared_ptr<UIEntity>> result;
|
||||
|
|
@ -137,57 +119,7 @@ std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryRadius(float x, float y
|
|||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<UIEntity>> SpatialHash::queryRect(float x, float y, float width, float height) const
|
||||
{
|
||||
std::vector<std::shared_ptr<UIEntity>> 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<std::weak_ptr<UIEntity>>& bucket)
|
||||
{
|
||||
bucket.erase(
|
||||
std::remove_if(bucket.begin(), bucket.end(),
|
||||
[](const std::weak_ptr<UIEntity>& wp) {
|
||||
return wp.expired();
|
||||
}),
|
||||
bucket.end()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,15 +37,11 @@ public:
|
|||
// Returns entities whose positions are within the circular radius
|
||||
std::vector<std::shared_ptr<UIEntity>> queryRadius(float x, float y, float radius) const;
|
||||
|
||||
// Query all entities within a rectangular region
|
||||
std::vector<std::shared_ptr<UIEntity>> 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<std::pair<int, int>> getBucketsInRadius(float x, float y, float radius) const;
|
||||
|
||||
// Get all bucket coordinates that overlap with a rectangle
|
||||
std::vector<std::pair<int, int>> getBucketsInRect(float x, float y, float width, float height) const;
|
||||
|
||||
// Clean expired weak_ptrs from a bucket
|
||||
void cleanBucket(std::vector<std::weak_ptr<UIEntity>>& bucket);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
#pragma once
|
||||
#include "UIDrawable.h"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
// 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<UIDrawable*> 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;
|
||||
}
|
||||
};
|
||||
84
tests/unit/scene_subclass_on_key_test.py
Normal file
84
tests/unit/scene_subclass_on_key_test.py
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue