diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 060c9c0..751adcc 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -14,7 +14,10 @@ UIGrid::UIGrid() { // Initialize entities list entities = std::make_shared>>(); - + + // Initialize children collection (for UIDrawables like speech bubbles, effects) + children = std::make_shared>>(); + // Initialize box with safe defaults box.setSize(sf::Vector2f(0, 0)); position = sf::Vector2f(0, 0); // Set base class position @@ -48,6 +51,9 @@ UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _x center_y = (gy/2) * cell_height; entities = std::make_shared>>(); + // Initialize children collection (for UIDrawables like speech bubbles, effects) + children = std::make_shared>>(); + box.setSize(_wh); position = _xy; // Set base class position box.setPosition(position); // Sync box position @@ -209,7 +215,38 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) Resources::game->metrics.entitiesRendered += entitiesRendered; Resources::game->metrics.totalEntities += totalEntities; } - + + // Children layer - UIDrawables in grid-world pixel coordinates + // Positioned between entities and FOV overlay for proper z-ordering + if (children && !children->empty()) { + // Sort by z_index if needed + if (children_need_sort) { + std::sort(children->begin(), children->end(), + [](const auto& a, const auto& b) { return a->z_index < b->z_index; }); + children_need_sort = false; + } + + for (auto& child : *children) { + if (!child->visible) continue; + + // Cull children outside visible region (convert pixel pos to cell coords) + float child_grid_x = child->position.x / cell_width; + float child_grid_y = child->position.y / cell_height; + + if (child_grid_x < left_edge - 2 || child_grid_x >= left_edge + width_sq + 2 || + child_grid_y < top_edge - 2 || child_grid_y >= top_edge + height_sq + 2) { + continue; // Not visible, skip rendering + } + + // Transform grid-world pixel position to RenderTexture pixel position + auto pixel_pos = sf::Vector2f( + (child->position.x - left_spritepixels) * zoom, + (child->position.y - top_spritepixels) * zoom + ); + + child->render(pixel_pos, renderTexture); + } + } // top layer - opacity for discovered / visible status based on perspective // Only render visibility overlay if perspective is enabled @@ -529,10 +566,32 @@ UIDrawable* UIGrid::click_at(sf::Vector2f point) int left_spritepixels = center_x - (box.getSize().x / 2.0 / zoom); int top_spritepixels = center_y - (box.getSize().y / 2.0 / zoom); - // Convert click position to grid coordinates - float grid_x = (localPoint.x / zoom + left_spritepixels) / cell_width; - float grid_y = (localPoint.y / zoom + top_spritepixels) / cell_height; - + // Convert click position to grid-world pixel coordinates + float grid_world_x = localPoint.x / zoom + left_spritepixels; + float grid_world_y = localPoint.y / zoom + top_spritepixels; + + // Convert to grid cell coordinates + float grid_x = grid_world_x / cell_width; + float grid_y = grid_world_y / cell_height; + + // Check children first (they render on top, so they get priority) + // Children are positioned in grid-world pixel coordinates + if (children && !children->empty()) { + // Check in reverse z-order (highest z_index first, rendered last = on top) + for (auto it = children->rbegin(); it != children->rend(); ++it) { + auto& child = *it; + if (!child->visible) continue; + + // Transform click to child's local coordinate space + // Children's position is in grid-world pixels + sf::Vector2f childLocalPoint = sf::Vector2f(grid_world_x, grid_world_y); + + if (auto target = child->click_at(childLocalPoint)) { + return target; + } + } + } + // Check entities in reverse order (assuming they should be checked top to bottom) // Note: entities list is not sorted by z-index currently, but we iterate in reverse // to match the render order assumption @@ -1408,7 +1467,8 @@ PyGetSetDef UIGrid::getsetters[] = { {"size", (getter)UIGrid::get_size, (setter)UIGrid::set_size, "Size of the grid (width, height)", NULL}, {"center", (getter)UIGrid::get_center, (setter)UIGrid::set_center, "Grid coordinate at the center of the Grid's view (pan)", NULL}, - {"entities", (getter)UIGrid::get_children, NULL, "EntityCollection of entities on this grid", NULL}, + {"entities", (getter)UIGrid::get_entities, NULL, "EntityCollection of entities on this grid", NULL}, + {"children", (getter)UIGrid::get_children, NULL, "UICollection of UIDrawable children (speech bubbles, effects, overlays)", NULL}, {"x", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner X-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 0)}, {"y", (getter)UIDrawable::get_float_member, (setter)UIDrawable::set_float_member, "top-left corner Y-coordinate", (void*)((intptr_t)PyObjectsEnum::UIGRID << 8 | 1)}, @@ -1442,19 +1502,29 @@ PyGetSetDef UIGrid::getsetters[] = { {NULL} /* Sentinel */ }; -PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure) +PyObject* UIGrid::get_entities(PyUIGridObject* self, void* closure) { - // create PyUICollection instance pointing to self->data->children - //PyUIEntityCollectionObject* o = (PyUIEntityCollectionObject*)PyUIEntityCollectionType.tp_alloc(&PyUIEntityCollectionType, 0); + // Returns EntityCollection for entity management auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "EntityCollection"); auto o = (PyUIEntityCollectionObject*)type->tp_alloc(type, 0); if (o) { - o->data = self->data->entities; // todone. / BUGFIX - entities isn't a shared pointer on UIGrid, what to do? -- I made it a sp>> + o->data = self->data->entities; o->grid = self->data; } return (PyObject*)o; } +PyObject* UIGrid::get_children(PyUIGridObject* self, void* closure) +{ + // Returns UICollection for UIDrawable children (speech bubbles, effects, overlays) + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "UICollection"); + auto o = (PyUICollectionObject*)type->tp_alloc(type, 0); + if (o) { + o->data = self->data->children; + } + return (PyObject*)o; +} + PyObject* UIGrid::repr(PyUIGridObject* self) { std::ostringstream ss; diff --git a/src/UIGrid.h b/src/UIGrid.h index e8f9311..bbf6b4e 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -75,7 +75,11 @@ public: sf::RenderTexture renderTexture; std::vector points; std::shared_ptr>> entities; - + + // UIDrawable children collection (speech bubbles, effects, overlays, etc.) + std::shared_ptr>> children; + bool children_need_sort = true; // Dirty flag for z_index sorting + // Background rendering sf::Color fill_color; @@ -118,6 +122,7 @@ public: static PyObject* py_compute_astar_path(PyUIGridObject* self, PyObject* args, PyObject* kwds); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; + static PyObject* get_entities(PyUIGridObject* self, void* closure); static PyObject* get_children(PyUIGridObject* self, void* closure); static PyObject* repr(PyUIGridObject* self); diff --git a/tests/test_grid_children.py b/tests/test_grid_children.py new file mode 100644 index 0000000..5615886 --- /dev/null +++ b/tests/test_grid_children.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""Test Grid.children collection - Issue #132""" +import mcrfpy +from mcrfpy import automation +import sys + +def take_screenshot(runtime): + """Take screenshot after render completes""" + mcrfpy.delTimer("screenshot") + automation.screenshot("test_grid_children_result.png") + + print("Screenshot saved to test_grid_children_result.png") + print("PASS - Grid.children test completed") + sys.exit(0) + +def run_test(runtime): + """Main test - runs after scene is set up""" + mcrfpy.delTimer("test") + + # Get the scene UI + ui = mcrfpy.sceneUI("test") + + # Create a grid without texture (uses default 16x16 cells) + print("Test 1: Creating Grid with children...") + grid = mcrfpy.Grid(grid_size=(20, 15), pos=(50, 50), size=(320, 240)) + grid.fill_color = mcrfpy.Color(30, 30, 60) + ui.append(grid) + + # Verify entities and children properties exist + print(f" grid.entities = {grid.entities}") + print(f" grid.children = {grid.children}") + + # Test 2: Add UIDrawable children to the grid + print("\nTest 2: Adding UIDrawable children...") + + # Speech bubble style caption - positioned in grid-world pixels + # At cell (5, 3) which is 5*16=80, 3*16=48 in pixels + caption = mcrfpy.Caption(text="Hello!", pos=(80, 48)) + caption.fill_color = mcrfpy.Color(255, 255, 200) + caption.outline = 1 + caption.outline_color = mcrfpy.Color(0, 0, 0) + grid.children.append(caption) + print(f" Added caption at (80, 48)") + + # A highlight circle around cell (10, 7) = (160, 112) + # Circle needs center, not pos + circle = mcrfpy.Circle(center=(168, 120), radius=20, + fill_color=mcrfpy.Color(255, 255, 0, 100), + outline_color=mcrfpy.Color(255, 255, 0), + outline=2) + grid.children.append(circle) + print(f" Added highlight circle at (168, 120)") + + # A line indicating a path from (2,2) to (8,6) + # In pixels: (32, 32) to (128, 96) + line = mcrfpy.Line(start=(32, 32), end=(128, 96), + color=mcrfpy.Color(0, 255, 0), thickness=3) + grid.children.append(line) + print(f" Added path line from (32,32) to (128,96)") + + # An arc for range indicator at (15, 10) = (240, 160) + arc = mcrfpy.Arc(center=(240, 160), radius=40, start_angle=0, end_angle=270, + color=mcrfpy.Color(255, 0, 255), thickness=4) + grid.children.append(arc) + print(f" Added range arc at (240, 160)") + + # Test 3: Verify children count + print(f"\nTest 3: Verifying children count...") + print(f" grid.children count = {len(grid.children)}") + assert len(grid.children) == 4, f"Expected 4 children, got {len(grid.children)}" + + # Test 4: Children should be accessible by index + print("\nTest 4: Accessing children by index...") + child0 = grid.children[0] + print(f" grid.children[0] = {child0}") + child1 = grid.children[1] + print(f" grid.children[1] = {child1}") + + # Test 5: Modify a child's position (should update in grid) + print("\nTest 5: Modifying child position...") + original_pos = (caption.pos.x, caption.pos.y) + caption.pos = mcrfpy.Vector(90, 58) + new_pos = (caption.pos.x, caption.pos.y) + print(f" Moved caption from {original_pos} to {new_pos}") + + # Test 6: Test z_index for children + print("\nTest 6: Testing z_index ordering...") + # Add overlapping elements with different z_index + frame1 = mcrfpy.Frame(pos=(150, 80), size=(40, 40)) + frame1.fill_color = mcrfpy.Color(255, 0, 0, 200) + frame1.z_index = 10 + grid.children.append(frame1) + + frame2 = mcrfpy.Frame(pos=(160, 90), size=(40, 40)) + frame2.fill_color = mcrfpy.Color(0, 255, 0, 200) + frame2.z_index = 5 # Lower z_index, rendered first (behind) + grid.children.append(frame2) + print(f" Added overlapping frames: red z=10, green z=5") + + # Test 7: Test visibility + print("\nTest 7: Testing child visibility...") + frame3 = mcrfpy.Frame(pos=(50, 150), size=(30, 30)) + frame3.fill_color = mcrfpy.Color(0, 0, 255) + frame3.visible = False + grid.children.append(frame3) + print(f" Added invisible blue frame (should not appear)") + + # Test 8: Pan the grid and verify children move with it + print("\nTest 8: Testing pan (children should follow grid camera)...") + # Center the view on cell (10, 7.5) - default was grid center + grid.center = (160, 120) # Center on pixel (160, 120) + print(f" Centered grid on (160, 120)") + + # Test 9: Test zoom + print("\nTest 9: Testing zoom...") + grid.zoom = 1.5 + print(f" Set zoom to 1.5") + + print(f"\nFinal children count: {len(grid.children)}") + + # Schedule screenshot for next frame + mcrfpy.setTimer("screenshot", take_screenshot, 100) + +# Create a test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 50)