diff --git a/.archive/caption_invisible.png b/.archive/caption_invisible.png deleted file mode 100644 index e75647b..0000000 Binary files a/.archive/caption_invisible.png and /dev/null differ diff --git a/.archive/caption_moved.png b/.archive/caption_moved.png deleted file mode 100644 index e75647b..0000000 Binary files a/.archive/caption_moved.png and /dev/null differ diff --git a/.archive/caption_opacity_0.png b/.archive/caption_opacity_0.png deleted file mode 100644 index e75647b..0000000 Binary files a/.archive/caption_opacity_0.png and /dev/null differ diff --git a/.archive/caption_opacity_25.png b/.archive/caption_opacity_25.png deleted file mode 100644 index e75647b..0000000 Binary files a/.archive/caption_opacity_25.png and /dev/null differ diff --git a/.archive/caption_opacity_50.png b/.archive/caption_opacity_50.png deleted file mode 100644 index e75647b..0000000 Binary files a/.archive/caption_opacity_50.png and /dev/null differ diff --git a/.archive/caption_visible.png b/.archive/caption_visible.png deleted file mode 100644 index e75647b..0000000 Binary files a/.archive/caption_visible.png and /dev/null differ diff --git a/.archive/debug_immediate.png b/.archive/debug_immediate.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/debug_immediate.png and /dev/null differ diff --git a/.archive/debug_multi_0.png b/.archive/debug_multi_0.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/debug_multi_0.png and /dev/null differ diff --git a/.archive/debug_multi_1.png b/.archive/debug_multi_1.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/debug_multi_1.png and /dev/null differ diff --git a/.archive/debug_multi_2.png b/.archive/debug_multi_2.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/debug_multi_2.png and /dev/null differ diff --git a/.archive/entity_property_setters_test.py b/.archive/entity_property_setters_test.py deleted file mode 100644 index b912b43..0000000 --- a/.archive/entity_property_setters_test.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Entity property setters - fixing "new style getargs format" error - -Verifies that Entity position and sprite_number setters work correctly. -""" - -def test_entity_setters(timer_name): - """Test that Entity property setters work correctly""" - import mcrfpy - - print("Testing Entity property setters...") - - # Create test scene and grid - mcrfpy.createScene("entity_test") - ui = mcrfpy.sceneUI("entity_test") - - # Create grid with texture - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) - ui.append(grid) - - # Create entity - initial_pos = mcrfpy.Vector(2.5, 3.5) - entity = mcrfpy.Entity(initial_pos, texture, 5, grid) - grid.entities.append(entity) - - print(f"✓ Created entity at position {entity.pos}") - - # Test position setter with Vector - new_pos = mcrfpy.Vector(4.0, 5.0) - try: - entity.pos = new_pos - assert entity.pos.x == 4.0, f"Expected x=4.0, got {entity.pos.x}" - assert entity.pos.y == 5.0, f"Expected y=5.0, got {entity.pos.y}" - print(f"✓ Position setter works with Vector: {entity.pos}") - except Exception as e: - print(f"✗ Position setter failed: {e}") - raise - - # Test position setter with tuple (should also work via PyVector::from_arg) - try: - entity.pos = (7.5, 8.5) - assert entity.pos.x == 7.5, f"Expected x=7.5, got {entity.pos.x}" - assert entity.pos.y == 8.5, f"Expected y=8.5, got {entity.pos.y}" - print(f"✓ Position setter works with tuple: {entity.pos}") - except Exception as e: - print(f"✗ Position setter with tuple failed: {e}") - raise - - # Test draw_pos setter (collision position) - try: - entity.draw_pos = mcrfpy.Vector(3, 4) - assert entity.draw_pos.x == 3, f"Expected x=3, got {entity.draw_pos.x}" - assert entity.draw_pos.y == 4, f"Expected y=4, got {entity.draw_pos.y}" - print(f"✓ Draw position setter works: {entity.draw_pos}") - except Exception as e: - print(f"✗ Draw position setter failed: {e}") - raise - - # Test sprite_number setter - try: - entity.sprite_number = 10 - assert entity.sprite_number == 10, f"Expected sprite_number=10, got {entity.sprite_number}" - print(f"✓ Sprite number setter works: {entity.sprite_number}") - except Exception as e: - print(f"✗ Sprite number setter failed: {e}") - raise - - # Test invalid position setter (should raise TypeError) - try: - entity.pos = "invalid" - print("✗ Position setter should have raised TypeError for string") - assert False, "Should have raised TypeError" - except TypeError as e: - print(f"✓ Position setter correctly rejects invalid type: {e}") - except Exception as e: - print(f"✗ Unexpected error: {e}") - raise - - # Test invalid sprite number (should raise TypeError) - try: - entity.sprite_number = "invalid" - print("✗ Sprite number setter should have raised TypeError for string") - assert False, "Should have raised TypeError" - except TypeError as e: - print(f"✓ Sprite number setter correctly rejects invalid type: {e}") - except Exception as e: - print(f"✗ Unexpected error: {e}") - raise - - # Cleanup timer - mcrfpy.delTimer("test_timer") - - print("\n✅ Entity property setters test PASSED - All setters work correctly") - -# Execute the test after a short delay to ensure window is ready -import mcrfpy -mcrfpy.setTimer("test_timer", test_entity_setters, 100) \ No newline at end of file diff --git a/.archive/entity_setter_simple_test.py b/.archive/entity_setter_simple_test.py deleted file mode 100644 index e9b9fbb..0000000 --- a/.archive/entity_setter_simple_test.py +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple test for Entity property setters -""" - -def test_entity_setters(timer_name): - """Test Entity property setters""" - import mcrfpy - import sys - - print("Testing Entity property setters...") - - # Create test scene and grid - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create grid with texture - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) - ui.append(grid) - - # Create entity - entity = mcrfpy.Entity((2.5, 3.5), texture, 5, grid) - grid.entities.append(entity) - - # Test 1: Initial position - print(f"Initial position: {entity.pos}") - print(f"Initial position x={entity.pos.x}, y={entity.pos.y}") - - # Test 2: Set position with Vector - entity.pos = mcrfpy.Vector(4.0, 5.0) - print(f"After Vector setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}") - - # Test 3: Set position with tuple - entity.pos = (7.5, 8.5) - print(f"After tuple setter: pos={entity.pos}, x={entity.pos.x}, y={entity.pos.y}") - - # Test 4: sprite_number - print(f"Initial sprite_number: {entity.sprite_number}") - entity.sprite_number = 10 - print(f"After setter: sprite_number={entity.sprite_number}") - - # Test 5: Invalid types - try: - entity.pos = "invalid" - print("ERROR: Should have raised TypeError") - except TypeError as e: - print(f"✓ Correctly rejected invalid position: {e}") - - try: - entity.sprite_number = "invalid" - print("ERROR: Should have raised TypeError") - except TypeError as e: - print(f"✓ Correctly rejected invalid sprite_number: {e}") - - print("\n✅ Entity property setters test completed") - sys.exit(0) - -# Execute the test after a short delay -import mcrfpy -mcrfpy.setTimer("test", test_entity_setters, 100) \ No newline at end of file diff --git a/.archive/grid_none_texture_test_197.png b/.archive/grid_none_texture_test_197.png deleted file mode 100644 index fe3210d..0000000 Binary files a/.archive/grid_none_texture_test_197.png and /dev/null differ diff --git a/.archive/issue27_entity_extend_test.py b/.archive/issue27_entity_extend_test.py deleted file mode 100644 index 41fd744..0000000 --- a/.archive/issue27_entity_extend_test.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #27: EntityCollection.extend() method - -Verifies that EntityCollection can extend with multiple entities at once. -""" - -def test_entity_extend(timer_name): - """Test that EntityCollection.extend() method works correctly""" - import mcrfpy - import sys - - print("Issue #27 test: EntityCollection.extend() method") - - # Create test scene and grid - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create grid with texture - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) - ui.append(grid) - - # Add some initial entities - entity1 = mcrfpy.Entity((1, 1), texture, 1, grid) - entity2 = mcrfpy.Entity((2, 2), texture, 2, grid) - grid.entities.append(entity1) - grid.entities.append(entity2) - - print(f"✓ Initial entities: {len(grid.entities)}") - - # Test 1: Extend with a list of entities - new_entities = [ - mcrfpy.Entity((3, 3), texture, 3, grid), - mcrfpy.Entity((4, 4), texture, 4, grid), - mcrfpy.Entity((5, 5), texture, 5, grid) - ] - - try: - grid.entities.extend(new_entities) - assert len(grid.entities) == 5, f"Expected 5 entities, got {len(grid.entities)}" - print(f"✓ Extended with list: now {len(grid.entities)} entities") - except Exception as e: - print(f"✗ Failed to extend with list: {e}") - raise - - # Test 2: Extend with a tuple - more_entities = ( - mcrfpy.Entity((6, 6), texture, 6, grid), - mcrfpy.Entity((7, 7), texture, 7, grid) - ) - - try: - grid.entities.extend(more_entities) - assert len(grid.entities) == 7, f"Expected 7 entities, got {len(grid.entities)}" - print(f"✓ Extended with tuple: now {len(grid.entities)} entities") - except Exception as e: - print(f"✗ Failed to extend with tuple: {e}") - raise - - # Test 3: Extend with generator expression - try: - grid.entities.extend(mcrfpy.Entity((8, i), texture, 8+i, grid) for i in range(3)) - assert len(grid.entities) == 10, f"Expected 10 entities, got {len(grid.entities)}" - print(f"✓ Extended with generator: now {len(grid.entities)} entities") - except Exception as e: - print(f"✗ Failed to extend with generator: {e}") - raise - - # Test 4: Verify all entities have correct grid association - for i, entity in enumerate(grid.entities): - # Just checking that we can iterate and access them - assert entity.sprite_number >= 1, f"Entity {i} has invalid sprite number" - print("✓ All entities accessible and valid") - - # Test 5: Invalid input - non-iterable - try: - grid.entities.extend(42) - print("✗ Should have raised TypeError for non-iterable") - except TypeError as e: - print(f"✓ Correctly rejected non-iterable: {e}") - - # Test 6: Invalid input - iterable with non-Entity - try: - grid.entities.extend([entity1, "not an entity", entity2]) - print("✗ Should have raised TypeError for non-Entity in iterable") - except TypeError as e: - print(f"✓ Correctly rejected non-Entity in iterable: {e}") - - # Test 7: Empty iterable (should work) - initial_count = len(grid.entities) - try: - grid.entities.extend([]) - assert len(grid.entities) == initial_count, "Empty extend changed count" - print("✓ Empty extend works correctly") - except Exception as e: - print(f"✗ Empty extend failed: {e}") - raise - - print(f"\n✅ Issue #27 test PASSED - EntityCollection.extend() works correctly") - sys.exit(0) - -# Execute the test after a short delay -import mcrfpy -mcrfpy.setTimer("test", test_entity_extend, 100) \ No newline at end of file diff --git a/.archive/issue33_sprite_index_validation_test.py b/.archive/issue33_sprite_index_validation_test.py deleted file mode 100644 index 4e321dd..0000000 --- a/.archive/issue33_sprite_index_validation_test.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #33: Sprite index validation - -Verifies that Sprite and Entity objects validate sprite indices -against the texture's actual sprite count. -""" - -def test_sprite_index_validation(timer_name): - """Test that sprite index validation works correctly""" - import mcrfpy - import sys - - print("Issue #33 test: Sprite index validation") - - # Create test scene - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create texture - kenney_ice.png is 11x12 sprites of 16x16 each - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - # Total sprites = 11 * 12 = 132 sprites (indices 0-131) - - # Test 1: Create sprite with valid index - try: - sprite = mcrfpy.Sprite(100, 100, texture, 50) # Valid index - ui.append(sprite) - print(f"✓ Created sprite with valid index 50") - except Exception as e: - print(f"✗ Failed to create sprite with valid index: {e}") - raise - - # Test 2: Set valid sprite index - try: - sprite.sprite_number = 100 # Still valid - assert sprite.sprite_number == 100 - print(f"✓ Set sprite to valid index 100") - except Exception as e: - print(f"✗ Failed to set valid sprite index: {e}") - raise - - # Test 3: Set maximum valid index - try: - sprite.sprite_number = 131 # Maximum valid index - assert sprite.sprite_number == 131 - print(f"✓ Set sprite to maximum valid index 131") - except Exception as e: - print(f"✗ Failed to set maximum valid index: {e}") - raise - - # Test 4: Invalid negative index - try: - sprite.sprite_number = -1 - print("✗ Should have raised ValueError for negative index") - except ValueError as e: - print(f"✓ Correctly rejected negative index: {e}") - except Exception as e: - print(f"✗ Wrong exception type for negative index: {e}") - raise - - # Test 5: Invalid index too large - try: - sprite.sprite_number = 132 # One past the maximum - print("✗ Should have raised ValueError for index 132") - except ValueError as e: - print(f"✓ Correctly rejected out-of-bounds index: {e}") - except Exception as e: - print(f"✗ Wrong exception type for out-of-bounds index: {e}") - raise - - # Test 6: Very large invalid index - try: - sprite.sprite_number = 1000 - print("✗ Should have raised ValueError for index 1000") - except ValueError as e: - print(f"✓ Correctly rejected large invalid index: {e}") - - # Test 7: Entity sprite_number validation - grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) - ui.append(grid) - - entity = mcrfpy.Entity((5, 5), texture, 50, grid) - grid.entities.append(entity) - - try: - entity.sprite_number = 200 # Out of bounds - print("✗ Entity should also validate sprite indices") - except ValueError as e: - print(f"✓ Entity also validates sprite indices: {e}") - except Exception as e: - # Entity might not have the same validation yet - print(f"Note: Entity validation not implemented yet: {e}") - - # Test 8: Different texture sizes - # Create a smaller texture to test different bounds - small_texture = mcrfpy.Texture("assets/Sprite-0001.png", 32, 32) - small_sprite = mcrfpy.Sprite(200, 200, small_texture, 0) - - # This texture might have fewer sprites, test accordingly - try: - small_sprite.sprite_number = 100 # Might be out of bounds - print("Note: Small texture accepted index 100") - except ValueError as e: - print(f"✓ Small texture has different bounds: {e}") - - print(f"\n✅ Issue #33 test PASSED - Sprite index validation works correctly") - sys.exit(0) - -# Execute the test after a short delay -import mcrfpy -mcrfpy.setTimer("test", test_sprite_index_validation, 100) \ No newline at end of file diff --git a/.archive/issue73_entity_index_test.py b/.archive/issue73_entity_index_test.py deleted file mode 100644 index 18662ec..0000000 --- a/.archive/issue73_entity_index_test.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #73: Entity.index() method for removal - -Verifies that Entity objects can report their index in the grid's entity collection. -""" - -def test_entity_index(timer_name): - """Test that Entity.index() method works correctly""" - import mcrfpy - import sys - - print("Issue #73 test: Entity.index() method") - - # Create test scene and grid - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create grid with texture - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) - ui.append(grid) - - # Create multiple entities - entities = [] - for i in range(5): - entity = mcrfpy.Entity((i, i), texture, i, grid) - entities.append(entity) - grid.entities.append(entity) - - print(f"✓ Created {len(entities)} entities") - - # Test 1: Check each entity knows its index - for expected_idx, entity in enumerate(entities): - try: - actual_idx = entity.index() - assert actual_idx == expected_idx, f"Expected index {expected_idx}, got {actual_idx}" - print(f"✓ Entity {expected_idx} correctly reports index {actual_idx}") - except Exception as e: - print(f"✗ Entity {expected_idx} index() failed: {e}") - raise - - # Test 2: Remove entity using index - entity_to_remove = entities[2] - remove_idx = entity_to_remove.index() - grid.entities.remove(remove_idx) - print(f"✓ Removed entity at index {remove_idx}") - - # Test 3: Verify indices updated after removal - for i, entity in enumerate(entities): - if i == 2: - # This entity was removed, should raise error - try: - idx = entity.index() - print(f"✗ Removed entity still reports index {idx}") - except ValueError as e: - print(f"✓ Removed entity correctly raises error: {e}") - elif i < 2: - # These entities should keep their indices - idx = entity.index() - assert idx == i, f"Entity before removal has wrong index: {idx}" - else: - # These entities should have shifted down by 1 - idx = entity.index() - assert idx == i - 1, f"Entity after removal has wrong index: {idx}" - - # Test 4: Entity without grid - orphan_entity = mcrfpy.Entity((0, 0), texture, 0, None) - try: - idx = orphan_entity.index() - print(f"✗ Orphan entity should raise error but returned {idx}") - except RuntimeError as e: - print(f"✓ Orphan entity correctly raises error: {e}") - - # Test 5: Use index() in practical removal pattern - # Add some new entities - for i in range(3): - entity = mcrfpy.Entity((7+i, 7+i), texture, 10+i, grid) - grid.entities.append(entity) - - # Remove entities with sprite_number > 10 - removed_count = 0 - i = 0 - while i < len(grid.entities): - entity = grid.entities[i] - if entity.sprite_number > 10: - grid.entities.remove(entity.index()) - removed_count += 1 - # Don't increment i, as entities shifted down - else: - i += 1 - - print(f"✓ Removed {removed_count} entities using index() in loop") - assert len(grid.entities) == 5, f"Expected 5 entities remaining, got {len(grid.entities)}" - - print("\n✅ Issue #73 test PASSED - Entity.index() method works correctly") - sys.exit(0) - -# Execute the test after a short delay -import mcrfpy -mcrfpy.setTimer("test", test_entity_index, 100) \ No newline at end of file diff --git a/.archive/issue73_simple_index_test.py b/.archive/issue73_simple_index_test.py deleted file mode 100644 index a206f65..0000000 --- a/.archive/issue73_simple_index_test.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 -""" -Simple test for Issue #73: Entity.index() method -""" - -def test_entity_index(timer_name): - """Test that Entity.index() method works correctly""" - import mcrfpy - import sys - - print("Testing Entity.index() method...") - - # Create test scene and grid - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create grid with texture - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(10, 10, texture, (10, 10), (400, 400)) - ui.append(grid) - - # Clear any existing entities - while len(grid.entities) > 0: - grid.entities.remove(0) - - # Create entities - entity1 = mcrfpy.Entity((1, 1), texture, 1, grid) - entity2 = mcrfpy.Entity((2, 2), texture, 2, grid) - entity3 = mcrfpy.Entity((3, 3), texture, 3, grid) - - grid.entities.append(entity1) - grid.entities.append(entity2) - grid.entities.append(entity3) - - print(f"Created {len(grid.entities)} entities") - - # Test index() method - idx1 = entity1.index() - idx2 = entity2.index() - idx3 = entity3.index() - - print(f"Entity 1 index: {idx1}") - print(f"Entity 2 index: {idx2}") - print(f"Entity 3 index: {idx3}") - - assert idx1 == 0, f"Entity 1 should be at index 0, got {idx1}" - assert idx2 == 1, f"Entity 2 should be at index 1, got {idx2}" - assert idx3 == 2, f"Entity 3 should be at index 2, got {idx3}" - - print("✓ All entities report correct indices") - - # Test removal using index - remove_idx = entity2.index() - grid.entities.remove(remove_idx) - print(f"✓ Removed entity at index {remove_idx}") - - # Check remaining entities - assert len(grid.entities) == 2 - assert entity1.index() == 0 - assert entity3.index() == 1 # Should have shifted down - - print("✓ Indices updated correctly after removal") - - # Test entity not in grid - orphan = mcrfpy.Entity((5, 5), texture, 5, None) - try: - idx = orphan.index() - print(f"✗ Orphan entity should raise error but returned {idx}") - except RuntimeError as e: - print(f"✓ Orphan entity correctly raises error") - - print("\n✅ Entity.index() test PASSED") - sys.exit(0) - -# Execute the test after a short delay -import mcrfpy -mcrfpy.setTimer("test", test_entity_index, 100) \ No newline at end of file diff --git a/.archive/issue74_grid_xy_properties_test.py b/.archive/issue74_grid_xy_properties_test.py deleted file mode 100644 index 590c14e..0000000 --- a/.archive/issue74_grid_xy_properties_test.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Issue #74: Add missing Grid.grid_y property - -Verifies that Grid objects expose grid_x and grid_y properties correctly. -""" - -def test_grid_xy_properties(timer_name): - """Test that Grid has grid_x and grid_y properties""" - import mcrfpy - - # Test was run - print("Issue #74 test: Grid.grid_x and Grid.grid_y properties") - - # Test with texture - texture = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - grid = mcrfpy.Grid(20, 15, texture, (0, 0), (800, 600)) - - # Test grid_x property - assert hasattr(grid, 'grid_x'), "Grid should have grid_x property" - assert grid.grid_x == 20, f"Expected grid_x=20, got {grid.grid_x}" - print(f"✓ grid.grid_x = {grid.grid_x}") - - # Test grid_y property - assert hasattr(grid, 'grid_y'), "Grid should have grid_y property" - assert grid.grid_y == 15, f"Expected grid_y=15, got {grid.grid_y}" - print(f"✓ grid.grid_y = {grid.grid_y}") - - # Test grid_size still works - assert hasattr(grid, 'grid_size'), "Grid should still have grid_size property" - assert grid.grid_size == (20, 15), f"Expected grid_size=(20, 15), got {grid.grid_size}" - print(f"✓ grid.grid_size = {grid.grid_size}") - - # Test without texture - grid2 = mcrfpy.Grid(30, 25, None, (10, 10), (480, 400)) - assert grid2.grid_x == 30, f"Expected grid_x=30, got {grid2.grid_x}" - assert grid2.grid_y == 25, f"Expected grid_y=25, got {grid2.grid_y}" - assert grid2.grid_size == (30, 25), f"Expected grid_size=(30, 25), got {grid2.grid_size}" - print("✓ Grid without texture also has correct grid_x and grid_y") - - # Test using in error message context (original issue) - try: - grid.at((-1, 0)) # Should raise error - except ValueError as e: - error_msg = str(e) - assert "Grid.grid_x" in error_msg, f"Error message should reference Grid.grid_x: {error_msg}" - print(f"✓ Error message correctly references Grid.grid_x: {error_msg}") - - try: - grid.at((0, -1)) # Should raise error - except ValueError as e: - error_msg = str(e) - assert "Grid.grid_y" in error_msg, f"Error message should reference Grid.grid_y: {error_msg}" - print(f"✓ Error message correctly references Grid.grid_y: {error_msg}") - - print("\n✅ Issue #74 test PASSED - Grid.grid_x and Grid.grid_y properties work correctly") - -# Execute the test after a short delay to ensure window is ready -import mcrfpy -mcrfpy.setTimer("test_timer", test_grid_xy_properties, 100) \ No newline at end of file diff --git a/.archive/issue78_fixed_1658.png b/.archive/issue78_fixed_1658.png deleted file mode 100644 index 1e7680a..0000000 Binary files a/.archive/issue78_fixed_1658.png and /dev/null differ diff --git a/.archive/issue78_middle_click_fix_test.py b/.archive/issue78_middle_click_fix_test.py deleted file mode 100644 index fac4f18..0000000 --- a/.archive/issue78_middle_click_fix_test.py +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python3 -"""Test that Issue #78 is fixed - Middle Mouse Click should NOT send 'C' keyboard event""" -import mcrfpy -from mcrfpy import automation -import sys - -# Track events -keyboard_events = [] -click_events = [] - -def keyboard_handler(key): - """Track keyboard events""" - keyboard_events.append(key) - print(f"Keyboard event received: '{key}'") - -def click_handler(x, y, button): - """Track click events""" - click_events.append((x, y, button)) - print(f"Click event received: ({x}, {y}, button={button})") - -def test_middle_click_fix(runtime): - """Test that middle click no longer sends 'C' key event""" - print(f"\n=== Testing Issue #78 Fix (runtime: {runtime}) ===") - - # Simulate middle click - print("\nSimulating middle click at (200, 200)...") - automation.middleClick(200, 200) - - # Also test other clicks for comparison - print("Simulating left click at (100, 100)...") - automation.click(100, 100) - - print("Simulating right click at (300, 300)...") - automation.rightClick(300, 300) - - # Wait a moment for events to process - mcrfpy.setTimer("check_results", check_results, 500) - -def check_results(runtime): - """Check if the bug is fixed""" - print(f"\n=== Results ===") - print(f"Keyboard events received: {len(keyboard_events)}") - print(f"Click events received: {len(click_events)}") - - # Check if 'C' was incorrectly triggered - if 'C' in keyboard_events or 'c' in keyboard_events: - print("\n✗ FAIL - Issue #78 still exists: Middle click triggered 'C' keyboard event!") - print(f"Keyboard events: {keyboard_events}") - else: - print("\n✓ PASS - Issue #78 is FIXED: No spurious 'C' keyboard event from middle click!") - - # Take screenshot - filename = f"issue78_fixed_{int(runtime)}.png" - automation.screenshot(filename) - print(f"\nScreenshot saved: {filename}") - - # Cleanup and exit - mcrfpy.delTimer("check_results") - sys.exit(0) - -# Set up test scene -print("Setting up test scene...") -mcrfpy.createScene("issue78_test") -mcrfpy.setScene("issue78_test") -ui = mcrfpy.sceneUI("issue78_test") - -# Register keyboard handler -mcrfpy.keypressScene(keyboard_handler) - -# Create a clickable frame -frame = mcrfpy.Frame(50, 50, 400, 400, - fill_color=mcrfpy.Color(100, 150, 200), - outline_color=mcrfpy.Color(255, 255, 255), - outline=3.0) -frame.click = click_handler -ui.append(frame) - -# Add label -caption = mcrfpy.Caption(mcrfpy.Vector(100, 100), - text="Issue #78 Test - Middle Click", - fill_color=mcrfpy.Color(255, 255, 255)) -caption.size = 24 -ui.append(caption) - -# Schedule test -print("Scheduling test to run after render loop starts...") -mcrfpy.setTimer("test", test_middle_click_fix, 1000) \ No newline at end of file diff --git a/.archive/screenshot_opaque_fix_20250703_174829.png b/.archive/screenshot_opaque_fix_20250703_174829.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/screenshot_opaque_fix_20250703_174829.png and /dev/null differ diff --git a/.archive/sequence_demo_screenshot.png b/.archive/sequence_demo_screenshot.png deleted file mode 100644 index 8dd48de..0000000 Binary files a/.archive/sequence_demo_screenshot.png and /dev/null differ diff --git a/.archive/sequence_protocol_test.png b/.archive/sequence_protocol_test.png deleted file mode 100644 index 158f93f..0000000 Binary files a/.archive/sequence_protocol_test.png and /dev/null differ diff --git a/.archive/sprite_texture_setter_test.py b/.archive/sprite_texture_setter_test.py deleted file mode 100644 index fb6019c..0000000 --- a/.archive/sprite_texture_setter_test.py +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env python3 -""" -Test for Sprite texture setter - fixing "error return without exception set" -""" - -def test_sprite_texture_setter(timer_name): - """Test that Sprite texture setter works correctly""" - import mcrfpy - import sys - - print("Testing Sprite texture setter...") - - # Create test scene - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create textures - texture1 = mcrfpy.Texture("assets/kenney_ice.png", 16, 16) - texture2 = mcrfpy.Texture("assets/kenney_lava.png", 16, 16) - - # Create sprite with first texture - sprite = mcrfpy.Sprite(100, 100, texture1, 5) - ui.append(sprite) - - # Test getting texture - try: - current_texture = sprite.texture - print(f"✓ Got texture: {current_texture}") - except Exception as e: - print(f"✗ Failed to get texture: {e}") - raise - - # Test setting new texture - try: - sprite.texture = texture2 - print("✓ Set new texture successfully") - - # Verify it changed - new_texture = sprite.texture - if new_texture != texture2: - print(f"✗ Texture didn't change properly") - else: - print("✓ Texture changed correctly") - except Exception as e: - print(f"✗ Failed to set texture: {e}") - raise - - # Test invalid texture type - try: - sprite.texture = "invalid" - print("✗ Should have raised TypeError for invalid texture") - except TypeError as e: - print(f"✓ Correctly rejected invalid texture: {e}") - except Exception as e: - print(f"✗ Wrong exception type: {e}") - raise - - # Test None texture - try: - sprite.texture = None - print("✗ Should have raised TypeError for None texture") - except TypeError as e: - print(f"✓ Correctly rejected None texture: {e}") - - # Test that sprite still renders correctly - print("✓ Sprite still renders with new texture") - - print("\n✅ Sprite texture setter test PASSED") - sys.exit(0) - -# Execute the test after a short delay -import mcrfpy -mcrfpy.setTimer("test", test_sprite_texture_setter, 100) \ No newline at end of file diff --git a/.archive/timer_success_1086.png b/.archive/timer_success_1086.png deleted file mode 100644 index a09f8d5..0000000 Binary files a/.archive/timer_success_1086.png and /dev/null differ diff --git a/.archive/validate_screenshot_basic_20250703_174532.png b/.archive/validate_screenshot_basic_20250703_174532.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/validate_screenshot_basic_20250703_174532.png and /dev/null differ diff --git a/.archive/validate_screenshot_final_20250703_174532.png b/.archive/validate_screenshot_final_20250703_174532.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/validate_screenshot_final_20250703_174532.png and /dev/null differ diff --git a/.archive/validate_screenshot_with_spaces 20250703_174532.png b/.archive/validate_screenshot_with_spaces 20250703_174532.png deleted file mode 100644 index a61c929..0000000 Binary files a/.archive/validate_screenshot_with_spaces 20250703_174532.png and /dev/null differ diff --git a/ALPHA_STREAMLINE_WORKLOG.md b/ALPHA_STREAMLINE_WORKLOG.md deleted file mode 100644 index e6ada2b..0000000 --- a/ALPHA_STREAMLINE_WORKLOG.md +++ /dev/null @@ -1,1093 +0,0 @@ -# Alpha Streamline 2 Work Log - -## Phase 6: Rendering Revolution - -### Task: RenderTexture Base Infrastructure (#6 - Part 1) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement opt-in RenderTexture support in UIDrawable base class and enable clipping for UIFrame - -**Implementation**: -1. Added RenderTexture infrastructure to UIDrawable: - - `std::unique_ptr render_texture` - - `sf::Sprite render_sprite` - - `bool use_render_texture` and `bool render_dirty` flags - - `enableRenderTexture()` and `markDirty()` methods -2. Implemented clip_children property for UIFrame: - - Python property getter/setter - - Automatic RenderTexture creation when enabled - - Proper handling of nested render contexts -3. Updated UIFrame::render() to support clipping: - - Renders frame and children to RenderTexture when clipping enabled - - Handles coordinate transformations correctly - - Optimizes by only re-rendering when dirty -4. Added dirty flag propagation: - - All property setters call markDirty() - - Size changes recreate RenderTexture - - Animation system integration - -**Technical Details**: -- RenderTexture created lazily on first use -- Size matches frame dimensions, recreated on resize -- Children rendered at local coordinates (0,0) in texture -- Final texture drawn at frame's world position -- Transparent background preserves alpha blending - -**Test Results**: -- Basic clipping works correctly - children are clipped to parent bounds -- Nested clipping (frames within frames) works properly -- Dynamic resizing recreates RenderTexture as needed -- No performance regression for non-clipped frames -- Memory usage reasonable (textures only created when needed) - -**Result**: Foundation laid for advanced rendering features. UIFrame can now clip children to bounds, enabling professional UI layouts. Architecture supports future effects like blur, glow, and shaders. - ---- - -### Task: Grid Background Colors (#50) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add customizable background color to UIGrid - -**Implementation**: -1. Added `sf::Color background_color` member to UIGrid class -2. Implemented Python property getter/setter for background_color -3. Updated UIGrid::render() to clear RenderTexture with background color -4. Added animation support for individual color components: - - background_color.r, background_color.g, background_color.b, background_color.a -5. Default background color set to dark gray (8, 8, 8, 255) - -**Test Results**: -- Background color properly renders behind grid content -- Python property access works correctly -- Color animation would work with Animation system -- No performance impact - -**Result**: Quick win completed. Grids now have customizable background colors, improving visual flexibility for game developers. - ---- - -## Phase 5: Window/Scene Architecture - -### Task: Window Object Singleton (#34) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement Window singleton object with access to resolution, fullscreen, vsync properties - -**Implementation**: -1. Created PyWindow.h/cpp with singleton pattern -2. Window.get() class method returns singleton instance -3. Properties implemented: - - resolution: Get/set window resolution as (width, height) tuple - - fullscreen: Toggle fullscreen mode - - vsync: Enable/disable vertical sync - - title: Get/set window title string - - visible: Window visibility state - - framerate_limit: FPS limit (0 for unlimited) -4. Methods implemented: - - center(): Center window on screen - - screenshot(filename=None): Take screenshot to file or return bytes -5. Proper handling for headless mode - -**Technical Details**: -- Uses static singleton instance -- Window properties tracked in GameEngine for persistence -- Resolution/fullscreen changes recreate window with SFML -- Screenshot supports both RenderWindow and RenderTexture targets - -**Test Results**: -- Singleton pattern works correctly -- All properties accessible and modifiable -- Screenshot functionality works in both modes -- Center method appropriately fails in headless mode - -**Result**: Window singleton provides clean Python API for window control. Games can now easily manage window properties and take screenshots. - ---- - -### Task: Object-Oriented Scene Support (#61) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Create Python Scene class that can be subclassed with methods like on_keypress(), on_enter(), on_exit() - -**Implementation**: -1. Created PySceneObject.h/cpp with Python Scene type -2. Scene class features: - - Can be subclassed in Python - - Constructor creates underlying C++ PyScene - - Lifecycle methods: on_enter(), on_exit(), on_keypress(key, state), update(dt) - - Properties: name (string), active (bool) - - Methods: activate(), get_ui(), register_keyboard(callable) -3. Integration with GameEngine: - - changeScene() triggers on_exit/on_enter callbacks - - update() called each frame with delta time - - Maintains registry of Python scene objects -4. Backward compatibility maintained with existing scene API - -**Technical Details**: -- PySceneObject wraps C++ PyScene -- Python objects stored in static registry by name -- GIL management for thread-safe callbacks -- Lifecycle events triggered from C++ side -- Update loop integrated with game loop - -**Usage Example**: -```python -class MenuScene(mcrfpy.Scene): - def __init__(self): - super().__init__("menu") - # Create UI elements - - def on_enter(self): - print("Entering menu") - - def on_keypress(self, key, state): - if key == "Space" and state == "start": - mcrfpy.setScene("game") - - def update(self, dt): - # Update logic - pass - -menu = MenuScene() -menu.activate() -``` - -**Test Results**: -- Scene creation and subclassing works -- Lifecycle callbacks (on_enter, on_exit) trigger correctly -- update() called each frame with proper delta time -- Scene switching preserves Python object state -- Properties and methods accessible - -**Result**: Object-oriented scenes provide a much more Pythonic and maintainable way to structure game code. Developers can now use inheritance, encapsulation, and clean method overrides instead of registering callback functions. - ---- - -### Task: Window Resize Events (#1) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Enable window resize events to trigger scene.on_resize(width, height) callbacks - -**Implementation**: -1. Added `triggerResize(int width, int height)` to McRFPy_API -2. Enabled window resizing by adding `sf::Style::Resize` to window creation -3. Modified GameEngine::processEvent() to handle resize events: - - Updates the view to match new window size - - Calls McRFPy_API::triggerResize() to notify Python scenes -4. PySceneClass already had `call_on_resize()` method implemented -5. Python Scene objects can override `on_resize(self, width, height)` - -**Technical Details**: -- Window style changed from `Titlebar | Close` to `Titlebar | Close | Resize` -- Resize event updates `visible` view with new dimensions -- Only the active scene receives resize notifications -- Resize callbacks work the same as other lifecycle events - -**Test Results**: -- Window is now resizable by dragging edges/corners -- Python scenes receive resize callbacks with new dimensions -- View properly adjusts to maintain correct coordinate system -- Manual testing required (can't resize in headless mode) - -**Result**: Window resize events are now fully functional. Games can respond to window size changes by overriding the `on_resize` method in their Scene classes. This enables responsive UI layouts and proper view adjustments. - ---- - -### Task: Scene Transitions (#105) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement smooth scene transitions with methods like fade_to() and slide_out() - -**Implementation**: -1. Created SceneTransition class to manage transition state and rendering -2. Added transition support to GameEngine: - - New overload: `changeScene(sceneName, transitionType, duration)` - - Transition types: Fade, SlideLeft, SlideRight, SlideUp, SlideDown - - Renders both scenes to textures during transition - - Smooth easing function for natural motion -3. Extended Python API: - - `mcrfpy.setScene(scene, transition=None, duration=0.0)` - - Transition strings: "fade", "slide_left", "slide_right", "slide_up", "slide_down" -4. Integrated with render loop: - - Transitions update each frame - - Scene lifecycle events trigger after transition completes - - Normal rendering resumes when transition finishes - -**Technical Details**: -- Uses sf::RenderTexture to capture scene states -- Transitions manipulate sprite alpha (fade) or position (slides) -- Easing function: smooth ease-in-out curve -- Duration specified in seconds (float) -- Immediate switch if duration <= 0 or transition is None - -**Test Results**: -- All transition types work correctly -- Smooth animations between scenes -- Lifecycle events (on_exit, on_enter) properly sequenced -- API is clean and intuitive - -**Usage Example**: -```python -# Fade transition over 1 second -mcrfpy.setScene("menu", "fade", 1.0) - -# Slide left transition over 0.5 seconds -mcrfpy.setScene("game", "slide_left", 0.5) - -# Instant transition (no animation) -mcrfpy.setScene("credits") -``` - -**Result**: Scene transitions provide a professional polish to games. The implementation leverages SFML's render textures for smooth, GPU-accelerated transitions. Games can now have cinematic scene changes that enhance the player experience. - ---- - -### Task: SFML Exposure Research (#14) - -**Status**: Research Completed -**Date**: 2025-07-06 - -**Research Summary**: -1. Analyzed current SFML usage in McRogueFace: - - Using SFML 2.6.1 (built from source in modules/SFML) - - Moderate to heavy integration with SFML types throughout codebase - - Already exposing Color, Vector, Font, and Texture to Python - - All rendering, input, and audio systems depend on SFML - -2. Evaluated python-sfml (pysfml): - - Last version 2.3.2 only supports SFML 2.3.2 (incompatible with our 2.6.1) - - Project appears abandoned since ~2019 - - No viable maintained alternatives found - - Installation issues widely reported - -3. Recommendation: **Direct Integration** - - Implement `mcrfpy.sfml` as built-in module - - Maintain API compatibility with python-sfml where sensible - - Gives full control and ensures version compatibility - - Can selectively expose only what makes sense for game scripting - -**Key Findings**: -- Direct integration allows resource sharing between mcrfpy and sfml modules -- Can prevent unsafe operations (e.g., closing the game window) -- Opportunity to provide modern SFML 2.6+ Python bindings -- Implementation phases outlined in SFML_EXPOSURE_RESEARCH.md - -**Result**: Created comprehensive research document recommending direct integration approach with detailed implementation plan. - ---- - -### Task: SFML 3.0 Migration Research - -**Status**: Research Completed -**Date**: 2025-07-06 - -**Research Summary**: -1. SFML 3.0 Release Analysis: - - Released December 21, 2024 (very recent) - - First major version in 12 years - - Requires C++17 (vs C++03 for SFML 2.x) - - Major breaking changes in event system, enums, resource loading - -2. McRogueFace Impact Assessment: - - 40+ source files use SFML directly - - Event handling requires complete rewrite (high impact) - - All keyboard/mouse enums need updating (medium impact) - - Resource loading needs exception handling (medium impact) - - Geometry constructors need updating (low impact) - -3. Key Breaking Changes: - - Event system now uses `std::variant` with `getIf()` API - - All enums are now scoped (e.g., `sf::Keyboard::Key::A`) - - Resource loading via constructors that throw exceptions - - `pollEvent()` returns `std::optional` - - CMake targets now namespaced (e.g., `SFML::Graphics`) - -4. Recommendation: **Defer Migration** - - SFML 3.0 is too new (potential stability issues) - - Migration effort is substantial (especially event system) - - Better to implement `mcrfpy.sfml` with stable SFML 2.6.1 first - - Revisit migration in 6-12 months - -**Key Decisions**: -- Proceed with `mcrfpy.sfml` implementation using SFML 2.6.1 -- Design module API to minimize future breaking changes -- Monitor SFML 3.0 adoption and stability -- Plan migration for late 2025 or early 2026 - -**Result**: Created SFML_3_MIGRATION_RESEARCH.md with comprehensive analysis and migration strategy. - ---- - -## Phase 4: Visibility & Performance - -### Task 3: Basic Profiling/Metrics (#104) - -**Status**: Completed -**Date**: 2025-07-06 - -**Implementation**: -1. Added ProfilingMetrics struct to GameEngine: - - Frame time tracking (current and 60-frame average) - - FPS calculation from average frame time - - Draw call counting per frame - - UI element counting (total and visible) - - Runtime tracking - -2. Integrated metrics collection: - - GameEngine::run() updates frame time metrics each frame - - PyScene::render() counts UI elements and draw calls - - Metrics reset at start of each frame - -3. Exposed metrics to Python: - - Added mcrfpy.getMetrics() function - - Returns dictionary with all metrics - - Accessible from Python scripts for monitoring - -**Features**: -- Real-time frame time and FPS tracking -- 60-frame rolling average for stable FPS display -- Per-frame draw call counting -- UI element counting (total vs visible) -- Total runtime tracking -- Current frame counter - -**Testing**: -- Created test scripts (test_metrics.py, test_metrics_simple.py) -- Verified metrics API is accessible from Python -- Note: Metrics are only populated after game loop starts - -**Result**: Basic profiling system ready for performance monitoring and optimization. - ---- - -### Task 2: Click Handling Improvements - -**Status**: Completed -**Date**: 2025-07-06 - -**Implementation**: -1. Fixed UIFrame coordinate transformation: - - Now correctly subtracts parent position for child coordinates (was adding) - - Checks children in reverse order (highest z-index first) - - Checks bounds first for optimization - - Invisible elements are skipped entirely - -2. Fixed Scene click handling z-order: - - PyScene::do_mouse_input now sorts elements by z-index (highest first) - - Click events stop at the first handler found - - Ensures top-most elements receive clicks first - -3. Implemented UIGrid entity clicking: - - Transforms screen coordinates to grid coordinates - - Checks entities in reverse order - - Returns entity sprite as click target (entities delegate to their sprite) - - Accounts for grid zoom and center position - -**Features**: -- Correct z-order click priority (top elements get clicks first) -- Click transparency (elements without handlers don't block clicks) -- Proper coordinate transformation for nested frames -- Grid entity click detection with coordinate transformation -- Invisible elements don't receive or block clicks - -**Testing**: -- Created comprehensive test suite (test_click_handling.py) -- Tests cannot run in headless mode due to PyScene::do_mouse_input early return -- Manual testing would be required to verify functionality - -**Result**: Click handling now correctly respects z-order, coordinate transforms, and visibility. - ---- - -### Task 1: Name System Implementation (#39/40/41) - -**Status**: Completed -**Date**: 2025-07-06 - -**Implementation**: -1. Added `std::string name` member to UIDrawable base class -2. Implemented get_name/set_name static methods in UIDrawable for Python bindings -3. Added name property to all UI class Python getsetters: - - Frame, Caption, Sprite, Grid: Use UIDrawable::get_name/set_name directly - - Entity: Special handlers that delegate to entity->sprite.name -4. Implemented find() and findAll() functions in McRFPy_API: - - find(name, scene=None) - Returns first element with exact name match - - findAll(pattern, scene=None) - Returns list of elements matching pattern (supports * wildcards) - - Both functions search recursively through Frame children and Grid entities - - Can search current scene or specific named scene - -**Features**: -- All UI elements (Frame, Caption, Sprite, Grid, Entity) support .name property -- Names default to empty string "" -- Names support Unicode characters -- find() returns None if no match found -- findAll() returns empty list if no matches -- Wildcard patterns: "*_frame" matches "main_frame", "sidebar_frame" -- Searches nested elements: Frame children and Grid entities - -**Testing**: -- Created comprehensive test suite (test_name_property.py, test_find_functions.py) -- All tests pass for name property on all UI types -- All tests pass for find/findAll functionality including wildcards - -**Result**: Complete name-based element finding system ready for use. - ---- - -## Phase 1: Foundation Stabilization - -### Task #7: Audit Unsafe Constructors - -**Status**: Completed -**Date**: 2025-07-06 - -**Findings**: -- All UI classes (UIFrame, UICaption, UISprite, UIGrid, UIEntity) have no-argument constructors -- These are required by the Python C API's two-phase initialization pattern: - - `tp_new` creates a default C++ object with `std::make_shared()` - - `tp_init` initializes the object with actual values -- This pattern ensures proper shared_ptr lifetime management and exception safety - -**Decision**: Keep the no-argument constructors but ensure they're safe: -1. Initialize all members to safe defaults -2. Set reasonable default sizes (0,0) and positions (0,0) -3. Ensure no uninitialized pointers - -**Code Changes**: -- UIFrame: Already safe - initializes outline, children, position, and size -- UISprite: Empty constructor - needs safety improvements -- UIGrid: Empty constructor - needs safety improvements -- UIEntity: Empty constructor with TODO comment - needs safety improvements -- UICaption: Uses compiler default - needs explicit constructor with safe defaults - -**Recommendation**: Rather than remove these constructors (which would break Python bindings), we should ensure they initialize all members to safe, predictable values. - -**Implementation**: -1. Added safe default constructors for all UI classes: - - UISprite: Initializes sprite_index=0, ptex=nullptr, position=(0,0), scale=(1,1) - - UIGrid: Initializes all dimensions to 0, creates empty entity list, minimal render texture - - UIEntity: Initializes self=nullptr, grid=nullptr, position=(0,0), collision_pos=(0,0) - - UICaption: Initializes empty text, position=(0,0), size=12, white color - -2. Fixed Python init functions to accept no arguments: - - Changed PyArg_ParseTupleAndKeywords format strings to make all args optional (using |) - - Properly initialized all variables that receive optional arguments - - Added NULL checks for optional PyObject* parameters - - Set sensible defaults when no arguments provided - -**Result**: All UI classes can now be safely instantiated with no arguments from both C++ and Python. - ---- - -### Task #71: Create Python Base Class _Drawable - -**Status**: In Progress -**Date**: 2025-07-06 - -**Implementation**: -1. Created PyDrawable.h/cpp with Python type for _Drawable base class -2. Added properties to UIDrawable base class: - - visible (bool) - #87 - - opacity (float) - #88 -3. Added virtual methods to UIDrawable: - - get_bounds() - returns sf::FloatRect - #89 - - move(dx, dy) - relative movement - #98 - - resize(w, h) - absolute sizing - #98 -4. Implemented these methods in all derived classes: - - UIFrame: Uses box position/size - - UICaption: Uses text bounds, resize is no-op - - UISprite: Uses sprite bounds, resize scales sprite - - UIGrid: Uses box position/size, recreates render texture -5. Updated render methods to check visibility and apply opacity -6. Registered PyDrawableType in McRFPy_API module initialization - -**Decision**: While the C++ implementation is complete, updating the Python type hierarchy to inherit from PyDrawable would require significant refactoring of the existing getsetters. This is deferred to a future phase to avoid breaking existing code. The properties and methods are implemented at the C++ level and will take effect when rendering. - -**Result**: -- C++ UIDrawable base class now has visible (bool) and opacity (float) properties -- All derived classes implement get_bounds(), move(dx,dy), and resize(w,h) methods -- Render methods check visibility and apply opacity where supported -- Python _Drawable type created but not yet used as base class - ---- - -### Task #101: Standardize Default Positions - -**Status**: Completed (already implemented) -**Date**: 2025-07-06 - -**Findings**: All UI classes (Frame, Caption, Sprite, Grid) already default to position (0,0) when position arguments are not provided. This was implemented as part of the safe constructor work in #7. - ---- - -### Task #38: Frame Children Parameter - -**Status**: In Progress -**Date**: 2025-07-06 - -**Goal**: Allow Frame initialization with children parameter: `Frame(x, y, w, h, children=[...])` - -**Implementation**: -1. Added `children` parameter to Frame.__init__ keyword arguments -2. Process children after frame initialization -3. Validate each child is a Frame, Caption, Sprite, or Grid -4. Add valid children to frame's children collection -5. Set children_need_sort flag for z-index sorting - -**Result**: Frames can now be initialized with their children in a single call, making UI construction more concise. - ---- - -### Task #42: Click Handler in __init__ - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Allow setting click handlers during initialization for all UI elements - -**Implementation**: -1. Added `click` parameter to __init__ methods for Frame, Caption, and Sprite -2. Validates that click handler is callable (or None) -3. Registers click handler using existing click_register() method -4. Works alongside other initialization parameters - -**Changes Made**: -- UIFrame: Added click parameter to init, validates and registers handler -- UICaption: Added click parameter to init, validates and registers handler -- UISprite: Added click parameter to init, validates and registers handler -- UIGrid: Already had click parameter support - -**Result**: All UI elements can now have click handlers set during initialization, making interactive UI creation more concise. Lambda functions and other callables work correctly. - ---- - -### Task #90: Grid Size Tuple Support - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Allow Grid to accept grid_size=(width, height) as an alternative to separate grid_x, grid_y arguments - -**Implementation**: -1. Added `grid_size` keyword parameter to Grid.__init__ -2. Accepts either tuple or list of two integers -3. If provided, grid_size overrides any grid_x/grid_y values -4. Maintains backward compatibility with positional grid_x, grid_y arguments - -**Changes Made**: -- Modified UIGrid::init to use PyArg_ParseTupleAndKeywords -- Added parsing logic for grid_size parameter -- Validates that grid_size contains exactly 2 integers -- Falls back to positional arguments if keywords not used - -**Test Results**: -- grid_size tuple works correctly -- grid_size list works correctly -- Traditional grid_x, grid_y still works -- grid_size properly overrides grid_x, grid_y if both provided -- Proper error handling for invalid grid_size values - -**Result**: Grid initialization is now more flexible, allowing either `Grid(10, 15)` or `Grid(grid_size=(10, 15))` syntax - ---- - -### Task #19: Sprite Texture Swapping - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Verify and document sprite texture swapping functionality - -**Findings**: -- Sprite texture swapping was already implemented via the `texture` property -- The getter and setter were already exposed in the Python API -- `setTexture()` method preserves sprite position and scale - -**Implementation Details**: -- UISprite::get_texture returns the texture via pyObject() -- UISprite::set_texture validates the input is a Texture instance -- The C++ setTexture method updates the sprite with the new texture -- Sprite index can be optionally updated when setting texture - -**Test Results**: -- Texture swapping works correctly -- Position and scale are preserved during texture swap -- Type validation prevents assigning non-Texture objects -- Sprite count changes verify texture was actually swapped - -**Result**: Sprite texture swapping is fully functional. Sprites can change their texture at runtime while preserving position and scale. - ---- - -### Task #52: Grid Skip Out-of-Bounds Entities - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add bounds checking to skip rendering entities outside the visible grid area for performance - -**Implementation**: -1. Added visibility bounds check in UIGrid::render() entity loop -2. Calculate visible bounds based on left_edge, top_edge, width_sq, height_sq -3. Skip entities outside bounds with 1 cell margin for partially visible entities -4. Bounds check considers zoom and pan settings - -**Code Changes**: -```cpp -// Check if entity is within visible bounds (with 1 cell margin) -if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 || - e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) { - continue; // Skip this entity -} -``` - -**Test Results**: -- Entities outside view bounds are successfully skipped -- Performance improvement when rendering grids with many entities -- Zoom and pan correctly affect culling bounds -- 1 cell margin ensures partially visible entities still render - -**Result**: Grid rendering now skips out-of-bounds entities, improving performance for large grids with many entities. This is especially beneficial for games with large maps. - ---- - -## Phase 3: Entity Lifecycle Management - -### Task #30: Entity.die() Method - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement Entity.die() method to remove entity from its grid - -**Implementation**: -1. Added die() method to UIEntity class -2. Method finds and removes entity from grid's entity list -3. Clears entity's grid reference after removal -4. Safe to call multiple times (no-op if not on grid) - -**Code Details**: -- UIEntityCollection::append already sets entity->grid when added -- UIEntityCollection::remove already clears grid reference when removed -- die() method uses std::find_if to locate entity in grid's list -- Uses shared_ptr comparison to find correct entity - -**Test Results**: -- Basic die() functionality works correctly -- Safe to call on entities not in a grid -- Works correctly with multiple entities -- Can be called multiple times safely -- Works in loops over entity collections -- Python references remain valid after die() - -**Result**: Entities can now remove themselves from their grid with a simple die() call. This enables cleaner entity lifecycle management in games. - ---- - -### Standardized Position Arguments - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Standardize position argument handling across all UI classes for consistency - -**Problem**: -- Caption expected pos first, not x, y -- Grid didn't use keywords -- Grid.at() didn't accept tuple format -- Inconsistent position argument formats across classes - -**Implementation**: -1. Created PyPositionHelper.h with standardized position parsing utilities -2. Updated Grid.at() to accept: (x, y), ((x,y)), x=x, y=y, pos=(x,y) -3. Updated Caption to accept: (x, y), ((x,y)), x=x, y=y, pos=(x,y) -4. Ensured Grid supports keyword arguments -5. Maintained backward compatibility for all formats - -**Standardized Formats**: -All position arguments now support: -- `(x, y)` - two positional arguments -- `((x, y))` - single tuple argument -- `x=x, y=y` - keyword arguments -- `pos=(x,y)` - pos keyword with tuple -- `pos=Vector` - pos keyword with Vector object - -**Classes Updated**: -- Grid.at() - Now accepts all standard position formats -- Caption - Now accepts x,y in addition to pos -- Grid - Keywords fully supported -- Frame - Already supported both formats -- Sprite - Already supported both formats -- Entity - Uses pos keyword - -**Test Results**: -- All position formats work correctly -- Backward compatibility maintained -- Consistent error messages across classes - -**Result**: All UI classes now have consistent, flexible position argument handling. This improves API usability and reduces confusion when working with different UI elements. - -**Update**: Extended standardization to Frame, Sprite, and Entity: -- Frame already had dual format support, improved with pos keyword override -- Sprite already had dual format support, improved with pos keyword override -- Entity now supports x, y arguments in addition to pos (was previously pos-only) -- No blockers found - all classes benefit from standardization -- PyPositionHelper could be used for even cleaner implementation in future - ---- - -### Bug Fix: Click Handler Segfault - -**Status**: Completed -**Date**: 2025-07-06 - -**Issue**: Accessing the `click` property on UI elements that don't have a click handler set caused a segfault. - -**Root Cause**: In `UIDrawable::get_click()`, the code was calling `->borrow()` on the `click_callable` unique_ptr without checking if it was null first. - -**Fix**: Added null checks before accessing `click_callable->borrow()` for all UI element types. - -**Result**: Click handler property access is now safe. Elements without click handlers return None as expected. - ---- - -## Phase 3: Enhanced Core Types - -### Task #93: Vector Arithmetic - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Implement arithmetic operations for the Vector class - -**Implementation**: -1. Added PyNumberMethods structure with arithmetic operators: - - Addition (`__add__`): v1 + v2 - - Subtraction (`__sub__`): v1 - v2 - - Multiplication (`__mul__`): v * scalar or scalar * v - - Division (`__truediv__`): v / scalar - - Negation (`__neg__`): -v - - Absolute value (`__abs__`): abs(v) returns magnitude - - Boolean check (`__bool__`): False for zero vector - - Rich comparison (`__eq__`, `__ne__`) - -2. Added vector-specific methods: - - `magnitude()`: Returns length of vector - - `magnitude_squared()`: Returns length squared (faster for comparisons) - - `normalize()`: Returns unit vector in same direction - - `dot(other)`: Dot product with another vector - - `distance_to(other)`: Euclidean distance to another vector - - `angle()`: Angle in radians from positive X axis - - `copy()`: Create an independent copy - -**Technical Details**: -- PyNumberMethods structure defined in mcrfpydef namespace -- Type checking returns NotImplemented for invalid operations -- Zero division protection in divide operation -- Zero vector normalization returns zero vector - -**Test Results**: -All arithmetic operations work correctly: -- Basic arithmetic (add, subtract, multiply, divide, negate) -- Comparison operations (equality, inequality) -- Vector methods (magnitude, normalize, dot product, etc.) -- Type safety with proper error handling - -**Result**: Vector class now supports full arithmetic operations, making game math much more convenient and Pythonic. - ---- - -### Bug Fix: UTF-8 Encoding for Python Output - -**Status**: Completed -**Date**: 2025-07-06 - -**Issue**: Python print statements with unicode characters (like ✓ or emoji) were causing UnicodeEncodeError because stdout/stderr were using ASCII encoding. - -**Root Cause**: Python's stdout and stderr were defaulting to ASCII encoding instead of UTF-8, even though `utf8_mode = 1` was set in PyPreConfig. - -**Fix**: Properly configure UTF-8 encoding in PyConfig during initialization: -```cpp -PyConfig_SetString(&config, &config.stdio_encoding, L"UTF-8"); -PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape"); -config.configure_c_stdio = 1; -``` - -**Implementation**: -- Added UTF-8 configuration in `init_python()` for normal game mode -- Added UTF-8 configuration in `init_python_with_config()` for interpreter mode -- Used `surrogateescape` error handler for robustness with invalid UTF-8 -- Removed temporary stream wrapper hack in favor of proper configuration - -**Technical Details**: -- `stdio_encoding`: Sets encoding for stdin, stdout, stderr -- `stdio_errors`: "surrogateescape" allows round-tripping invalid byte sequences -- `configure_c_stdio`: Lets Python properly configure C runtime stdio behavior - -**Result**: Unicode characters now work correctly in all Python output, including print statements, f-strings, and error messages. Tests can now use checkmarks (✓), cross marks (✗), emojis (🎮), and any other Unicode characters. The solution is cleaner and more robust than wrapping streams after initialization. - ---- - -### Task #94: Color Helper Methods - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add helper methods to the Color class for hex conversion and interpolation - -**Implementation**: -1. **from_hex(hex_string)** - Class method to create Color from hex string - - Accepts formats: "#RRGGBB", "RRGGBB", "#RRGGBBAA", "RRGGBBAA" - - Automatically strips "#" prefix if present - - Validates hex string length and format - - Returns new Color instance - -2. **to_hex()** - Instance method to convert Color to hex string - - Returns "#RRGGBB" for fully opaque colors - - Returns "#RRGGBBAA" for colors with alpha < 255 - - Always includes "#" prefix - -3. **lerp(other_color, t)** - Linear interpolation between colors - - Interpolates all components (r, g, b, a) - - Clamps t to [0.0, 1.0] range - - t=0 returns self, t=1 returns other_color - - Returns new Color instance - -**Technical Details**: -- Used `std::stoul` for hex parsing with base 16 -- `snprintf` for efficient hex string formatting -- Linear interpolation: `result = start + (end - start) * t` -- Added as methods to PyColorType with METH_CLASS flag for from_hex - -**Test Results**: -- All hex formats parse correctly -- Round-trip conversion preserves values -- Interpolation produces smooth gradients -- Error handling works for invalid input - -**Result**: Color class now has convenient helper methods for common color operations. This makes it easier to work with colors in games, especially for UI theming and effects. - -### Task: #103 - Timer objects - -**Issue**: Add mcrfpy.Timer object to encapsulate timer functionality with pause/resume/cancel capabilities - -**Research**: -- Current timer system uses setTimer/delTimer with string names -- Timers stored in GameEngine::timers map as shared_ptr -- No pause/resume functionality exists -- Need object-oriented interface for better control - -**Implementation**: -1. Created PyTimer.h/cpp with PyTimerObject structure -2. Enhanced PyTimerCallable with pause/resume state tracking: - - Added paused, pause_start_time, total_paused_time members - - Modified hasElapsed() to check paused state - - Adjusted timing calculations to account for paused duration -3. Timer object features: - - Constructor: Timer(name, callback, interval) - - Methods: pause(), resume(), cancel(), restart() - - Properties: interval, remaining, paused, active, callback - - Automatically registers with game engine on creation -4. Pause/resume logic: - - When paused: Store pause time, set paused flag - - When resumed: Calculate pause duration, adjust last_ran time - - Prevents timer from "catching up" after resume - -**Key Decisions**: -- Timer object owns a shared_ptr to PyTimerCallable for lifetime management -- Made GameEngine::runtime and timers public for Timer access -- Used placement new for std::string member in PyTimerObject -- Fixed const-correctness issue with isNone() method - -**Test Results**: -- Timer creation and basic firing works correctly -- Pause/resume maintains proper timing without rapid catch-up -- Cancel removes timer from system properly -- Restart resets timer to current time -- Interval modification takes effect immediately -- Timer states (active, paused) report correctly - -**Result**: Timer objects provide a cleaner, more intuitive API for managing timed callbacks. Games can now pause/resume timers for menus, animations, or gameplay mechanics. The object-oriented interface is more Pythonic than the string-based setTimer/delTimer approach. - ---- - -### Test Suite Stabilization - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Make all test files terminate properly and fix various test failures - -**Issues Addressed**: - -1. **Audio Cleanup Warning** - - Issue: `AL lib: (EE) alc_cleanup: 1 device not closed` warning on exit - - Attempted Fix: Converted static audio objects (sf::Music, sf::Sound) to pointers and added explicit cleanup in api_shutdown() - - Result: Warning persists but is a known OpenAL/SFML issue that doesn't affect functionality - - This is a benign warning seen in many SFML applications - -2. **Test Termination Issues** - - Issue: test_click_init.py and test_frame_children.py didn't terminate on their own - - Fix: Added `mcrfpy.delTimer("test")` at start of test functions to prevent re-running - - Added fallback exit timers with 1-2 second timeouts as safety net - - Result: All tests now terminate properly - -3. **Missing Python Methods/Properties** - - Issue: visible, opacity, get_bounds, move, resize methods were missing from UI objects - - Implementation: - - Created UIDrawable_methods.h with template functions for shared functionality - - Added UIDRAWABLE_METHODS and UIDRAWABLE_GETSETTERS macros - - Updated all UI classes (Frame, Caption, Sprite, Grid) to include these - - Special handling for UIEntity which wraps UISprite - created template specializations - - Technical Details: - - Template functions allow code reuse across different PyObject types - - UIEntity delegates to its sprite member for drawable properties - - Fixed static/extern linkage issues with method arrays - - Result: All UI objects now have complete drawable interface - -4. **test_sprite_texture_swap.py Fixes** - - TypeError Issue: Click handler was missing 4th parameter 'action' - - Fix: Updated click handler signature from (x, y, button) to (x, y, button, action) - - Texture Comparison Issue: Direct object comparison failed because sprite.texture returns new wrapper - - Fix: Changed tests to avoid direct texture object comparison, use state tracking instead - - Result: Test passes with all functionality verified - -5. **Timer Test Segfaults** - - Issue: test_timer_object.py and test_timer_object_fixed.py mentioned potential segfaults - - Investigation: Tests were actually running fine, no segfaults detected - - Both timer tests complete successfully with proper exit codes - -6. **test_drawable_base.py Segfault** - - Issue: Segmentation fault when rendering Caption objects in headless mode - - Root Cause: Graphics driver crash in iris_dri.so when rendering text without display - - Stack trace showed crash in sf::Text::draw -> Font::getGlyph -> Texture::update - - Fix: Skip visual test portion in headless mode to avoid rendering - - Result: Test completes successfully, all non-visual tests pass - -**Additional Issues Resolved**: - -1. **Caption Constructor Format** - - Issue: test_drawable_base.py was using incorrect Caption constructor format - - Fix: Changed from keyword arguments to positional format: `Caption((x, y), text)` - - Caption doesn't support x=, y= keywords yet, only positional or pos= formats - -2. **Debug Print Cleanup** - - Removed debug print statement in UICaption color setter that was outputting "got 255, 255, 255, 255" - - This was cluttering test output - -**Test Suite Status**: -- ✓ test_click_init.py - Terminates properly -- ✓ test_frame_children.py - Terminates properly -- ✓ test_sprite_texture_swap.py - All tests pass, terminates properly -- ✓ test_timer_object.py - All tests pass, terminates properly -- ✓ test_timer_object_fixed.py - All tests pass, terminates properly -- ✓ test_drawable_base.py - All tests pass (visual test skipped in headless) - -**Result**: All test files are now "airtight" - they complete successfully, terminate on their own, and handle edge cases properly. The only remaining output is the benign OpenAL cleanup warning. - ---- - -### Window Close Segfault Fix - -**Status**: Completed -**Date**: 2025-07-06 - -**Issue**: Segmentation fault when closing the window via the OS X button (but not when exiting via Ctrl+C) - -**Root Cause**: -When the window was closed externally via the X button, the cleanup order was incorrect: -1. SFML window would be destroyed by the window manager -2. GameEngine destructor would delete scenes containing Python objects -3. Python was still running and might try to access destroyed C++ objects -4. This caused a segfault due to accessing freed memory - -**Solution**: -1. Added `cleanup()` method to GameEngine class that properly clears Python references before C++ destruction -2. The cleanup method: - - Clears all timers (which hold Python callables) - - Clears McRFPy_API's reference to the game engine - - Explicitly closes the window if still open -3. Call `cleanup()` at the end of the run loop when window close is detected -4. Also call in destructor with guard to prevent double cleanup -5. Added `cleaned_up` member variable to track cleanup state - -**Implementation Details**: -- Modified `GameEngine::run()` to call `cleanup()` before exiting -- Modified `GameEngine::~GameEngine()` to call `cleanup()` before deleting scenes -- Added `GameEngine::cleanup()` method with proper cleanup sequence -- Added `bool cleaned_up` member to prevent double cleanup - -**Result**: Window can now be closed via the X button without segfaulting. Python references are properly cleared before C++ objects are destroyed. - ---- - -### Additional Improvements - -**Status**: Completed -**Date**: 2025-07-06 - -1. **Caption Keyword Arguments** - - Issue: Caption didn't accept `x, y` as keyword arguments (e.g., `Caption("text", x=5, y=10)`) - - Solution: Rewrote Caption init to handle multiple argument patterns: - - `Caption("text", x=10, y=20)` - text first with keyword position - - `Caption(x, y, "text")` - traditional positional arguments - - `Caption((x, y), "text")` - position tuple format - - All patterns now work correctly with full keyword support - -2. **Code Organization Refactoring** - - Issue: `UIDrawable_methods.h` was a separate file that could have been better integrated - - Solution: - - Moved template functions and macros from `UIDrawable_methods.h` into `UIBase.h` - - Created `UIEntityPyMethods.h` for UIEntity-specific implementations - - Removed the now-unnecessary `UIDrawable_methods.h` - - Result: Better code organization with Python binding code in appropriate headers ---- - -## Phase 6: Rendering Revolution - -### Task: Grid Background Colors (#50) - -**Status**: Completed -**Date**: 2025-07-06 - -**Goal**: Add background_color property to UIGrid for customizable grid backgrounds - -**Implementation**: -1. Added `sf::Color background_color` member to UIGrid class -2. Initialized with default dark gray (8, 8, 8, 255) in constructors -3. Replaced hardcoded clear color with `renderTexture.clear(background_color)` -4. Added Python property getter/setter: - - `grid.background_color` returns Color object - - Can set with any Color object -5. Added animation support via property system: - - `background_color.r/g/b/a` can be animated individually - - Proper clamping to 0-255 range - -**Technical Details**: -- Background renders before grid tiles and entities -- Animation support through existing property system -- Type-safe Color object validation -- No performance impact (just changes clear color) - -**Test Results**: -- Default background color (8, 8, 8) works correctly -- Setting background_color property changes render -- Individual color components can be modified -- Color cycling demonstration successful - -**Result**: Grid backgrounds are now customizable, allowing for themed dungeons, environmental effects, and visual polish. This was a perfect warm-up task for Phase 6. - ---- diff --git a/PHASE_1_2_3_COMPLETION_SUMMARY.md b/PHASE_1_2_3_COMPLETION_SUMMARY.md deleted file mode 100644 index c77b60c..0000000 --- a/PHASE_1_2_3_COMPLETION_SUMMARY.md +++ /dev/null @@ -1,93 +0,0 @@ -# Phase 1-3 Completion Summary - -## Overview -Successfully completed all tasks in Phases 1, 2, and 3 of the alpha_streamline_2 branch. This represents a major architectural improvement to McRogueFace's Python API, making it more consistent, safer, and feature-rich. - -## Phase 1: Architecture Stabilization (Completed) -- ✅ #7 - Audited and fixed unsafe constructors across all UI classes -- ✅ #71 - Implemented _Drawable base class properties at C++ level -- ✅ #87 - Added visible property for show/hide functionality -- ✅ #88 - Added opacity property for transparency control -- ✅ #89 - Added get_bounds() method returning (x, y, width, height) -- ✅ #98 - Added move()/resize() methods for dynamic UI manipulation - -## Phase 2: API Enhancements (Completed) -- ✅ #101 - Standardized default positions (all UI elements default to 0,0) -- ✅ #38 - Frame accepts children parameter in constructor -- ✅ #42 - All UI elements accept click handler in __init__ -- ✅ #90 - Grid accepts size as tuple: Grid((20, 15)) -- ✅ #19 - Sprite texture swapping via texture property -- ✅ #52 - Grid rendering skips out-of-bounds entities - -## Phase 3: Game-Ready Features (Completed) -- ✅ #30 - Entity.die() method for proper cleanup -- ✅ #93 - Vector arithmetic operators (+, -, *, /, ==, bool, abs, neg) -- ✅ #94 - Color helper methods (from_hex, to_hex, lerp) -- ✅ #103 - Timer objects with pause/resume/cancel functionality - -## Additional Improvements -- ✅ Standardized position arguments across all UI classes - - Created PyPositionHelper for consistent argument parsing - - All classes now accept: (x, y), pos=(x,y), x=x, y=y formats -- ✅ Fixed UTF-8 encoding configuration for Python output - - Configured PyConfig.stdio_encoding during initialization - - Resolved unicode character printing issues - -## Technical Achievements - -### Architecture -- Safe two-phase initialization for all Python objects -- Consistent constructor patterns across UI hierarchy -- Proper shared_ptr lifetime management -- Clean separation between C++ implementation and Python API - -### API Consistency -- All UI elements follow same initialization patterns -- Position arguments work uniformly across all classes -- Properties accessible via standard Python attribute access -- Methods follow Python naming conventions - -### Developer Experience -- Intuitive object construction with sensible defaults -- Flexible argument formats reduce boilerplate -- Clear error messages for invalid inputs -- Comprehensive test coverage for all features - -## Impact on Game Development - -### Before -```python -# Inconsistent, error-prone API -frame = mcrfpy.Frame() -frame.x = 100 # Had to set position after creation -frame.y = 50 -caption = mcrfpy.Caption(mcrfpy.default_font, "Hello", 20, 20) # Different argument order -grid = mcrfpy.Grid(10, 10, 32, 32, 0, 0) # Confusing parameter order -``` - -### After -```python -# Clean, consistent API -frame = mcrfpy.Frame(x=100, y=50, children=[ - mcrfpy.Caption("Hello", pos=(20, 20)), - mcrfpy.Sprite("icon.png", (10, 10)) -]) -grid = mcrfpy.Grid(size=(10, 10), pos=(0, 0)) - -# Advanced features -timer = mcrfpy.Timer("animation", update_frame, 16) -timer.pause() # Pause during menu -timer.resume() # Resume when gameplay continues - -player.move(velocity * delta_time) # Vector math works naturally -ui_theme = mcrfpy.Color.from_hex("#2D3436") -``` - -## Next Steps -With Phases 1-3 complete, the codebase is ready for: -- Phase 4: Event System & Animations (advanced interactivity) -- Phase 5: Scene Management (transitions, lifecycle) -- Phase 6: Audio System (procedural generation, effects) -- Phase 7: Optimization (sprite batching, profiling) - -The foundation is now solid for building sophisticated roguelike games with McRogueFace. \ No newline at end of file diff --git a/RENDERTEXTURE_DESIGN.md b/RENDERTEXTURE_DESIGN.md deleted file mode 100644 index fe03e90..0000000 --- a/RENDERTEXTURE_DESIGN.md +++ /dev/null @@ -1,167 +0,0 @@ -# RenderTexture Overhaul Design Document - -## Overview - -This document outlines the design for implementing RenderTexture support across all UIDrawable classes in McRogueFace. This is Issue #6 and represents a major architectural change to the rendering system. - -## Goals - -1. **Automatic Clipping**: Children rendered outside parent bounds should be clipped -2. **Off-screen Rendering**: Enable post-processing effects and complex compositing -3. **Performance**: Cache static content, only re-render when changed -4. **Backward Compatibility**: Existing code should continue to work - -## Current State - -### Classes Already Using RenderTexture: -- **UIGrid**: Uses a 1920x1080 RenderTexture for compositing grid view -- **SceneTransition**: Uses two 1024x768 RenderTextures for transitions -- **HeadlessRenderer**: Uses RenderTexture for headless mode - -### Classes Using Direct Rendering: -- **UIFrame**: Renders box and children directly -- **UICaption**: Renders text directly -- **UISprite**: Renders sprite directly - -## Design Decisions - -### 1. Opt-in Architecture - -Not all UIDrawables need RenderTextures. We'll use an opt-in approach: - -```cpp -class UIDrawable { -protected: - // RenderTexture support (opt-in) - std::unique_ptr render_texture; - sf::Sprite render_sprite; - bool use_render_texture = false; - bool render_dirty = true; - - // Enable RenderTexture for this drawable - void enableRenderTexture(unsigned int width, unsigned int height); - void updateRenderTexture(); -}; -``` - -### 2. When to Use RenderTexture - -RenderTextures will be enabled for: -1. **UIFrame with clipping enabled** (new property: `clip_children = true`) -2. **UIDrawables with effects** (future: shaders, blend modes) -3. **Complex composites** (many children that rarely change) - -### 3. Render Flow - -``` -Standard Flow: -render() → render directly to target - -RenderTexture Flow: -render() → if dirty → clear RT → render to RT → dirty = false - → draw RT sprite to target -``` - -### 4. Dirty Flag Management - -Mark as dirty when: -- Properties change (position, size, color, etc.) -- Children added/removed -- Child marked as dirty (propagate up) -- Animation frame - -### 5. Size Management - -RenderTexture size options: -1. **Fixed Size**: Set at creation (current UIGrid approach) -2. **Dynamic Size**: Match bounds, recreate on resize -3. **Pooled Sizes**: Use standard sizes from pool - -We'll use **Dynamic Size** with lazy creation. - -## Implementation Plan - -### Phase 1: Base Infrastructure (This PR) -1. Add RenderTexture members to UIDrawable -2. Add `enableRenderTexture()` method -3. Implement dirty flag system -4. Add `clip_children` property to UIFrame - -### Phase 2: UIFrame Implementation -1. Update UIFrame::render() to use RenderTexture when clipping -2. Test with nested frames -3. Verify clipping works correctly - -### Phase 3: Performance Optimization -1. Implement texture pooling -2. Add dirty flag propagation -3. Profile and optimize - -### Phase 4: Extended Features -1. Blur/glow effects using RenderTexture -2. Viewport-based rendering (#8) -3. Screenshot improvements - -## API Changes - -### Python API: -```python -# Enable clipping on frames -frame.clip_children = True # New property - -# Future: effects -frame.blur_amount = 5.0 -sprite.glow_color = Color(255, 200, 100) -``` - -### C++ API: -```cpp -// Enable RenderTexture -frame->enableRenderTexture(width, height); -frame->setClipChildren(true); - -// Mark dirty -frame->markDirty(); -``` - -## Performance Considerations - -1. **Memory**: Each RenderTexture uses GPU memory (width * height * 4 bytes) -2. **Creation Cost**: Creating RenderTextures is expensive, use pooling -3. **Clear Cost**: Clearing large RenderTextures each frame is costly -4. **Bandwidth**: Drawing to RenderTexture then to screen doubles bandwidth - -## Migration Strategy - -1. All existing code continues to work (direct rendering by default) -2. Gradually enable RenderTexture for specific use cases -3. Profile before/after to ensure performance gains -4. Document best practices - -## Risks and Mitigation - -| Risk | Mitigation | -|------|------------| -| Performance regression | Opt-in design, profile extensively | -| Memory usage increase | Texture pooling, size limits | -| Complexity increase | Clear documentation, examples | -| Integration issues | Extensive testing with SceneTransition | - -## Success Criteria - -1. ✓ Frames can clip children to bounds -2. ✓ No performance regression for direct rendering -3. ✓ Scene transitions continue to work -4. ✓ Memory usage is reasonable -5. ✓ API is intuitive and documented - -## Future Extensions - -1. **Shader Support** (#106): RenderTextures enable post-processing shaders -2. **Particle Systems** (#107): Render particles to texture for effects -3. **Caching**: Static UI elements cached in RenderTextures -4. **Resolution Independence**: RenderTextures for DPI scaling - -## Conclusion - -This design provides a foundation for professional rendering capabilities while maintaining backward compatibility and performance. The opt-in approach allows gradual adoption and testing. \ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 453c125..0000000 --- a/ROADMAP.md +++ /dev/null @@ -1,524 +0,0 @@ -# McRogueFace - Development Roadmap - -## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉 - -**Current State**: Alpha release achieved! All critical blockers resolved! -**Latest Update**: Moved RenderTexture (#6) to Beta - Alpha is READY! (2025-07-05) -**Branch**: interpreter_mode (ready for alpha release merge) -**Open Issues**: ~46 remaining (non-blocking quality-of-life improvements) - ---- - -## Recent Achievements - -### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾 -**All Alpha Blockers Resolved!** -- Z-order rendering with performance optimization (Issue #63) -- Python Sequence Protocol for collections (Issue #69) -- Comprehensive Animation System (Issue #59) -- Moved RenderTexture to Beta (not needed for Alpha) -- **McRogueFace is ready for Alpha release!** - -### 2025-07-05: Z-order Rendering Complete! 🎉 -**Issue #63 Resolved**: Consistent z-order rendering with performance optimization -- Dirty flag pattern prevents unnecessary per-frame sorting -- Lazy sorting for both Scene elements and Frame children -- Frame children now respect z_index (fixed inconsistency) -- Automatic dirty marking on z_index changes and collection modifications -- Performance: O(1) check for static scenes vs O(n log n) every frame - -### 2025-07-05: Python Sequence Protocol Complete! 🎉 -**Issue #69 Resolved**: Full sequence protocol implementation for collections -- Complete __setitem__, __delitem__, __contains__ support -- Slice operations with extended slice support (step != 1) -- Concatenation (+) and in-place concatenation (+=) with validation -- Negative indexing throughout, index() and count() methods -- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only) -- Default value support: None for texture/font parameters uses engine defaults - -### 2025-07-05: Animation System Complete! 🎉 -**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions -- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity) -- Individual color component animation (r/g/b/a) -- Sprite sequence animation and text typewriter effects -- Pure C++ execution without Python callbacks -- Delta animation support for relative values - -### 2025-01-03: Major Stability Update -**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines) -**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters -**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33) -**Test Coverage**: Comprehensive test suite with timer callback pattern established - ---- - -## 🔧 CURRENT WORK: Alpha Streamline 2 - Major Architecture Improvements - -### Recent Completions: -- ✅ **Phase 1-4 Complete** - Foundation, API Polish, Entity Lifecycle, Visibility/Performance -- ✅ **Phase 5 Complete** - Window/Scene Architecture fully implemented! - - Window singleton with properties (#34) - - OOP Scene support with lifecycle methods (#61) - - Window resize events (#1) - - Scene transitions with animations (#105) -- 🚧 **Phase 6 Started** - Rendering Revolution in progress! - - Grid background colors (#50) ✅ - - RenderTexture base infrastructure ✅ - - UIFrame clipping support ✅ - -### Active Development: -- **Branch**: alpha_streamline_2 -- **Current Phase**: Phase 6 - Rendering Revolution (IN PROGRESS) -- **Timeline**: 3-4 weeks for Phase 6 implementation -- **Strategic Vision**: See STRATEGIC_VISION.md for platform roadmap -- **Latest**: RenderTexture base infrastructure complete, UIFrame clipping working! - -### 🏗️ Architectural Dependencies Map - -``` -Foundation Layer: -├── #71 Base Class (_Drawable) -│ ├── #10 Visibility System (needs AABB from base) -│ ├── #87 visible property -│ └── #88 opacity property -│ -├── #7 Safe Constructors (affects all classes) -│ └── Blocks any new class creation until resolved -│ -└── #30 Entity/Grid Integration (lifecycle management) - └── Enables reliable entity management - -Window/Scene Layer: -├── #34 Window Object -│ ├── #61 Scene Object (depends on Window) -│ ├── #14 SFML Exposure (helps implement Window) -│ └── Future: Multi-window support - -Rendering Layer: -└── #6 RenderTexture Overhaul - ├── Enables clipping - ├── Off-screen rendering - └── Post-processing effects -``` - -## 🚀 Alpha Streamline 2 - Comprehensive Phase Plan - -### Phase 1: Foundation Stabilization (1-2 weeks) -**Goal**: Safe, predictable base for all future work -``` -1. #7 - Audit and fix unsafe constructors (CRITICAL - do first!) - - Find all manually implemented no-arg constructors - - Verify map compatibility requirements - - Make pointer-safe or remove - -2. #71 - _Drawable base class implementation - - Common properties: x, y, w, h, visible, opacity - - Virtual methods: get_bounds(), render() - - Proper Python inheritance setup - -3. #87 - visible property - - Add to base class - - Update all render methods to check - -4. #88 - opacity property (depends on #87) - - 0.0-1.0 float range - - Apply in render methods - -5. #89 - get_bounds() method - - Virtual method returning (x, y, w, h) - - Override in each UI class - -6. #98 - move()/resize() convenience methods - - move(dx, dy) - relative movement - - resize(w, h) - absolute sizing -``` -*Rationale*: Can't build on unsafe foundations. Base class enables all UI improvements. - -### Phase 2: Constructor & API Polish (1 week) -**Goal**: Pythonic, intuitive API -``` -1. #101 - Standardize (0,0) defaults for all positions -2. #38 - Frame children parameter: Frame(children=[...]) -3. #42 - Click handler in __init__: Button(click=callback) -4. #90 - Grid size tuple: Grid(grid_size=(10, 10)) -5. #19 - Sprite texture swapping: sprite.texture = new_texture -6. #52 - Grid skip out-of-bounds entities (performance) -``` -*Rationale*: Quick wins that make the API more pleasant before bigger changes. - -### Phase 3: Entity Lifecycle Management (1 week) -**Goal**: Bulletproof entity/grid relationships -``` -1. #30 - Entity.die() and grid association - - Grid.entities.append(e) sets e.grid = self - - Grid.entities.remove(e) sets e.grid = None - - Entity.die() calls self.grid.remove(self) - - Entity can only be in 0 or 1 grid - -2. #93 - Vector arithmetic methods - - add, subtract, multiply, divide - - distance, normalize, dot product - -3. #94 - Color helper methods - - from_hex("#FF0000"), to_hex() - - lerp(other_color, t) for interpolation - -4. #103 - Timer objects - timer = mcrfpy.Timer("my_timer", callback, 1000) - timer.pause() - timer.resume() - timer.cancel() -``` -*Rationale*: Games need reliable entity management. Timer objects enable entity AI. - -### Phase 4: Visibility & Performance (1-2 weeks) -**Goal**: Only render/process what's needed -``` -1. #10 - [UNSCHEDULED] Full visibility system with AABB - - Postponed: UIDrawables can exist in multiple collections - - Cannot reliably determine screen position due to multiple render contexts - - Needs architectural solution for parent-child relationships - -2. #52 - Grid culling (COMPLETED in Phase 2) - -3. #39/40/41 - Name system for finding elements - - name="button1" property on all UIDrawables - - only_one=True for unique names - - scene.find("button1") returns element - - collection.find("enemy*") returns list - -4. #104 - Basic profiling/metrics - - Frame time tracking - - Draw call counting - - Python vs C++ time split -``` -*Rationale*: Performance is feature. Finding elements by name is huge QoL. - -### Phase 5: Window/Scene Architecture ✅ COMPLETE! (2025-07-06) -**Goal**: Modern, flexible architecture -``` -1. ✅ #34 - Window object (singleton first) - window = mcrfpy.Window.get() - window.resolution = (1920, 1080) - window.fullscreen = True - window.vsync = True - -2. ✅ #1 - Window resize events - scene.on_resize(self, width, height) callback implemented - -3. ✅ #61 - Scene object (OOP scenes) - class MenuScene(mcrfpy.Scene): - def on_keypress(self, key, state): - # handle input - def on_enter(self): - # setup UI - def on_exit(self): - # cleanup - def update(self, dt): - # frame update - -4. ✅ #14 - SFML exposure research - - Completed comprehensive analysis - - Recommendation: Direct integration as mcrfpy.sfml - - SFML 3.0 migration deferred to late 2025 - -5. ✅ #105 - Scene transitions - mcrfpy.setScene("menu", "fade", 1.0) - # Supports: fade, slide_left, slide_right, slide_up, slide_down -``` -*Result*: Entire window/scene system modernized with OOP design! - -### Phase 6: Rendering Revolution (3-4 weeks) 🚧 IN PROGRESS! -**Goal**: Professional rendering capabilities -``` -1. ✅ #50 - Grid background colors [COMPLETED] - grid.background_color = mcrfpy.Color(50, 50, 50) - - Added background_color property with animation support - - Default dark gray background (8, 8, 8, 255) - -2. 🚧 #6 - RenderTexture overhaul [PARTIALLY COMPLETE] - ✅ Base infrastructure in UIDrawable - ✅ UIFrame clip_children property - ✅ Dirty flag optimization system - ✅ Nested clipping support - ⏳ Extend to other UI classes - ⏳ Effects (blur, glow, etc.) - -3. #8 - Viewport-based rendering [NEXT PRIORITY] - - RenderTexture matches viewport - - Proper scaling/letterboxing - - Coordinate system transformations - -4. #106 - Shader support [STRETCH GOAL] - sprite.shader = mcrfpy.Shader.load("glow.frag") - frame.shader_params = {"intensity": 0.5} - -5. #107 - Particle system [STRETCH GOAL] - emitter = mcrfpy.ParticleEmitter() - emitter.texture = spark_texture - emitter.emission_rate = 100 - emitter.lifetime = (0.5, 2.0) -``` - -**Phase 6 Technical Notes**: -- RenderTexture is the foundation - everything else depends on it -- Grid backgrounds (#50) ✅ completed as warm-up task -- RenderTexture implementation uses opt-in architecture to preserve backward compatibility -- Dirty flag system crucial for performance - only re-render when properties change -- Nested clipping works correctly with proper coordinate transformations -- Scene transitions already use RenderTextures - good integration test -- Next: Viewport rendering (#8) will build on RenderTexture foundation -- Shader/Particle systems might be deferred to Phase 7 or Gamma - -*Rationale*: This unlocks professional visual effects but is complex. - -### Phase 7: Documentation & Distribution (1-2 weeks) -**Goal**: Ready for the world -``` -1. #85 - Replace all "docstring" placeholders -2. #86 - Add parameter documentation -3. #108 - Generate .pyi type stubs for IDE support -4. #70 - PyPI wheel preparation -5. API reference generator tool -``` - -## 📋 Critical Path & Parallel Tracks - -### 🔴 **Critical Path** (Must do in order) -**Safe Constructors (#7)** → **Base Class (#71)** → **Visibility (#10)** → **Window (#34)** → **Scene (#61)** - -### 🟡 **Parallel Tracks** (Can be done alongside critical path) - -**Track A: Entity Systems** -- Entity/Grid integration (#30) -- Timer objects (#103) -- Vector/Color helpers (#93, #94) - -**Track B: API Polish** -- Constructor improvements (#101, #38, #42, #90) -- Sprite texture swap (#19) -- Name/search system (#39/40/41) - -**Track C: Performance** -- Grid culling (#52) -- Visibility culling (part of #10) -- Profiling tools (#104) - -### 💎 **Quick Wins to Sprinkle Throughout** -1. Color helpers (#94) - 1 hour -2. Vector methods (#93) - 1 hour -3. Grid backgrounds (#50) - 30 minutes -4. Default positions (#101) - 30 minutes - -### 🎯 **Recommended Execution Order** - -**Week 1-2**: Foundation (Critical constructors + base class) -**Week 3**: Entity lifecycle + API polish -**Week 4**: Visibility system + performance -**Week 5-6**: Window/Scene architecture -**Week 7-9**: Rendering revolution (or defer to gamma) -**Week 10**: Documentation + release prep - -### 🆕 **New Issues to Create/Track** - -1. [x] **Timer Objects** - Pythonic timer management (#103) - *Completed Phase 3* -2. [ ] **Event System Enhancement** - Mouse enter/leave, drag, right-click -3. [ ] **Resource Manager** - Centralized asset loading -4. [ ] **Serialization System** - Save/load game state -5. [x] **Scene Transitions** - Fade, slide, custom effects (#105) - *Completed Phase 5* -6. [x] **Profiling Tools** - Performance metrics (#104) - *Completed Phase 4* -7. [ ] **Particle System** - Visual effects framework (#107) -8. [ ] **Shader Support** - Custom rendering effects (#106) - ---- - -## 📋 Phase 6 Implementation Strategy - -### RenderTexture Overhaul (#6) - Technical Approach - -**Current State**: -- UIGrid already uses RenderTexture for entity rendering -- Scene transitions use RenderTextures for smooth animations -- Direct rendering to window for Frame, Caption, Sprite - -**Implementation Plan**: -1. **Base Infrastructure**: - - Add `sf::RenderTexture* target` to UIDrawable base - - Modify `render()` to check if target exists - - If target: render to texture, then draw texture to parent - - If no target: render directly (backward compatible) - -2. **Clipping Support**: - - Frame enforces bounds on children via RenderTexture - - Children outside bounds are automatically clipped - - Nested frames create render texture hierarchy - -3. **Performance Optimization**: - - Lazy RenderTexture creation (only when needed) - - Dirty flag system (only re-render when changed) - - Texture pooling for commonly used sizes - -4. **Integration Points**: - - Scene transitions already working with RenderTextures - - UIGrid can be reference implementation - - Test with deeply nested UI structures - -**Quick Wins Before Core Work**: -1. **Grid Background (#50)** - 30 min implementation - - Add `background_color` and `background_texture` properties - - Render before entities in UIGrid::render() - - Good warm-up before tackling RenderTexture - -2. **Research Tasks**: - - Study UIGrid's current RenderTexture usage - - Profile scene transition performance - - Identify potential texture size limits - ---- - -## 🚀 NEXT PHASE: Beta Features & Polish - -### Alpha Complete! Moving to Beta Priorities: -1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)* -2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)* -3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)* -4. **#6** - RenderTexture concept - *Extensive Overhaul* -5. ~~**#47** - New README.md for Alpha release~~ - *Completed* -- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed* -- [x] **#77** - Fix error message copy/paste bug - *Fixed* -- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed* -- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix* - Issue #37 is **on hold** until we have a Windows build environment available. I actually suspect this is already fixed by the updates to the makefile, anyway. -- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed* -- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed* -- [x] **keypressScene() Validation** - Add proper error handling - *Fixed* - -### 🔄 Complete Iterator System -**Status**: Core iterators complete (#72 closed), Grid point iterators still pending - -- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work -- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed* -- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)* - -**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul - ---- - -## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues) - -### 🎮 Core Engine Systems - -#### Iterator/Collection System (2 issues) -- [x] **#73** - Entity index() method for removal - *Fixed* -- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)* - -#### Python/C++ Integration (7 issues) -- [x] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations* -- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul* -- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul* -- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)* -- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul* -- [~] **#14** - Expose SFML as built-in module - *Research Complete, Implementation Pending* -- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations* - -#### UI/Rendering System (12 issues) -- [x] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations* -- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)* -- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul* -- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul* -- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations* -- [x] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations* -- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix* -- [ ] **#50** - UIGrid background color field - *Isolated Fix* -- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations* -- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix* -- [x] **#33** - Sprite index validation against texture range - *Fixed* - -#### Grid/Entity System (6 issues) -- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul* -- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul* -- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul* -- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations* -- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations* -- [x] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix* - -#### Scene/Window Management (5 issues) -- [x] **#61** - Scene object encapsulating key callbacks - *Completed Phase 5* -- [x] **#34** - Window object for resolution/scaling - *Completed Phase 5* -- [ ] **#62** - Multiple windows support - *Extensive Overhaul* -- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations* -- [x] **#1** - Scene resize event handling - *Completed Phase 5* - -### 🔧 Quality of Life Features - -#### UI Enhancement Features (8 issues) -- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations* -- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations* -- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations* -- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix* -- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix* -- [x] **#27** - UIEntityCollection.extend() method - *Fixed* -- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix* -- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix* - -### 🧹 Refactoring & Cleanup - -#### Code Cleanup (7 issues) -- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed* -- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed* -- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations* -- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix* -- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations* - -### 📚 Demo & Documentation - -#### Documentation (2 issues) -- [x] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix* -- [ ] **#48** - Dependency compilation documentation - *Isolated Fix* - -#### Demo Projects (6 issues) -- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations* -- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations* -- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)* -- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)* -- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul* -- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul* - ---- - -## 🎮 STRATEGIC DIRECTION - -### Engine Philosophy Maintained -- **C++ First**: Performance-critical code stays in C++ -- **Python Close Behind**: Rich scripting without frame-rate impact -- **Game-Ready**: Each improvement should benefit actual game development - -### Architecture Goals -1. **Clean Inheritance**: Drawable → UI components, proper type preservation -2. **Collection Consistency**: Uniform iteration, indexing, and search patterns -3. **Resource Management**: RAII everywhere, proper lifecycle handling -4. **Multi-Platform**: Windows/Linux feature parity maintained - ---- - -## 📚 REFERENCES & CONTEXT - -**Issue Dependencies** (Key Chains): -- Iterator System: Grid points → #73 → #69 (Alpha Blocker) -- UI Hierarchy: #71 → #63 (Alpha Blocker) -- Rendering: #6 (Alpha Blocker) → #8, #9 → #10 -- Entity System: #30 → #16 → #67 -- Window Management: #34 → #49, #61 → #62 - -**Commit References**: -- 167636c: Iterator improvements (UICollection/UIEntityCollection complete) -- Recent work: 7DRL 2025 completion, RPATH updates, console improvements - -**Architecture Files**: -- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp -- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h -- Game implementation: src/scripts/ (Crypt of Sokoban complete game) - ---- - -*Last Updated: 2025-07-05* diff --git a/SFML_3_MIGRATION_RESEARCH.md b/SFML_3_MIGRATION_RESEARCH.md deleted file mode 100644 index 71c35e1..0000000 --- a/SFML_3_MIGRATION_RESEARCH.md +++ /dev/null @@ -1,257 +0,0 @@ -# SFML 3.0 Migration Research for McRogueFace - -## Executive Summary - -SFML 3.0 was released on December 21, 2024, marking the first major version in 12 years. While it offers significant improvements in type safety, modern C++ features, and API consistency, migrating McRogueFace would require substantial effort. Given our plans for `mcrfpy.sfml`, I recommend **deferring migration to SFML 3.0** until after implementing the initial `mcrfpy.sfml` module with SFML 2.6.1. - -## SFML 3.0 Overview - -### Release Highlights -- **Release Date**: December 21, 2024 -- **Development**: 3 years, 1,100+ commits, 41 new contributors -- **Major Feature**: C++17 support (now required) -- **Audio Backend**: Replaced OpenAL with miniaudio -- **Test Coverage**: Expanded to 57% -- **New Features**: Scissor and stencil testing - -### Key Breaking Changes - -#### 1. C++ Standard Requirements -- **Minimum**: C++17 (was C++03) -- **Compilers**: MSVC 16 (VS 2019), GCC 9, Clang 9, AppleClang 12 - -#### 2. Event System Overhaul -```cpp -// SFML 2.x -sf::Event event; -while (window.pollEvent(event)) { - switch (event.type) { - case sf::Event::Closed: - window.close(); - break; - case sf::Event::KeyPressed: - handleKey(event.key.code); - break; - } -} - -// SFML 3.0 -while (const std::optional event = window.pollEvent()) { - if (event->is()) { - window.close(); - } - else if (const auto* keyPressed = event->getIf()) { - handleKey(keyPressed->code); - } -} -``` - -#### 3. Scoped Enumerations -```cpp -// SFML 2.x -sf::Keyboard::A -sf::Mouse::Left - -// SFML 3.0 -sf::Keyboard::Key::A -sf::Mouse::Button::Left -``` - -#### 4. Resource Loading -```cpp -// SFML 2.x -sf::Texture texture; -if (!texture.loadFromFile("image.png")) { - // Handle error -} - -// SFML 3.0 -try { - sf::Texture texture("image.png"); -} catch (const std::exception& e) { - // Handle error -} -``` - -#### 5. Geometry Changes -```cpp -// SFML 2.x -sf::FloatRect rect(left, top, width, height); - -// SFML 3.0 -sf::FloatRect rect({left, top}, {width, height}); -// Now uses position and size vectors -``` - -#### 6. CMake Changes -```cmake -# SFML 2.x -find_package(SFML 2.6 COMPONENTS graphics window system audio REQUIRED) -target_link_libraries(app sfml-graphics sfml-window sfml-system sfml-audio) - -# SFML 3.0 -find_package(SFML 3.0 COMPONENTS Graphics Window System Audio REQUIRED) -target_link_libraries(app SFML::Graphics SFML::Window SFML::System SFML::Audio) -``` - -## McRogueFace SFML Usage Analysis - -### Current Usage Statistics -- **SFML Version**: 2.6.1 -- **Integration Level**: Moderate to Heavy -- **Affected Files**: ~40+ source files - -### Major Areas Requiring Changes - -#### 1. Event Handling (High Impact) -- **Files**: `GameEngine.cpp`, `PyScene.cpp` -- **Changes**: Complete rewrite of event loops -- **Effort**: High - -#### 2. Enumerations (Medium Impact) -- **Files**: `ActionCode.h`, all input handling -- **Changes**: Update all keyboard/mouse enum references -- **Effort**: Medium (mostly find/replace) - -#### 3. Resource Loading (Medium Impact) -- **Files**: `PyTexture.cpp`, `PyFont.cpp`, `McRFPy_API.cpp` -- **Changes**: Constructor-based loading with exception handling -- **Effort**: Medium - -#### 4. Geometry (Low Impact) -- **Files**: Various UI classes -- **Changes**: Update Rect construction -- **Effort**: Low - -#### 5. CMake Build System (Low Impact) -- **Files**: `CMakeLists.txt` -- **Changes**: Update find_package and target names -- **Effort**: Low - -### Code Examples from McRogueFace - -#### Current Event Loop (GameEngine.cpp) -```cpp -sf::Event event; -while (window && window->pollEvent(event)) { - processEvent(event); - if (event.type == sf::Event::Closed) { - running = false; - } -} -``` - -#### Current Key Mapping (ActionCode.h) -```cpp -{sf::Keyboard::Key::A, KEY_A}, -{sf::Keyboard::Key::Left, KEY_LEFT}, -{sf::Mouse::Left, MOUSEBUTTON_LEFT} -``` - -## Impact on mcrfpy.sfml Module Plans - -### Option 1: Implement with SFML 2.6.1 First (Recommended) -**Pros**: -- Faster initial implementation -- Stable, well-tested SFML version -- Can provide value immediately -- Migration can be done later - -**Cons**: -- Will require migration work later -- API might need changes for SFML 3.0 - -### Option 2: Wait and Implement with SFML 3.0 -**Pros**: -- Future-proof implementation -- Modern C++ features -- No migration needed later - -**Cons**: -- Delays `mcrfpy.sfml` implementation -- SFML 3.0 is very new (potential bugs) -- Less documentation/examples available - -### Option 3: Dual Support -**Pros**: -- Maximum flexibility -- Gradual migration path - -**Cons**: -- Significant additional complexity -- Maintenance burden -- Conditional compilation complexity - -## Migration Strategy Recommendation - -### Phase 1: Current State (Now) -1. Continue with SFML 2.6.1 -2. Implement `mcrfpy.sfml` module as planned -3. Design module API to minimize future breaking changes - -### Phase 2: Preparation (3-6 months) -1. Monitor SFML 3.0 stability and adoption -2. Create migration branch for testing -3. Update development environment to C++17 - -### Phase 3: Migration (6-12 months) -1. Migrate McRogueFace core to SFML 3.0 -2. Update `mcrfpy.sfml` to match -3. Provide migration guide for users - -### Phase 4: Deprecation (12-18 months) -1. Deprecate SFML 2.6.1 support -2. Focus on SFML 3.0 features - -## Specific Migration Tasks - -### Prerequisites -- [ ] Update to C++17 compatible compiler -- [ ] Update CMake to 3.16+ -- [ ] Review all SFML usage locations - -### Core Changes -- [ ] Rewrite all event handling loops -- [ ] Update all enum references -- [ ] Convert resource loading to constructors -- [ ] Update geometry construction -- [ ] Update CMake configuration - -### mcrfpy.sfml Considerations -- [ ] Design API to be version-agnostic where possible -- [ ] Use abstraction layer for version-specific code -- [ ] Document version requirements clearly - -## Risk Assessment - -### High Risk Areas -1. **Event System**: Complete paradigm shift -2. **Exception Handling**: New resource loading model -3. **Third-party Dependencies**: May not support SFML 3.0 yet - -### Medium Risk Areas -1. **Performance**: New implementations may differ -2. **Platform Support**: New version may have issues -3. **Documentation**: Less community knowledge - -### Low Risk Areas -1. **Basic Rendering**: Core concepts unchanged -2. **CMake**: Straightforward updates -3. **Enums**: Mechanical changes - -## Conclusion - -While SFML 3.0 offers significant improvements, the migration effort is substantial. Given that: - -1. SFML 3.0 is very new (released December 2024) -2. McRogueFace has heavy SFML integration -3. We plan to implement `mcrfpy.sfml` soon -4. The event system requires complete rewriting - -**I recommend deferring SFML 3.0 migration** until after successfully implementing `mcrfpy.sfml` with SFML 2.6.1. This allows us to: -- Deliver value sooner with `mcrfpy.sfml` -- Learn from early adopters of SFML 3.0 -- Design our module API with migration in mind -- Migrate when SFML 3.0 is more mature - -The migration should be revisited in 6-12 months when SFML 3.0 has proven stability and wider adoption. \ No newline at end of file diff --git a/SFML_EXPOSURE_RESEARCH.md b/SFML_EXPOSURE_RESEARCH.md deleted file mode 100644 index af0826c..0000000 --- a/SFML_EXPOSURE_RESEARCH.md +++ /dev/null @@ -1,200 +0,0 @@ -# SFML Exposure Research (#14) - -## Executive Summary - -After thorough research, I recommend **Option 3: Direct Integration** - implementing our own `mcrfpy.sfml` module with API compatibility to existing python-sfml bindings. This approach gives us full control while maintaining familiarity for developers who have used python-sfml. - -## Current State Analysis - -### McRogueFace SFML Usage - -**Version**: SFML 2.6.1 (confirmed in `modules/SFML/include/SFML/Config.hpp`) - -**Integration Level**: Moderate to Heavy -- SFML types appear in most header files -- Core rendering depends on `sf::RenderTarget` -- Event system uses `sf::Event` directly -- Input mapping uses SFML enums - -**SFML Modules Used**: -- Graphics (sprites, textures, fonts, shapes) -- Window (events, keyboard, mouse) -- System (vectors, time, clocks) -- Audio (sound effects, music) - -**Already Exposed to Python**: -- `mcrfpy.Color` → `sf::Color` -- `mcrfpy.Vector` → `sf::Vector2f` -- `mcrfpy.Font` → `sf::Font` -- `mcrfpy.Texture` → `sf::Texture` - -### Python-SFML Status - -**Official python-sfml (pysfml)**: -- Last version: 2.3.2 (supports SFML 2.3.2) -- Last meaningful update: ~2019 -- Not compatible with SFML 2.6.1 -- Project appears abandoned (domain redirects elsewhere) -- GitHub repo has 43 forks but no active maintained fork - -**Alternatives**: -- No other major Python SFML bindings found -- Most alternatives were archived by 2021 - -## Option Analysis - -### Option 1: Use Existing python-sfml -**Pros**: -- No development work needed -- Established API - -**Cons**: -- Incompatible with SFML 2.6.1 -- Would require downgrading to SFML 2.3.2 -- Abandoned project (security/bug risks) -- Installation issues reported - -**Verdict**: Not viable due to version incompatibility and abandonment - -### Option 2: Fork and Update python-sfml -**Pros**: -- Leverage existing codebase -- Maintain API compatibility - -**Cons**: -- Significant work to update from 2.3.2 to 2.6.1 -- Cython complexity -- Maintenance burden of external codebase -- Still requires users to pip install separately - -**Verdict**: High effort with limited benefit - -### Option 3: Direct Integration (Recommended) -**Pros**: -- Full control over implementation -- Tight integration with McRogueFace -- No external dependencies -- Can expose exactly what we need -- Built-in module (no pip install) -- Can maintain API compatibility with python-sfml - -**Cons**: -- Development effort required -- Need to maintain bindings - -**Verdict**: Best long-term solution - -## Implementation Plan for Direct Integration - -### 1. Module Structure -```python -# Built-in module: mcrfpy.sfml -import mcrfpy.sfml as sf - -# Maintain compatibility with python-sfml API -window = sf.RenderWindow(sf.VideoMode(800, 600), "My Window") -sprite = sf.Sprite() -texture = sf.Texture() -``` - -### 2. Priority Classes to Expose - -**Phase 1 - Core Types** (Already partially done): -- [x] `sf::Vector2f`, `sf::Vector2i` -- [x] `sf::Color` -- [ ] `sf::Rect` (FloatRect, IntRect) -- [ ] `sf::VideoMode` -- [ ] `sf::Time`, `sf::Clock` - -**Phase 2 - Graphics**: -- [x] `sf::Texture` (partial) -- [x] `sf::Font` (partial) -- [ ] `sf::Sprite` (full exposure) -- [ ] `sf::Text` -- [ ] `sf::Shape` hierarchy -- [ ] `sf::View` -- [ ] `sf::RenderWindow` (carefully managed) - -**Phase 3 - Window/Input**: -- [ ] `sf::Event` and event types -- [ ] `sf::Keyboard` enums -- [ ] `sf::Mouse` enums -- [ ] `sf::Joystick` - -**Phase 4 - Audio** (lower priority): -- [ ] `sf::SoundBuffer` -- [ ] `sf::Sound` -- [ ] `sf::Music` - -### 3. Design Principles - -1. **API Compatibility**: Match python-sfml's API where possible -2. **Memory Safety**: Use shared_ptr for resource management -3. **Thread Safety**: Consider GIL implications -4. **Integration**: Allow mixing with existing mcrfpy types -5. **Documentation**: Comprehensive docstrings - -### 4. Technical Considerations - -**Resource Sharing**: -- McRogueFace already manages SFML resources -- Need to share textures/fonts between mcrfpy and sfml modules -- Use the same underlying SFML objects - -**Window Management**: -- McRogueFace owns the main window -- Expose read-only access or controlled modification -- Prevent users from closing/destroying the game window - -**Event Handling**: -- Game engine processes events in main loop -- Need mechanism to expose events to Python safely -- Consider callback system or event queue - -### 5. Implementation Phases - -**Phase 1** (1-2 weeks): -- Create `mcrfpy.sfml` module structure -- Implement basic types (Vector, Color, Rect) -- Add comprehensive tests - -**Phase 2** (2-3 weeks): -- Expose graphics classes -- Implement resource sharing with mcrfpy -- Create example scripts - -**Phase 3** (2-3 weeks): -- Add window/input functionality -- Integrate with game event loop -- Performance optimization - -**Phase 4** (1 week): -- Audio support -- Documentation -- PyPI packaging of mcrfpy.sfml separately - -## Benefits of Direct Integration - -1. **No Version Conflicts**: Always in sync with our SFML version -2. **Better Performance**: Direct C++ bindings without Cython overhead -3. **Selective Exposure**: Only expose what makes sense for game scripting -4. **Integrated Documentation**: Part of McRogueFace docs -5. **Future-Proof**: We control the implementation - -## Migration Path for Users - -Users familiar with python-sfml can easily migrate: -```python -# Old python-sfml code -import sfml as sf - -# New McRogueFace code -import mcrfpy.sfml as sf -# Most code remains the same! -``` - -## Conclusion - -Direct integration as `mcrfpy.sfml` provides the best balance of control, compatibility, and user experience. While it requires development effort, it ensures long-term maintainability and tight integration with McRogueFace's architecture. - -The abandoned state of python-sfml actually presents an opportunity: we can provide a modern, maintained SFML binding for Python as part of McRogueFace, potentially attracting users who need SFML 2.6+ support. \ No newline at end of file diff --git a/STRATEGIC_VISION.md b/STRATEGIC_VISION.md deleted file mode 100644 index 3d15447..0000000 --- a/STRATEGIC_VISION.md +++ /dev/null @@ -1,226 +0,0 @@ -# McRogueFace Strategic Vision: Beyond Alpha - -## 🎯 Three Transformative Directions - -### 1. **The Roguelike Operating System** 🖥️ - -Transform McRogueFace into a platform where games are apps: - -#### Core Platform Features -- **Game Package Manager**: `mcrf install dungeon-crawler` -- **Hot-swappable Game Modules**: Switch between games without restarting -- **Shared Asset Library**: Common sprites, sounds, and UI components -- **Cross-Game Saves**: Universal character/inventory system -- **Multi-Game Sessions**: Run multiple roguelikes simultaneously in tabs - -#### Technical Implementation -```python -# Future API Example -import mcrfpy.platform as platform - -# Install and launch games -platform.install("nethack-remake") -platform.install("pixel-dungeon-port") - -# Create multi-game session -session = platform.MultiGameSession() -session.add_tab("nethack-remake", save_file="warrior_lvl_15.sav") -session.add_tab("pixel-dungeon-port", new_game=True) -session.run() -``` - -### 2. **AI-Native Game Development** 🤖 - -Position McRogueFace as the first **AI-first roguelike engine**: - -#### Integrated AI Features -- **GPT-Powered NPCs**: Dynamic dialogue and quest generation -- **Procedural Content via LLMs**: Describe a dungeon, AI generates it -- **AI Dungeon Master**: Adaptive difficulty and narrative -- **Code Assistant Integration**: Built-in AI helps write game logic - -#### Revolutionary Possibilities -```python -# AI-Assisted Game Creation -from mcrfpy import ai_tools - -# Natural language level design -dungeon = ai_tools.generate_dungeon(""" - Create a haunted library with 3 floors. - First floor: Reading rooms with ghost librarians - Second floor: Restricted section with magical traps - Third floor: Ancient archive with boss encounter -""") - -# AI-driven NPCs -npc = ai_tools.create_npc( - personality="Grumpy dwarf merchant who secretly loves poetry", - knowledge=["local rumors", "item prices", "hidden treasures"], - dynamic_dialogue=True -) -``` - -### 3. **Web-Native Multiplayer Platform** 🌐 - -Make McRogueFace the **Discord of Roguelikes**: - -#### Multiplayer Revolution -- **Seamless Co-op**: Drop-in/drop-out multiplayer -- **Competitive Modes**: Racing, PvP arenas, daily challenges -- **Spectator System**: Watch and learn from others -- **Cloud Saves**: Play anywhere, sync everywhere -- **Social Features**: Guilds, tournaments, leaderboards - -#### WebAssembly Future -```python -# Future Web API -import mcrfpy.web as web - -# Host a game room -room = web.create_room("Epic Dungeon Run", max_players=4) -room.set_rules(friendly_fire=False, shared_loot=True) -room.open_to_public() - -# Stream gameplay -stream = web.GameStream(room) -stream.to_twitch(channel="awesome_roguelike") -``` - -## 🏗️ Architecture Evolution Roadmap - -### Phase 1: Beta Foundation (3-4 months) -**Focus**: Stability and Polish -- Complete RenderTexture system (#6) -- Implement save/load system -- Add audio mixing and 3D sound -- Create plugin architecture -- **Deliverable**: Beta release with plugin support - -### Phase 2: Platform Infrastructure (6-8 months) -**Focus**: Multi-game Support -- Game package format specification -- Resource sharing system -- Inter-game communication API -- Cloud save infrastructure -- **Deliverable**: McRogueFace Platform 1.0 - -### Phase 3: AI Integration (8-12 months) -**Focus**: AI-Native Features -- LLM integration framework -- Procedural content pipelines -- Natural language game scripting -- AI behavior trees -- **Deliverable**: McRogueFace AI Studio - -### Phase 4: Web Deployment (12-18 months) -**Focus**: Browser-based Gaming -- WebAssembly compilation -- WebRTC multiplayer -- Cloud computation for AI -- Mobile touch controls -- **Deliverable**: play.mcrogueface.com - -## 🎮 Killer App Ideas - -### 1. **Roguelike Maker** (Like Mario Maker) -- Visual dungeon editor -- Share levels online -- Play-test with AI -- Community ratings - -### 2. **The Infinite Dungeon** -- Persistent world all players explore -- Procedurally expands based on player actions -- AI Dungeon Master creates personalized quests -- Cross-platform play - -### 3. **Roguelike Battle Royale** -- 100 players start in connected dungeons -- Dungeons collapse, forcing encounters -- Last adventurer standing wins -- AI-generated commentary - -## 🛠️ Technical Innovations to Pursue - -### 1. **Temporal Debugging** -- Rewind game state -- Fork timelines for "what-if" scenarios -- Visual debugging of entity histories - -### 2. **Neural Tileset Generation** -- Train on existing tilesets -- Generate infinite variations -- Style transfer between games - -### 3. **Quantum Roguelike Mechanics** -- Superposition states for entities -- Probability-based combat -- Observer-effect puzzles - -## 🌍 Community Building Strategy - -### 1. **Education First** -- University partnerships -- Free curriculum: "Learn Python with Roguelikes" -- Summer of Code participation -- Student game jams - -### 2. **Open Core Model** -- Core engine: MIT licensed -- Premium platforms: Cloud, AI, multiplayer -- Revenue sharing for content creators -- Sponsored tournaments - -### 3. **Developer Ecosystem** -- Comprehensive API documentation -- Example games and tutorials -- Asset marketplace -- GitHub integration for mods - -## 🎯 Success Metrics - -### Year 1 Goals -- 1,000+ games created on platform -- 10,000+ monthly active developers -- 3 AAA-quality showcase games -- University curriculum adoption - -### Year 2 Goals -- 100,000+ monthly active players -- $1M in platform transactions -- Major game studio partnership -- Native VR support - -### Year 3 Goals -- #1 roguelike development platform -- IPO or acquisition readiness -- 1M+ monthly active players -- Industry standard for roguelikes - -## 🚀 Next Immediate Actions - -1. **Finish Beta Polish** - - Merge alpha_streamline_2 → master - - Complete RenderTexture (#6) - - Implement basic save/load - -2. **Build Community** - - Launch Discord server - - Create YouTube tutorials - - Host first game jam - -3. **Prototype AI Features** - - Simple GPT integration - - Procedural room descriptions - - Dynamic NPC dialogue - -4. **Plan Platform Architecture** - - Design plugin system - - Spec game package format - - Cloud infrastructure research - ---- - -*"McRogueFace: Not just an engine, but a universe of infinite dungeons."* - -Remember: The best platforms create possibilities their creators never imagined. Build for the community you want to see, and they will create wonders. \ No newline at end of file diff --git a/_test.py b/_test.py deleted file mode 100644 index f4cdb44..0000000 --- a/_test.py +++ /dev/null @@ -1,16 +0,0 @@ -import mcrfpy - -# Create a new scene -mcrfpy.createScene("intro") - -# Add a text caption -caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!") -caption.size = 48 -caption.fill_color = (255, 255, 255) - -# Add to scene -mcrfpy.sceneUI("intro").append(caption) - -# Switch to the scene -mcrfpy.setScene("intro") - diff --git a/automation_example.py b/automation_example.py deleted file mode 100644 index 5d94dc4..0000000 --- a/automation_example.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -""" -McRogueFace Automation API Example - -This demonstrates how to use the automation API for testing game UIs. -The API is PyAutoGUI-compatible for easy migration of existing tests. -""" - -from mcrfpy import automation -import mcrfpy -import time - -def automation_demo(): - """Demonstrate all automation API features""" - - print("=== McRogueFace Automation API Demo ===\n") - - # 1. Screen Information - print("1. Screen Information:") - screen_size = automation.size() - print(f" Screen size: {screen_size[0]}x{screen_size[1]}") - - mouse_pos = automation.position() - print(f" Current mouse position: {mouse_pos}") - - on_screen = automation.onScreen(100, 100) - print(f" Is (100, 100) on screen? {on_screen}") - print() - - # 2. Mouse Movement - print("2. Mouse Movement:") - print(" Moving to center of screen...") - center_x, center_y = screen_size[0]//2, screen_size[1]//2 - automation.moveTo(center_x, center_y, duration=0.5) - - print(" Moving relative by (100, 100)...") - automation.moveRel(100, 100, duration=0.5) - print() - - # 3. Mouse Clicks - print("3. Mouse Clicks:") - print(" Single click...") - automation.click() - time.sleep(0.2) - - print(" Double click...") - automation.doubleClick() - time.sleep(0.2) - - print(" Right click...") - automation.rightClick() - time.sleep(0.2) - - print(" Triple click...") - automation.tripleClick() - print() - - # 4. Keyboard Input - print("4. Keyboard Input:") - print(" Typing message...") - automation.typewrite("Hello from McRogueFace automation!", interval=0.05) - - print(" Pressing Enter...") - automation.keyDown("enter") - automation.keyUp("enter") - - print(" Hotkey Ctrl+A (select all)...") - automation.hotkey("ctrl", "a") - print() - - # 5. Drag Operations - print("5. Drag Operations:") - print(" Dragging from current position to (500, 500)...") - automation.dragTo(500, 500, duration=1.0) - - print(" Dragging relative by (-100, -100)...") - automation.dragRel(-100, -100, duration=0.5) - print() - - # 6. Scroll Operations - print("6. Scroll Operations:") - print(" Scrolling up 5 clicks...") - automation.scroll(5) - time.sleep(0.5) - - print(" Scrolling down 5 clicks...") - automation.scroll(-5) - print() - - # 7. Screenshots - print("7. Screenshots:") - print(" Taking screenshot...") - success = automation.screenshot("automation_demo_screenshot.png") - print(f" Screenshot saved: {success}") - print() - - print("=== Demo Complete ===") - -def create_test_ui(): - """Create a simple UI for testing automation""" - print("Creating test UI...") - - # Create a test scene - mcrfpy.createScene("automation_test") - mcrfpy.setScene("automation_test") - - # Add some UI elements - ui = mcrfpy.sceneUI("automation_test") - - # Add a frame - frame = mcrfpy.Frame(50, 50, 300, 200) - ui.append(frame) - - # Add a caption - caption = mcrfpy.Caption(60, 60, "Automation Test UI") - ui.append(caption) - - print("Test UI created!") - -if __name__ == "__main__": - # Create test UI first - create_test_ui() - - # Run automation demo - automation_demo() - - print("\nYou can now use the automation API to test your game!") \ No newline at end of file diff --git a/automation_exec_examples.py b/automation_exec_examples.py deleted file mode 100644 index 1145d2b..0000000 --- a/automation_exec_examples.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/usr/bin/env python3 -""" -Examples of automation patterns using the proposed --exec flag - -Usage: - ./mcrogueface game.py --exec automation_basic.py - ./mcrogueface game.py --exec automation_stress.py --exec monitor.py -""" - -# ===== automation_basic.py ===== -# Basic automation that runs alongside the game - -import mcrfpy -from mcrfpy import automation -import time - -class GameAutomation: - """Automated testing that runs periodically""" - - def __init__(self): - self.test_count = 0 - self.test_results = [] - - def run_test_suite(self): - """Called by timer - runs one test per invocation""" - test_name = f"test_{self.test_count}" - - try: - if self.test_count == 0: - # Test main menu - self.test_main_menu() - elif self.test_count == 1: - # Test inventory - self.test_inventory() - elif self.test_count == 2: - # Test combat - self.test_combat() - else: - # All tests complete - self.report_results() - return - - self.test_results.append((test_name, "PASS")) - except Exception as e: - self.test_results.append((test_name, f"FAIL: {e}")) - - self.test_count += 1 - - def test_main_menu(self): - """Test main menu interactions""" - automation.screenshot("test_main_menu_before.png") - automation.click(400, 300) # New Game button - time.sleep(0.5) - automation.screenshot("test_main_menu_after.png") - - def test_inventory(self): - """Test inventory system""" - automation.hotkey("i") # Open inventory - time.sleep(0.5) - automation.screenshot("test_inventory_open.png") - - # Drag item - automation.moveTo(100, 200) - automation.dragTo(200, 200, duration=0.5) - - automation.hotkey("i") # Close inventory - - def test_combat(self): - """Test combat system""" - # Move character - automation.keyDown("w") - time.sleep(0.5) - automation.keyUp("w") - - # Attack - automation.click(500, 400) - automation.screenshot("test_combat.png") - - def report_results(self): - """Generate test report""" - print("\n=== Automation Test Results ===") - for test, result in self.test_results: - print(f"{test}: {result}") - print(f"Total: {len(self.test_results)} tests") - - # Stop the timer - mcrfpy.delTimer("automation_suite") - -# Create automation instance and register timer -auto = GameAutomation() -mcrfpy.setTimer("automation_suite", auto.run_test_suite, 2000) # Run every 2 seconds - -print("Game automation started - tests will run every 2 seconds") - - -# ===== automation_stress.py ===== -# Stress testing with random inputs - -import mcrfpy -from mcrfpy import automation -import random - -class StressTester: - """Randomly interact with the game to find edge cases""" - - def __init__(self): - self.action_count = 0 - self.errors = [] - - def random_action(self): - """Perform a random UI action""" - try: - action = random.choice([ - self.random_click, - self.random_key, - self.random_drag, - self.random_hotkey - ]) - action() - self.action_count += 1 - - # Periodic screenshot - if self.action_count % 50 == 0: - automation.screenshot(f"stress_test_{self.action_count}.png") - print(f"Stress test: {self.action_count} actions performed") - - except Exception as e: - self.errors.append((self.action_count, str(e))) - - def random_click(self): - x = random.randint(0, 1024) - y = random.randint(0, 768) - button = random.choice(["left", "right"]) - automation.click(x, y, button=button) - - def random_key(self): - key = random.choice([ - "a", "b", "c", "d", "w", "s", - "space", "enter", "escape", - "1", "2", "3", "4", "5" - ]) - automation.keyDown(key) - automation.keyUp(key) - - def random_drag(self): - x1 = random.randint(0, 1024) - y1 = random.randint(0, 768) - x2 = random.randint(0, 1024) - y2 = random.randint(0, 768) - automation.moveTo(x1, y1) - automation.dragTo(x2, y2, duration=0.2) - - def random_hotkey(self): - modifier = random.choice(["ctrl", "alt", "shift"]) - key = random.choice(["a", "s", "d", "f"]) - automation.hotkey(modifier, key) - -# Create stress tester and run frequently -stress = StressTester() -mcrfpy.setTimer("stress_test", stress.random_action, 100) # Every 100ms - -print("Stress testing started - random actions every 100ms") - - -# ===== monitor.py ===== -# Performance and state monitoring - -import mcrfpy -from mcrfpy import automation -import json -import time - -class PerformanceMonitor: - """Monitor game performance and state""" - - def __init__(self): - self.samples = [] - self.start_time = time.time() - - def collect_sample(self): - """Collect performance data""" - sample = { - "timestamp": time.time() - self.start_time, - "fps": mcrfpy.getFPS() if hasattr(mcrfpy, 'getFPS') else 60, - "scene": mcrfpy.currentScene(), - "memory": self.estimate_memory_usage() - } - self.samples.append(sample) - - # Log every 10 samples - if len(self.samples) % 10 == 0: - avg_fps = sum(s["fps"] for s in self.samples[-10:]) / 10 - print(f"Average FPS (last 10 samples): {avg_fps:.1f}") - - # Save data every 100 samples - if len(self.samples) % 100 == 0: - self.save_report() - - def estimate_memory_usage(self): - """Estimate memory usage based on scene complexity""" - # This is a placeholder - real implementation would use psutil - ui_count = len(mcrfpy.sceneUI(mcrfpy.currentScene())) - return ui_count * 1000 # Rough estimate in KB - - def save_report(self): - """Save performance report""" - with open("performance_report.json", "w") as f: - json.dump({ - "samples": self.samples, - "summary": { - "total_samples": len(self.samples), - "duration": time.time() - self.start_time, - "avg_fps": sum(s["fps"] for s in self.samples) / len(self.samples) - } - }, f, indent=2) - print(f"Performance report saved ({len(self.samples)} samples)") - -# Create monitor and start collecting -monitor = PerformanceMonitor() -mcrfpy.setTimer("performance_monitor", monitor.collect_sample, 1000) # Every second - -print("Performance monitoring started - sampling every second") - - -# ===== automation_replay.py ===== -# Record and replay user actions - -import mcrfpy -from mcrfpy import automation -import json -import time - -class ActionRecorder: - """Record user actions for replay""" - - def __init__(self): - self.recording = False - self.actions = [] - self.start_time = None - - def start_recording(self): - """Start recording user actions""" - self.recording = True - self.actions = [] - self.start_time = time.time() - print("Recording started - perform actions to record") - - # Register callbacks for all input types - mcrfpy.registerPyAction("record_click", self.record_click) - mcrfpy.registerPyAction("record_key", self.record_key) - - # Map all mouse buttons - for button in range(3): - mcrfpy.registerInputAction(8192 + button, "record_click") - - # Map common keys - for key in range(256): - mcrfpy.registerInputAction(4096 + key, "record_key") - - def record_click(self, action_type): - """Record mouse click""" - if not self.recording or action_type != "start": - return - - pos = automation.position() - self.actions.append({ - "type": "click", - "time": time.time() - self.start_time, - "x": pos[0], - "y": pos[1] - }) - - def record_key(self, action_type): - """Record key press""" - if not self.recording or action_type != "start": - return - - # This is simplified - real implementation would decode the key - self.actions.append({ - "type": "key", - "time": time.time() - self.start_time, - "key": "unknown" - }) - - def stop_recording(self): - """Stop recording and save""" - self.recording = False - with open("recorded_actions.json", "w") as f: - json.dump(self.actions, f, indent=2) - print(f"Recording stopped - {len(self.actions)} actions saved") - - def replay_actions(self): - """Replay recorded actions""" - print("Replaying recorded actions...") - - with open("recorded_actions.json", "r") as f: - actions = json.load(f) - - start_time = time.time() - action_index = 0 - - def replay_next(): - nonlocal action_index - if action_index >= len(actions): - print("Replay complete") - mcrfpy.delTimer("replay") - return - - action = actions[action_index] - current_time = time.time() - start_time - - # Wait until it's time for this action - if current_time >= action["time"]: - if action["type"] == "click": - automation.click(action["x"], action["y"]) - elif action["type"] == "key": - automation.keyDown(action["key"]) - automation.keyUp(action["key"]) - - action_index += 1 - - mcrfpy.setTimer("replay", replay_next, 10) # Check every 10ms - -# Example usage - would be controlled by UI -recorder = ActionRecorder() - -# To start recording: -# recorder.start_recording() - -# To stop and save: -# recorder.stop_recording() - -# To replay: -# recorder.replay_actions() - -print("Action recorder ready - call recorder.start_recording() to begin") \ No newline at end of file diff --git a/clean.sh b/clean.sh deleted file mode 100755 index 817a9ee..0000000 --- a/clean.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -# Clean script for McRogueFace - removes build artifacts - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -echo -e "${YELLOW}Cleaning McRogueFace build artifacts...${NC}" - -# Remove build directory -if [ -d "build" ]; then - echo "Removing build directory..." - rm -rf build -fi - -# Remove CMake artifacts from project root -echo "Removing CMake artifacts from project root..." -rm -f CMakeCache.txt -rm -f cmake_install.cmake -rm -f Makefile -rm -rf CMakeFiles - -# Remove compiled executable from project root -rm -f mcrogueface - -# Remove any test artifacts -rm -f test_script.py -rm -rf test_venv -rm -f python3 # symlink - -echo -e "${GREEN}Clean complete!${NC}" \ No newline at end of file diff --git a/example_automation.py b/example_automation.py deleted file mode 100644 index a31375a..0000000 --- a/example_automation.py +++ /dev/null @@ -1,63 +0,0 @@ -#!/usr/bin/env python3 -""" -Example automation script using --exec flag -Usage: ./mcrogueface game.py --exec example_automation.py -""" -import mcrfpy -from mcrfpy import automation - -class GameAutomation: - def __init__(self): - self.frame_count = 0 - self.test_phase = 0 - print("Automation: Initialized") - - def periodic_test(self): - """Called every second to perform automation tasks""" - self.frame_count = mcrfpy.getFrame() - - print(f"Automation: Running test at frame {self.frame_count}") - - # Take periodic screenshots - if self.test_phase % 5 == 0: - filename = f"automation_screenshot_{self.test_phase}.png" - automation.screenshot(filename) - print(f"Automation: Saved {filename}") - - # Simulate user input based on current scene - scene = mcrfpy.currentScene() - print(f"Automation: Current scene is '{scene}'") - - if scene == "main_menu" and self.test_phase < 5: - # Click start button - automation.click(512, 400) - print("Automation: Clicked start button") - elif scene == "game": - # Perform game actions - if self.test_phase % 3 == 0: - automation.hotkey("i") # Toggle inventory - print("Automation: Toggled inventory") - else: - # Random movement - import random - key = random.choice(["w", "a", "s", "d"]) - automation.keyDown(key) - automation.keyUp(key) - print(f"Automation: Pressed '{key}' key") - - self.test_phase += 1 - - # Stop after 20 tests - if self.test_phase >= 20: - print("Automation: Test suite complete") - mcrfpy.delTimer("automation_test") - # Could also call mcrfpy.quit() to exit the game - -# Create automation instance -automation_instance = GameAutomation() - -# Register periodic timer -mcrfpy.setTimer("automation_test", automation_instance.periodic_test, 1000) - -print("Automation: Script loaded - tests will run every second") -print("Automation: The game and automation share the same Python environment") \ No newline at end of file diff --git a/example_config.py b/example_config.py deleted file mode 100644 index 0f0ef7e..0000000 --- a/example_config.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -""" -Example configuration script that sets up shared state for other scripts -Usage: ./mcrogueface --exec example_config.py --exec example_automation.py game.py -""" -import mcrfpy - -# Create a shared configuration namespace -class AutomationConfig: - # Test settings - test_enabled = True - screenshot_interval = 5 # Take screenshot every N tests - max_test_count = 50 - test_delay_ms = 1000 - - # Monitoring settings - monitor_enabled = True - monitor_interval_ms = 500 - report_delay_seconds = 30 - - # Game-specific settings - start_button_pos = (512, 400) - inventory_key = "i" - movement_keys = ["w", "a", "s", "d"] - - # Shared state - test_results = [] - performance_data = [] - - @classmethod - def log_result(cls, test_name, success, details=""): - """Log a test result""" - cls.test_results.append({ - "test": test_name, - "success": success, - "details": details, - "frame": mcrfpy.getFrame() - }) - - @classmethod - def get_summary(cls): - """Get test summary""" - total = len(cls.test_results) - passed = sum(1 for r in cls.test_results if r["success"]) - return f"Tests: {passed}/{total} passed" - -# Attach config to mcrfpy module so other scripts can access it -mcrfpy.automation_config = AutomationConfig - -print("Config: Automation configuration loaded") -print(f"Config: Test delay = {AutomationConfig.test_delay_ms}ms") -print(f"Config: Max tests = {AutomationConfig.max_test_count}") -print("Config: Other scripts can access config via mcrfpy.automation_config") \ No newline at end of file diff --git a/example_monitoring.py b/example_monitoring.py deleted file mode 100644 index 13e98cb..0000000 --- a/example_monitoring.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -Example monitoring script that works alongside automation -Usage: ./mcrogueface game.py --exec example_automation.py --exec example_monitoring.py -""" -import mcrfpy -import time - -class PerformanceMonitor: - def __init__(self): - self.start_time = time.time() - self.frame_samples = [] - self.scene_changes = [] - self.last_scene = None - print("Monitor: Performance monitoring initialized") - - def collect_metrics(self): - """Collect performance and state metrics""" - current_frame = mcrfpy.getFrame() - current_time = time.time() - self.start_time - current_scene = mcrfpy.currentScene() - - # Track frame rate - if len(self.frame_samples) > 0: - last_frame, last_time = self.frame_samples[-1] - fps = (current_frame - last_frame) / (current_time - last_time) - print(f"Monitor: FPS = {fps:.1f}") - - self.frame_samples.append((current_frame, current_time)) - - # Track scene changes - if current_scene != self.last_scene: - print(f"Monitor: Scene changed from '{self.last_scene}' to '{current_scene}'") - self.scene_changes.append((current_time, self.last_scene, current_scene)) - self.last_scene = current_scene - - # Keep only last 100 samples - if len(self.frame_samples) > 100: - self.frame_samples = self.frame_samples[-100:] - - def generate_report(self): - """Generate a summary report""" - if len(self.frame_samples) < 2: - return - - total_frames = self.frame_samples[-1][0] - self.frame_samples[0][0] - total_time = self.frame_samples[-1][1] - self.frame_samples[0][1] - avg_fps = total_frames / total_time - - print("\n=== Performance Report ===") - print(f"Monitor: Total time: {total_time:.1f} seconds") - print(f"Monitor: Total frames: {total_frames}") - print(f"Monitor: Average FPS: {avg_fps:.1f}") - print(f"Monitor: Scene changes: {len(self.scene_changes)}") - - # Stop monitoring - mcrfpy.delTimer("performance_monitor") - -# Create monitor instance -monitor = PerformanceMonitor() - -# Register monitoring timer (runs every 500ms) -mcrfpy.setTimer("performance_monitor", monitor.collect_metrics, 500) - -# Register report generation (runs after 30 seconds) -mcrfpy.setTimer("performance_report", monitor.generate_report, 30000) - -print("Monitor: Script loaded - collecting metrics every 500ms") -print("Monitor: Will generate report after 30 seconds") \ No newline at end of file diff --git a/exec_flag_implementation.cpp b/exec_flag_implementation.cpp deleted file mode 100644 index 3173585..0000000 --- a/exec_flag_implementation.cpp +++ /dev/null @@ -1,189 +0,0 @@ -// Example implementation of --exec flag for McRogueFace -// This shows the minimal changes needed to support multiple script execution - -// === In McRogueFaceConfig.h === -struct McRogueFaceConfig { - // ... existing fields ... - - // Scripts to execute after main script (McRogueFace style) - std::vector exec_scripts; -}; - -// === In CommandLineParser.cpp === -CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) { - // ... existing parsing code ... - - for (int i = 1; i < argc; i++) { - std::string arg = argv[i]; - - // ... existing flag handling ... - - else if (arg == "--exec") { - // Add script to exec list - if (i + 1 < argc) { - config.exec_scripts.push_back(argv[++i]); - } else { - std::cerr << "Error: --exec requires a script path\n"; - return {true, 1}; - } - } - } -} - -// === In GameEngine.cpp === -GameEngine::GameEngine(const McRogueFaceConfig& cfg) : config(cfg) { - // ... existing initialization ... - - // Only load game.py if no custom script/command/module is specified - bool should_load_game = config.script_path.empty() && - config.python_command.empty() && - config.python_module.empty() && - !config.interactive_mode && - !config.python_mode && - config.exec_scripts.empty(); // Add this check - - if (should_load_game) { - if (!Py_IsInitialized()) { - McRFPy_API::api_init(); - } - McRFPy_API::executePyString("import mcrfpy"); - McRFPy_API::executeScript("scripts/game.py"); - } - - // Execute any --exec scripts - for (const auto& exec_script : config.exec_scripts) { - std::cout << "Executing script: " << exec_script << std::endl; - McRFPy_API::executeScript(exec_script.string()); - } -} - -// === Usage Examples === - -// Example 1: Run game with automation -// ./mcrogueface game.py --exec automation.py - -// Example 2: Run game with multiple automation scripts -// ./mcrogueface game.py --exec test_suite.py --exec monitor.py --exec logger.py - -// Example 3: Run only automation (no game) -// ./mcrogueface --exec standalone_test.py - -// Example 4: Headless automation -// ./mcrogueface --headless game.py --exec automation.py - -// === Python Script Example (automation.py) === -/* -import mcrfpy -from mcrfpy import automation - -def periodic_test(): - """Run automated tests every 5 seconds""" - # Take screenshot - automation.screenshot(f"test_{mcrfpy.getFrame()}.png") - - # Check game state - scene = mcrfpy.currentScene() - if scene == "main_menu": - # Click start button - automation.click(400, 300) - elif scene == "game": - # Perform game tests - automation.hotkey("i") # Open inventory - - print(f"Test completed at frame {mcrfpy.getFrame()}") - -# Register timer for periodic testing -mcrfpy.setTimer("automation_test", periodic_test, 5000) - -print("Automation script loaded - tests will run every 5 seconds") - -# Script returns here - giving control back to C++ -*/ - -// === Advanced Example: Event-Driven Automation === -/* -# automation_advanced.py - -import mcrfpy -from mcrfpy import automation -import json - -class AutomationFramework: - def __init__(self): - self.test_queue = [] - self.results = [] - self.load_test_suite() - - def load_test_suite(self): - """Load test definitions from JSON""" - with open("test_suite.json") as f: - self.test_queue = json.load(f)["tests"] - - def run_next_test(self): - """Execute next test in queue""" - if not self.test_queue: - self.finish_testing() - return - - test = self.test_queue.pop(0) - - try: - if test["type"] == "click": - automation.click(test["x"], test["y"]) - elif test["type"] == "key": - automation.keyDown(test["key"]) - automation.keyUp(test["key"]) - elif test["type"] == "screenshot": - automation.screenshot(test["filename"]) - elif test["type"] == "wait": - # Re-queue this test for later - self.test_queue.insert(0, test) - return - - self.results.append({"test": test, "status": "pass"}) - except Exception as e: - self.results.append({"test": test, "status": "fail", "error": str(e)}) - - def finish_testing(self): - """Save test results and cleanup""" - with open("test_results.json", "w") as f: - json.dump(self.results, f, indent=2) - print(f"Testing complete: {len(self.results)} tests executed") - mcrfpy.delTimer("automation_framework") - -# Create and start automation -framework = AutomationFramework() -mcrfpy.setTimer("automation_framework", framework.run_next_test, 100) -*/ - -// === Thread Safety Considerations === - -// The --exec approach requires NO thread safety changes because: -// 1. All scripts run in the same Python interpreter -// 2. Scripts execute sequentially during initialization -// 3. After initialization, only callbacks run (timer/input based) -// 4. C++ maintains control of the render loop - -// This is the "honor system" - scripts must: -// - Set up their callbacks/timers -// - Return control to C++ -// - Not block or run infinite loops -// - Use timers for periodic tasks - -// === Future Extensions === - -// 1. Script communication via shared Python modules -// game.py: -// import mcrfpy -// mcrfpy.game_state = {"level": 1, "score": 0} -// -// automation.py: -// import mcrfpy -// if mcrfpy.game_state["level"] == 1: -// # Test level 1 specific features - -// 2. Priority-based script execution -// ./mcrogueface game.py --exec-priority high:critical.py --exec-priority low:logging.py - -// 3. Conditional execution -// ./mcrogueface game.py --exec-if-scene menu:menu_test.py --exec-if-scene game:game_test.py \ No newline at end of file diff --git a/gitea_issues.py b/gitea_issues.py deleted file mode 100644 index 9ba8bd9..0000000 --- a/gitea_issues.py +++ /dev/null @@ -1,102 +0,0 @@ -import json -from time import time -#with open("/home/john/issues.json", "r") as f: -# data = json.loads(f.read()) -#with open("/home/john/issues2.json", "r") as f: -# data.extend(json.loads(f.read())) - -print("Fetching issues...", end='') -start = time() -from gitea import Gitea, Repository, Issue -g = Gitea("https://gamedev.ffwf.net/gitea", token_text="3b450f66e21d62c22bb9fa1c8b975049a5d0c38d") -repo = Repository.request(g, "john", "McRogueFace") -issues = repo.get_issues() -dur = time() - start -print(f"({dur:.1f}s)") -print("Gitea Version: " + g.get_version()) -print("API-Token belongs to user: " + g.get_user().username) - -data = [ - { - "labels": i.labels, - "body": i.body, - "number": i.number, - } - for i in issues - ] - -input() - -def front_number(txt): - if not txt[0].isdigit(): return None - number = "" - for c in txt: - if not c.isdigit(): - break - number += c - return int(number) - -def split_any(txt, splitters): - tokens = [] - txt = [txt] - for s in splitters: - for t in txt: - tokens.extend(t.split(s)) - txt = tokens - tokens = [] - return txt - -def find_refs(txt): - tokens = [tok for tok in split_any(txt, ' ,;\t\r\n') if tok.startswith('#')] - return [front_number(tok[1:]) for tok in tokens] - -from collections import defaultdict -issue_relations = defaultdict(list) - -nodes = set() - -for issue in data: - #refs = issue['body'].split('#')[1::2] - - #refs = [front_number(r) for r in refs if front_number(r) is not None] - refs = find_refs(issue['body']) - print(issue['number'], ':', refs) - issue_relations[issue['number']].extend(refs) - nodes.add(issue['number']) - for r in refs: - nodes.add(r) - issue_relations[r].append(issue['number']) - - -# Find issue labels -issue_labels = {} -for d in data: - labels = [l['name'] for l in d['labels']] - #print(d['number'], labels) - issue_labels[d['number']] = labels - -import networkx as nx -import matplotlib.pyplot as plt - -relations = nx.Graph() - -for k in issue_relations: - relations.add_node(k) - for r in issue_relations[k]: - relations.add_edge(k, r) - relations.add_edge(r, k) - -#nx.draw_networkx(relations) - -pos = nx.spring_layout(relations) -nx.draw_networkx_nodes(relations, pos, - nodelist = [n for n in issue_labels if 'Alpha Release Requirement' in issue_labels[n]], - node_color="tab:red") -nx.draw_networkx_nodes(relations, pos, - nodelist = [n for n in issue_labels if 'Alpha Release Requirement' not in issue_labels[n]], - node_color="tab:blue") -nx.draw_networkx_edges(relations, pos, - edgelist = relations.edges() - ) -nx.draw_networkx_labels(relations, pos, {i: str(i) for i in relations.nodes()}) -plt.show() \ No newline at end of file diff --git a/src/GameEngine.cpp b/src/GameEngine.cpp index 836fe02..a5a195b 100644 --- a/src/GameEngine.cpp +++ b/src/GameEngine.cpp @@ -26,7 +26,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) render_target = &headless_renderer->getRenderTarget(); } else { window = std::make_unique(); - window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close | sf::Style::Resize); + window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close); window->setFramerateLimit(60); render_target = window.get(); } @@ -73,81 +73,19 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg) GameEngine::~GameEngine() { - cleanup(); for (auto& [name, scene] : scenes) { delete scene; } } -void GameEngine::cleanup() -{ - if (cleaned_up) return; - cleaned_up = true; - - // Clear Python references before destroying C++ objects - // Clear all timers (they hold Python callables) - timers.clear(); - - // Clear McRFPy_API's reference to this game engine - if (McRFPy_API::game == this) { - McRFPy_API::game = nullptr; - } - - // Force close the window if it's still open - if (window && window->isOpen()) { - window->close(); - } -} - Scene* GameEngine::currentScene() { return scenes[scene]; } void GameEngine::changeScene(std::string s) { - changeScene(s, TransitionType::None, 0.0f); -} - -void GameEngine::changeScene(std::string sceneName, TransitionType transitionType, float duration) -{ - if (scenes.find(sceneName) == scenes.end()) - { - std::cout << "Attempted to change to a scene that doesn't exist (`" << sceneName << "`)" << std::endl; - return; - } - - if (transitionType == TransitionType::None || duration <= 0.0f) - { - // Immediate scene change - std::string old_scene = scene; - scene = sceneName; - - // Trigger Python scene lifecycle events - McRFPy_API::triggerSceneChange(old_scene, sceneName); - } + /*std::cout << "Current scene is now '" << s << "'\n";*/ + if (scenes.find(s) != scenes.end()) + scene = s; else - { - // Start transition - transition.start(transitionType, scene, sceneName, duration); - - // Render current scene to texture - sf::RenderTarget* original_target = render_target; - render_target = transition.oldSceneTexture.get(); - transition.oldSceneTexture->clear(); - currentScene()->render(); - transition.oldSceneTexture->display(); - - // Change to new scene - std::string old_scene = scene; - scene = sceneName; - - // Render new scene to texture - render_target = transition.newSceneTexture.get(); - transition.newSceneTexture->clear(); - currentScene()->render(); - transition.newSceneTexture->display(); - - // Restore original render target and scene - render_target = original_target; - scene = old_scene; - } + std::cout << "Attempted to change to a scene that doesn't exist (`" << s << "`)" << std::endl; } void GameEngine::quit() { running = false; } void GameEngine::setPause(bool p) { paused = p; } @@ -181,15 +119,9 @@ void GameEngine::run() clock.restart(); while (running) { - // Reset per-frame metrics - metrics.resetPerFrame(); - currentScene()->update(); testTimers(); - // Update Python scenes - McRFPy_API::updatePythonScenes(frameTime); - // Update animations (only if frameTime is valid) if (frameTime > 0.0f && frameTime < 1.0f) { AnimationManager::getInstance().update(frameTime); @@ -201,33 +133,7 @@ void GameEngine::run() if (!paused) { } - - // Handle scene transitions - if (transition.type != TransitionType::None) - { - transition.update(frameTime); - - if (transition.isComplete()) - { - // Transition complete - finalize scene change - scene = transition.toScene; - transition.type = TransitionType::None; - - // Trigger Python scene lifecycle events - McRFPy_API::triggerSceneChange(transition.fromScene, transition.toScene); - } - else - { - // Render transition - render_target->clear(); - transition.render(*render_target); - } - } - else - { - // Normal scene rendering - currentScene()->render(); - } + currentScene()->render(); // Display the frame if (headless) { @@ -244,12 +150,8 @@ void GameEngine::run() currentFrame++; frameTime = clock.restart().asSeconds(); fps = 1 / frameTime; - - // Update profiling metrics - metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds - - int whole_fps = metrics.fps; - int tenth_fps = (metrics.fps * 10) % 10; + int whole_fps = (int)fps; + int tenth_fps = int(fps * 100) % 10; if (!headless && window) { window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS"); @@ -260,18 +162,6 @@ void GameEngine::run() running = false; } } - - // Clean up before exiting the run loop - cleanup(); -} - -std::shared_ptr GameEngine::getTimer(const std::string& name) -{ - auto it = timers.find(name); - if (it != timers.end()) { - return it->second; - } - return nullptr; } void GameEngine::manageTimer(std::string name, PyObject* target, int interval) @@ -318,15 +208,9 @@ void GameEngine::processEvent(const sf::Event& event) int actionCode = 0; if (event.type == sf::Event::Closed) { running = false; return; } - // Handle window resize events + // TODO: add resize event to Scene to react; call it after constructor too, maybe else if (event.type == sf::Event::Resized) { - // Update the view to match the new window size - sf::FloatRect visibleArea(0, 0, event.size.width, event.size.height); - visible = sf::View(visibleArea); - render_target->setView(visible); - - // Notify Python scenes about the resize - McRFPy_API::triggerResize(event.size.width, event.size.height); + return; // 7DRL short circuit. Resizing manually disabled } else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start"; @@ -386,27 +270,3 @@ std::shared_ptr>> GameEngine::scene_ui(s if (scenes.count(target) == 0) return NULL; return scenes[target]->ui_elements; } - -void GameEngine::setWindowTitle(const std::string& title) -{ - window_title = title; - if (!headless && window) { - window->setTitle(title); - } -} - -void GameEngine::setVSync(bool enabled) -{ - vsync_enabled = enabled; - if (!headless && window) { - window->setVerticalSyncEnabled(enabled); - } -} - -void GameEngine::setFramerateLimit(unsigned int limit) -{ - framerate_limit = limit; - if (!headless && window) { - window->setFramerateLimit(limit); - } -} diff --git a/src/GameEngine.h b/src/GameEngine.h index 1a0a235..02e02ae 100644 --- a/src/GameEngine.h +++ b/src/GameEngine.h @@ -8,7 +8,6 @@ #include "PyCallable.h" #include "McRogueFaceConfig.h" #include "HeadlessRenderer.h" -#include "SceneTransition.h" #include class GameEngine @@ -29,63 +28,19 @@ class GameEngine bool headless = false; McRogueFaceConfig config; - bool cleaned_up = false; - - // Window state tracking - bool vsync_enabled = false; - unsigned int framerate_limit = 60; - - // Scene transition state - SceneTransition transition; - void testTimers(); - -public: sf::Clock runtime; //std::map timers; std::map> timers; + void testTimers(); + +public: std::string scene; - - // Profiling metrics - struct ProfilingMetrics { - float frameTime = 0.0f; // Current frame time in milliseconds - float avgFrameTime = 0.0f; // Average frame time over last N frames - int fps = 0; // Frames per second - int drawCalls = 0; // Draw calls per frame - int uiElements = 0; // Number of UI elements rendered - int visibleElements = 0; // Number of visible elements - - // Frame time history for averaging - static constexpr int HISTORY_SIZE = 60; - float frameTimeHistory[HISTORY_SIZE] = {0}; - int historyIndex = 0; - - void updateFrameTime(float deltaMs) { - frameTime = deltaMs; - frameTimeHistory[historyIndex] = deltaMs; - historyIndex = (historyIndex + 1) % HISTORY_SIZE; - - // Calculate average - float sum = 0.0f; - for (int i = 0; i < HISTORY_SIZE; ++i) { - sum += frameTimeHistory[i]; - } - avgFrameTime = sum / HISTORY_SIZE; - fps = avgFrameTime > 0 ? static_cast(1000.0f / avgFrameTime) : 0; - } - - void resetPerFrame() { - drawCalls = 0; - uiElements = 0; - visibleElements = 0; - } - } metrics; GameEngine(); GameEngine(const McRogueFaceConfig& cfg); ~GameEngine(); Scene* currentScene(); void changeScene(std::string); - void changeScene(std::string sceneName, TransitionType transitionType, float duration); void createScene(std::string); void quit(); void setPause(bool); @@ -95,23 +50,13 @@ public: sf::RenderTarget* getRenderTargetPtr() { return render_target; } void run(); void sUserInput(); - void cleanup(); // Clean up Python references before destruction int getFrame() { return currentFrame; } float getFrameTime() { return frameTime; } sf::View getView() { return visible; } void manageTimer(std::string, PyObject*, int); - std::shared_ptr getTimer(const std::string& name); void setWindowScale(float); bool isHeadless() const { return headless; } void processEvent(const sf::Event& event); - - // Window property accessors - const std::string& getWindowTitle() const { return window_title; } - void setWindowTitle(const std::string& title); - bool getVSync() const { return vsync_enabled; } - void setVSync(bool enabled); - unsigned int getFramerateLimit() const { return framerate_limit; } - void setFramerateLimit(unsigned int limit); // global textures for scripts to access std::vector textures; diff --git a/src/McRFPy_API.cpp b/src/McRFPy_API.cpp index 1e8c212..a792150 100644 --- a/src/McRFPy_API.cpp +++ b/src/McRFPy_API.cpp @@ -2,10 +2,6 @@ #include "McRFPy_Automation.h" #include "platform.h" #include "PyAnimation.h" -#include "PyDrawable.h" -#include "PyTimer.h" -#include "PyWindow.h" -#include "PySceneObject.h" #include "GameEngine.h" #include "UI.h" #include "Resources.h" @@ -13,9 +9,9 @@ #include #include -std::vector* McRFPy_API::soundbuffers = nullptr; -sf::Music* McRFPy_API::music = nullptr; -sf::Sound* McRFPy_API::sfx = nullptr; +std::vector McRFPy_API::soundbuffers; +sf::Music McRFPy_API::music; +sf::Sound McRFPy_API::sfx; std::shared_ptr McRFPy_API::default_font; std::shared_ptr McRFPy_API::default_texture; @@ -35,7 +31,7 @@ static PyMethodDef mcrfpyMethods[] = { {"sceneUI", McRFPy_API::_sceneUI, METH_VARARGS, "sceneUI(scene) - Returns a list of UI elements"}, {"currentScene", McRFPy_API::_currentScene, METH_VARARGS, "currentScene() - Current scene's name. Returns a string"}, - {"setScene", McRFPy_API::_setScene, METH_VARARGS, "setScene(scene, transition=None, duration=0.0) - transition to a different scene. Transition can be 'fade', 'slide_left', 'slide_right', 'slide_up', or 'slide_down'"}, + {"setScene", McRFPy_API::_setScene, METH_VARARGS, "setScene(scene) - transition to a different scene"}, {"createScene", McRFPy_API::_createScene, METH_VARARGS, "createScene(scene) - create a new blank scene with given name"}, {"keypressScene", McRFPy_API::_keypressScene, METH_VARARGS, "keypressScene(callable) - assign a callable object to the current scene receive keypress events"}, @@ -43,12 +39,6 @@ static PyMethodDef mcrfpyMethods[] = { {"delTimer", McRFPy_API::_delTimer, METH_VARARGS, "delTimer(name:str) - stop calling the timer labelled with `name`"}, {"exit", McRFPy_API::_exit, METH_VARARGS, "exit() - close down the game engine"}, {"setScale", McRFPy_API::_setScale, METH_VARARGS, "setScale(multiplier:float) - resize the window (still 1024x768, but bigger)"}, - - {"find", McRFPy_API::_find, METH_VARARGS, "find(name, scene=None) - find first UI element with given name"}, - {"findAll", McRFPy_API::_findAll, METH_VARARGS, "findAll(pattern, scene=None) - find all UI elements matching name pattern (supports * wildcards)"}, - - {"getMetrics", McRFPy_API::_getMetrics, METH_VARARGS, "getMetrics() - get performance metrics (returns dict)"}, - {NULL, NULL, 0, NULL} }; @@ -79,9 +69,6 @@ PyObject* PyInit_mcrfpy() /*SFML exposed types*/ &PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType, - /*Base classes*/ - &PyDrawableType, - /*UI widgets*/ &PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType, @@ -94,26 +81,7 @@ PyObject* PyInit_mcrfpy() /*animation*/ &PyAnimationType, - - /*timer*/ - &PyTimerType, - - /*window singleton*/ - &PyWindowType, - - /*scene class*/ - &PySceneType, - nullptr}; - - // Set up PyWindowType methods and getsetters before PyType_Ready - PyWindowType.tp_methods = PyWindow::methods; - PyWindowType.tp_getset = PyWindow::getsetters; - - // Set up PySceneType methods and getsetters - PySceneType.tp_methods = PySceneClass::methods; - PySceneType.tp_getset = PySceneClass::getsetters; - int i = 0; auto t = pytypes[i]; while (t != nullptr) @@ -132,7 +100,8 @@ PyObject* PyInit_mcrfpy() // Add default_font and default_texture to module McRFPy_API::default_font = std::make_shared("assets/JetbrainsMono.ttf"); McRFPy_API::default_texture = std::make_shared("assets/kenney_tinydungeon.png", 16, 16); - // These will be set later when the window is created + //PyModule_AddObject(m, "default_font", McRFPy_API::default_font->pyObject()); + //PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject()); PyModule_AddObject(m, "default_font", Py_None); PyModule_AddObject(m, "default_texture", Py_None); @@ -168,11 +137,6 @@ PyStatus init_python(const char *program_name) PyConfig config; PyConfig_InitIsolatedConfig(&config); config.dev_mode = 0; - - // Configure UTF-8 for stdio - PyConfig_SetString(&config, &config.stdio_encoding, L"UTF-8"); - PyConfig_SetString(&config, &config.stdio_errors, L"surrogateescape"); - config.configure_c_stdio = 1; PyConfig_SetBytesString(&config, &config.home, narrow_string(executable_path() + L"/lib/Python").c_str()); @@ -220,11 +184,6 @@ PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, in PyConfig pyconfig; PyConfig_InitIsolatedConfig(&pyconfig); - // Configure UTF-8 for stdio - PyConfig_SetString(&pyconfig, &pyconfig.stdio_encoding, L"UTF-8"); - PyConfig_SetString(&pyconfig, &pyconfig.stdio_errors, L"surrogateescape"); - pyconfig.configure_c_stdio = 1; - // CRITICAL: Pass actual command line arguments to Python status = PyConfig_SetBytesArgv(&pyconfig, argc, argv); if (PyStatus_Exception(status)) { @@ -380,23 +339,6 @@ void McRFPy_API::executeScript(std::string filename) void McRFPy_API::api_shutdown() { - // Clean up audio resources in correct order - if (sfx) { - sfx->stop(); - delete sfx; - sfx = nullptr; - } - if (music) { - music->stop(); - delete music; - music = nullptr; - } - if (soundbuffers) { - soundbuffers->clear(); - delete soundbuffers; - soundbuffers = nullptr; - } - Py_Finalize(); } @@ -431,29 +373,25 @@ PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) { const char *fn_cstr; if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL; - // Initialize soundbuffers if needed - if (!McRFPy_API::soundbuffers) { - McRFPy_API::soundbuffers = new std::vector(); - } auto b = sf::SoundBuffer(); b.loadFromFile(fn_cstr); - McRFPy_API::soundbuffers->push_back(b); + McRFPy_API::soundbuffers.push_back(b); Py_INCREF(Py_None); return Py_None; } PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) { const char *fn_cstr; - PyObject* loop_obj = Py_False; + PyObject* loop_obj; if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL; - // Initialize music if needed - if (!McRFPy_API::music) { - McRFPy_API::music = new sf::Music(); - } - McRFPy_API::music->stop(); - McRFPy_API::music->openFromFile(fn_cstr); - McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj)); - McRFPy_API::music->play(); + McRFPy_API::music.stop(); + // get params for sf::Music initialization + //sf::InputSoundFile file; + //file.openFromFile(fn_cstr); + McRFPy_API::music.openFromFile(fn_cstr); + McRFPy_API::music.setLoop(PyObject_IsTrue(loop_obj)); + //McRFPy_API::music.initialize(file.getChannelCount(), file.getSampleRate()); + McRFPy_API::music.play(); Py_INCREF(Py_None); return Py_None; } @@ -461,10 +399,7 @@ PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) { int vol; if (!PyArg_ParseTuple(args, "i", &vol)) return NULL; - if (!McRFPy_API::music) { - McRFPy_API::music = new sf::Music(); - } - McRFPy_API::music->setVolume(vol); + McRFPy_API::music.setVolume(vol); Py_INCREF(Py_None); return Py_None; } @@ -472,10 +407,7 @@ PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) { float vol; if (!PyArg_ParseTuple(args, "f", &vol)) return NULL; - if (!McRFPy_API::sfx) { - McRFPy_API::sfx = new sf::Sound(); - } - McRFPy_API::sfx->setVolume(vol); + McRFPy_API::sfx.setVolume(vol); Py_INCREF(Py_None); return Py_None; } @@ -483,29 +415,20 @@ PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) { float index; if (!PyArg_ParseTuple(args, "f", &index)) return NULL; - if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL; - if (!McRFPy_API::sfx) { - McRFPy_API::sfx = new sf::Sound(); - } - McRFPy_API::sfx->stop(); - McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]); - McRFPy_API::sfx->play(); + if (index >= McRFPy_API::soundbuffers.size()) return NULL; + McRFPy_API::sfx.stop(); + McRFPy_API::sfx.setBuffer(McRFPy_API::soundbuffers[index]); + McRFPy_API::sfx.play(); Py_INCREF(Py_None); return Py_None; } PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) { - if (!McRFPy_API::music) { - return Py_BuildValue("f", 0.0f); - } - return Py_BuildValue("f", McRFPy_API::music->getVolume()); + return Py_BuildValue("f", McRFPy_API::music.getVolume()); } PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) { - if (!McRFPy_API::sfx) { - return Py_BuildValue("f", 0.0f); - } - return Py_BuildValue("f", McRFPy_API::sfx->getVolume()); + return Py_BuildValue("f", McRFPy_API::sfx.getVolume()); } // Removed deprecated player_input, computerTurn, playerTurn functions @@ -558,24 +481,8 @@ PyObject* McRFPy_API::_currentScene(PyObject* self, PyObject* args) { PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) { const char* newscene; - const char* transition_str = nullptr; - float duration = 0.0f; - - // Parse arguments: scene name, optional transition type, optional duration - if (!PyArg_ParseTuple(args, "s|sf", &newscene, &transition_str, &duration)) return NULL; - - // Map transition string to enum - TransitionType transition_type = TransitionType::None; - if (transition_str) { - std::string trans(transition_str); - if (trans == "fade") transition_type = TransitionType::Fade; - else if (trans == "slide_left") transition_type = TransitionType::SlideLeft; - else if (trans == "slide_right") transition_type = TransitionType::SlideRight; - else if (trans == "slide_up") transition_type = TransitionType::SlideUp; - else if (trans == "slide_down") transition_type = TransitionType::SlideDown; - } - - game->changeScene(newscene, transition_type, duration); + if (!PyArg_ParseTuple(args, "s", &newscene)) return NULL; + game->changeScene(newscene); Py_INCREF(Py_None); return Py_None; } @@ -660,283 +567,3 @@ void McRFPy_API::markSceneNeedsSort() { } } } - -// Helper function to check if a name matches a pattern with wildcards -static bool name_matches_pattern(const std::string& name, const std::string& pattern) { - if (pattern.find('*') == std::string::npos) { - // No wildcards, exact match - return name == pattern; - } - - // Simple wildcard matching - * matches any sequence - size_t name_pos = 0; - size_t pattern_pos = 0; - - while (pattern_pos < pattern.length() && name_pos < name.length()) { - if (pattern[pattern_pos] == '*') { - // Skip consecutive stars - while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') { - pattern_pos++; - } - if (pattern_pos == pattern.length()) { - // Pattern ends with *, matches rest of name - return true; - } - - // Find next non-star character in pattern - char next_char = pattern[pattern_pos]; - while (name_pos < name.length() && name[name_pos] != next_char) { - name_pos++; - } - } else if (pattern[pattern_pos] == name[name_pos]) { - pattern_pos++; - name_pos++; - } else { - return false; - } - } - - // Skip trailing stars in pattern - while (pattern_pos < pattern.length() && pattern[pattern_pos] == '*') { - pattern_pos++; - } - - return pattern_pos == pattern.length() && name_pos == name.length(); -} - -// Helper to recursively search a collection for named elements -static void find_in_collection(std::vector>* collection, const std::string& pattern, - bool find_all, PyObject* results) { - if (!collection) return; - - for (auto& drawable : *collection) { - if (!drawable) continue; - - // Check this element's name - if (name_matches_pattern(drawable->name, pattern)) { - // Convert to Python object using RET_PY_INSTANCE logic - PyObject* py_obj = nullptr; - - switch (drawable->derived_type()) { - case PyObjectsEnum::UIFRAME: { - auto frame = std::static_pointer_cast(drawable); - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); - auto o = (PyUIFrameObject*)type->tp_alloc(type, 0); - if (o) { - o->data = frame; - py_obj = (PyObject*)o; - } - break; - } - case PyObjectsEnum::UICAPTION: { - auto caption = std::static_pointer_cast(drawable); - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"); - auto o = (PyUICaptionObject*)type->tp_alloc(type, 0); - if (o) { - o->data = caption; - py_obj = (PyObject*)o; - } - break; - } - case PyObjectsEnum::UISPRITE: { - auto sprite = std::static_pointer_cast(drawable); - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"); - auto o = (PyUISpriteObject*)type->tp_alloc(type, 0); - if (o) { - o->data = sprite; - py_obj = (PyObject*)o; - } - break; - } - case PyObjectsEnum::UIGRID: { - auto grid = std::static_pointer_cast(drawable); - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); - auto o = (PyUIGridObject*)type->tp_alloc(type, 0); - if (o) { - o->data = grid; - py_obj = (PyObject*)o; - } - break; - } - default: - break; - } - - if (py_obj) { - if (find_all) { - PyList_Append(results, py_obj); - Py_DECREF(py_obj); - } else { - // For find (not findAll), we store in results and return early - PyList_Append(results, py_obj); - Py_DECREF(py_obj); - return; - } - } - } - - // Recursively search in Frame children - if (drawable->derived_type() == PyObjectsEnum::UIFRAME) { - auto frame = std::static_pointer_cast(drawable); - find_in_collection(frame->children.get(), pattern, find_all, results); - if (!find_all && PyList_Size(results) > 0) { - return; // Found one, stop searching - } - } - } -} - -// Also search Grid entities -static void find_in_grid_entities(UIGrid* grid, const std::string& pattern, - bool find_all, PyObject* results) { - if (!grid || !grid->entities) return; - - for (auto& entity : *grid->entities) { - if (!entity) continue; - - // Entities delegate name to their sprite - if (name_matches_pattern(entity->sprite.name, pattern)) { - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); - auto o = (PyUIEntityObject*)type->tp_alloc(type, 0); - if (o) { - o->data = entity; - PyObject* py_obj = (PyObject*)o; - - if (find_all) { - PyList_Append(results, py_obj); - Py_DECREF(py_obj); - } else { - PyList_Append(results, py_obj); - Py_DECREF(py_obj); - return; - } - } - } - } -} - -PyObject* McRFPy_API::_find(PyObject* self, PyObject* args) { - const char* name; - const char* scene_name = nullptr; - - if (!PyArg_ParseTuple(args, "s|s", &name, &scene_name)) { - return NULL; - } - - PyObject* results = PyList_New(0); - - // Get the UI elements to search - std::shared_ptr>> ui_elements; - if (scene_name) { - // Search specific scene - ui_elements = game->scene_ui(scene_name); - if (!ui_elements) { - PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name); - Py_DECREF(results); - return NULL; - } - } else { - // Search current scene - Scene* current = game->currentScene(); - if (!current) { - PyErr_SetString(PyExc_RuntimeError, "No current scene"); - Py_DECREF(results); - return NULL; - } - ui_elements = current->ui_elements; - } - - // Search the scene's UI elements - find_in_collection(ui_elements.get(), name, false, results); - - // Also search all grids in the scene for entities - if (PyList_Size(results) == 0 && ui_elements) { - for (auto& drawable : *ui_elements) { - if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) { - auto grid = std::static_pointer_cast(drawable); - find_in_grid_entities(grid.get(), name, false, results); - if (PyList_Size(results) > 0) break; - } - } - } - - // Return the first result or None - if (PyList_Size(results) > 0) { - PyObject* result = PyList_GetItem(results, 0); - Py_INCREF(result); - Py_DECREF(results); - return result; - } - - Py_DECREF(results); - Py_RETURN_NONE; -} - -PyObject* McRFPy_API::_findAll(PyObject* self, PyObject* args) { - const char* pattern; - const char* scene_name = nullptr; - - if (!PyArg_ParseTuple(args, "s|s", &pattern, &scene_name)) { - return NULL; - } - - PyObject* results = PyList_New(0); - - // Get the UI elements to search - std::shared_ptr>> ui_elements; - if (scene_name) { - // Search specific scene - ui_elements = game->scene_ui(scene_name); - if (!ui_elements) { - PyErr_Format(PyExc_ValueError, "Scene '%s' not found", scene_name); - Py_DECREF(results); - return NULL; - } - } else { - // Search current scene - Scene* current = game->currentScene(); - if (!current) { - PyErr_SetString(PyExc_RuntimeError, "No current scene"); - Py_DECREF(results); - return NULL; - } - ui_elements = current->ui_elements; - } - - // Search the scene's UI elements - find_in_collection(ui_elements.get(), pattern, true, results); - - // Also search all grids in the scene for entities - if (ui_elements) { - for (auto& drawable : *ui_elements) { - if (drawable && drawable->derived_type() == PyObjectsEnum::UIGRID) { - auto grid = std::static_pointer_cast(drawable); - find_in_grid_entities(grid.get(), pattern, true, results); - } - } - } - - return results; -} - -PyObject* McRFPy_API::_getMetrics(PyObject* self, PyObject* args) { - // Create a dictionary with metrics - PyObject* dict = PyDict_New(); - if (!dict) return NULL; - - // Add frame time metrics - PyDict_SetItemString(dict, "frame_time", PyFloat_FromDouble(game->metrics.frameTime)); - PyDict_SetItemString(dict, "avg_frame_time", PyFloat_FromDouble(game->metrics.avgFrameTime)); - PyDict_SetItemString(dict, "fps", PyLong_FromLong(game->metrics.fps)); - - // Add draw call metrics - PyDict_SetItemString(dict, "draw_calls", PyLong_FromLong(game->metrics.drawCalls)); - PyDict_SetItemString(dict, "ui_elements", PyLong_FromLong(game->metrics.uiElements)); - PyDict_SetItemString(dict, "visible_elements", PyLong_FromLong(game->metrics.visibleElements)); - - // Add general metrics - PyDict_SetItemString(dict, "current_frame", PyLong_FromLong(game->getFrame())); - PyDict_SetItemString(dict, "runtime", PyFloat_FromDouble(game->runtime.getElapsedTime().asSeconds())); - - return dict; -} diff --git a/src/McRFPy_API.h b/src/McRFPy_API.h index 6b32dcf..4d717df 100644 --- a/src/McRFPy_API.h +++ b/src/McRFPy_API.h @@ -36,9 +36,9 @@ public: static void REPL_device(FILE * fp, const char *filename); static void REPL(); - static std::vector* soundbuffers; - static sf::Music* music; - static sf::Sound* sfx; + static std::vector soundbuffers; + static sf::Music music; + static sf::Sound sfx; static PyObject* _createSoundBuffer(PyObject*, PyObject*); @@ -73,16 +73,4 @@ public: // Helper to mark scenes as needing z_index resort static void markSceneNeedsSort(); - - // Name-based finding methods - static PyObject* _find(PyObject*, PyObject*); - static PyObject* _findAll(PyObject*, PyObject*); - - // Profiling/metrics - static PyObject* _getMetrics(PyObject*, PyObject*); - - // Scene lifecycle management for Python Scene objects - 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); }; diff --git a/src/PyCallable.cpp b/src/PyCallable.cpp index c68275c..6d44501 100644 --- a/src/PyCallable.cpp +++ b/src/PyCallable.cpp @@ -16,24 +16,21 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs) return PyObject_Call(target, args, kwargs); } -bool PyCallable::isNone() const +bool PyCallable::isNone() { return (target == Py_None || target == NULL); } PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now) -: PyCallable(_target), interval(_interval), last_ran(now), - paused(false), pause_start_time(0), total_paused_time(0) +: PyCallable(_target), interval(_interval), last_ran(now) {} PyTimerCallable::PyTimerCallable() -: PyCallable(Py_None), interval(0), last_ran(0), - paused(false), pause_start_time(0), total_paused_time(0) +: PyCallable(Py_None), interval(0), last_ran(0) {} bool PyTimerCallable::hasElapsed(int now) { - if (paused) return false; return now >= last_ran + interval; } @@ -63,62 +60,6 @@ bool PyTimerCallable::test(int now) return false; } -void PyTimerCallable::pause(int current_time) -{ - if (!paused) { - paused = true; - pause_start_time = current_time; - } -} - -void PyTimerCallable::resume(int current_time) -{ - if (paused) { - paused = false; - int paused_duration = current_time - pause_start_time; - total_paused_time += paused_duration; - // Adjust last_ran to account for the pause - last_ran += paused_duration; - } -} - -void PyTimerCallable::restart(int current_time) -{ - last_ran = current_time; - paused = false; - pause_start_time = 0; - total_paused_time = 0; -} - -void PyTimerCallable::cancel() -{ - // Cancel by setting target to None - if (target && target != Py_None) { - Py_DECREF(target); - } - target = Py_None; - Py_INCREF(Py_None); -} - -int PyTimerCallable::getRemaining(int current_time) const -{ - if (paused) { - // When paused, calculate time remaining from when it was paused - int elapsed_when_paused = pause_start_time - last_ran; - return interval - elapsed_when_paused; - } - int elapsed = current_time - last_ran; - return interval - elapsed; -} - -void PyTimerCallable::setCallback(PyObject* new_callback) -{ - if (target && target != Py_None) { - Py_DECREF(target); - } - target = Py_XNewRef(new_callback); -} - PyClickCallable::PyClickCallable(PyObject* _target) : PyCallable(_target) {} diff --git a/src/PyCallable.h b/src/PyCallable.h index 6a4c7f6..ae828c7 100644 --- a/src/PyCallable.h +++ b/src/PyCallable.h @@ -10,7 +10,7 @@ protected: ~PyCallable(); PyObject* call(PyObject*, PyObject*); public: - bool isNone() const; + bool isNone(); }; class PyTimerCallable: public PyCallable @@ -19,32 +19,11 @@ private: int interval; int last_ran; void call(int); - - // Pause/resume support - bool paused; - int pause_start_time; - int total_paused_time; - public: bool hasElapsed(int); bool test(int); PyTimerCallable(PyObject*, int, int); PyTimerCallable(); - - // Timer control methods - void pause(int current_time); - void resume(int current_time); - void restart(int current_time); - void cancel(); - - // Timer state queries - bool isPaused() const { return paused; } - bool isActive() const { return !isNone() && !paused; } - int getInterval() const { return interval; } - void setInterval(int new_interval) { interval = new_interval; } - int getRemaining(int current_time) const; - PyObject* getCallback() { return target; } - void setCallback(PyObject* new_callback); }; class PyClickCallable: public PyCallable diff --git a/src/PyColor.cpp b/src/PyColor.cpp index e1a0b1a..8a40d5e 100644 --- a/src/PyColor.cpp +++ b/src/PyColor.cpp @@ -2,8 +2,6 @@ #include "McRFPy_API.h" #include "PyObjectUtils.h" #include "PyRAII.h" -#include -#include PyGetSetDef PyColor::getsetters[] = { {"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0}, @@ -13,13 +11,6 @@ PyGetSetDef PyColor::getsetters[] = { {NULL} }; -PyMethodDef PyColor::methods[] = { - {"from_hex", (PyCFunction)PyColor::from_hex, METH_VARARGS | METH_CLASS, "Create Color from hex string (e.g., '#FF0000' or 'FF0000')"}, - {"to_hex", (PyCFunction)PyColor::to_hex, METH_NOARGS, "Convert Color to hex string"}, - {"lerp", (PyCFunction)PyColor::lerp, METH_VARARGS, "Linearly interpolate between this color and another"}, - {NULL} -}; - PyColor::PyColor(sf::Color target) :data(target) {} @@ -226,105 +217,3 @@ PyColorObject* PyColor::from_arg(PyObject* args) // Release ownership and return return (PyColorObject*)obj.release(); } - -// Color helper method implementations -PyObject* PyColor::from_hex(PyObject* cls, PyObject* args) -{ - const char* hex_str; - if (!PyArg_ParseTuple(args, "s", &hex_str)) { - return NULL; - } - - std::string hex(hex_str); - - // Remove # if present - if (hex.length() > 0 && hex[0] == '#') { - hex = hex.substr(1); - } - - // Validate hex string - if (hex.length() != 6 && hex.length() != 8) { - PyErr_SetString(PyExc_ValueError, "Hex string must be 6 or 8 characters (RGB or RGBA)"); - return NULL; - } - - // Parse hex values - try { - unsigned int r = std::stoul(hex.substr(0, 2), nullptr, 16); - unsigned int g = std::stoul(hex.substr(2, 2), nullptr, 16); - unsigned int b = std::stoul(hex.substr(4, 2), nullptr, 16); - unsigned int a = 255; - - if (hex.length() == 8) { - a = std::stoul(hex.substr(6, 2), nullptr, 16); - } - - // Create new Color object - PyTypeObject* type = (PyTypeObject*)cls; - PyColorObject* color = (PyColorObject*)type->tp_alloc(type, 0); - if (color) { - color->data = sf::Color(r, g, b, a); - } - return (PyObject*)color; - - } catch (const std::exception& e) { - PyErr_SetString(PyExc_ValueError, "Invalid hex string"); - return NULL; - } -} - -PyObject* PyColor::to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored)) -{ - char hex[10]; // #RRGGBBAA + null terminator - - // Include alpha only if not fully opaque - if (self->data.a < 255) { - snprintf(hex, sizeof(hex), "#%02X%02X%02X%02X", - self->data.r, self->data.g, self->data.b, self->data.a); - } else { - snprintf(hex, sizeof(hex), "#%02X%02X%02X", - self->data.r, self->data.g, self->data.b); - } - - return PyUnicode_FromString(hex); -} - -PyObject* PyColor::lerp(PyColorObject* self, PyObject* args) -{ - PyObject* other_obj; - float t; - - if (!PyArg_ParseTuple(args, "Of", &other_obj, &t)) { - return NULL; - } - - // Validate other color - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); - if (!PyObject_IsInstance(other_obj, (PyObject*)type)) { - Py_DECREF(type); - PyErr_SetString(PyExc_TypeError, "First argument must be a Color"); - return NULL; - } - - PyColorObject* other = (PyColorObject*)other_obj; - - // Clamp t to [0, 1] - if (t < 0.0f) t = 0.0f; - if (t > 1.0f) t = 1.0f; - - // Perform linear interpolation - sf::Uint8 r = static_cast(self->data.r + (other->data.r - self->data.r) * t); - sf::Uint8 g = static_cast(self->data.g + (other->data.g - self->data.g) * t); - sf::Uint8 b = static_cast(self->data.b + (other->data.b - self->data.b) * t); - sf::Uint8 a = static_cast(self->data.a + (other->data.a - self->data.a) * t); - - // Create new Color object - PyColorObject* result = (PyColorObject*)type->tp_alloc(type, 0); - Py_DECREF(type); - - if (result) { - result->data = sf::Color(r, g, b, a); - } - - return (PyObject*)result; -} diff --git a/src/PyColor.h b/src/PyColor.h index c5cb2fb..e666154 100644 --- a/src/PyColor.h +++ b/src/PyColor.h @@ -28,13 +28,7 @@ public: static PyObject* get_member(PyObject*, void*); static int set_member(PyObject*, PyObject*, void*); - // Color helper methods - static PyObject* from_hex(PyObject* cls, PyObject* args); - static PyObject* to_hex(PyColorObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* lerp(PyColorObject* self, PyObject* args); - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; static PyColorObject* from_arg(PyObject*); }; @@ -48,7 +42,6 @@ namespace mcrfpydef { .tp_hash = PyColor::hash, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("SFML Color Object"), - .tp_methods = PyColor::methods, .tp_getset = PyColor::getsetters, .tp_init = (initproc)PyColor::init, .tp_new = PyColor::pynew, diff --git a/src/PyDrawable.cpp b/src/PyDrawable.cpp deleted file mode 100644 index 9648335..0000000 --- a/src/PyDrawable.cpp +++ /dev/null @@ -1,179 +0,0 @@ -#include "PyDrawable.h" -#include "McRFPy_API.h" - -// Click property getter -static PyObject* PyDrawable_get_click(PyDrawableObject* self, void* closure) -{ - if (!self->data->click_callable) - Py_RETURN_NONE; - - PyObject* ptr = self->data->click_callable->borrow(); - if (ptr && ptr != Py_None) - return ptr; - else - Py_RETURN_NONE; -} - -// Click property setter -static int PyDrawable_set_click(PyDrawableObject* self, PyObject* value, void* closure) -{ - if (value == Py_None) { - self->data->click_unregister(); - } else if (PyCallable_Check(value)) { - self->data->click_register(value); - } else { - PyErr_SetString(PyExc_TypeError, "click must be callable or None"); - return -1; - } - return 0; -} - -// Z-index property getter -static PyObject* PyDrawable_get_z_index(PyDrawableObject* self, void* closure) -{ - return PyLong_FromLong(self->data->z_index); -} - -// Z-index property setter -static int PyDrawable_set_z_index(PyDrawableObject* self, PyObject* value, void* closure) -{ - if (!PyLong_Check(value)) { - PyErr_SetString(PyExc_TypeError, "z_index must be an integer"); - return -1; - } - - int val = PyLong_AsLong(value); - self->data->z_index = val; - - // Mark scene as needing resort - self->data->notifyZIndexChanged(); - - return 0; -} - -// Visible property getter (new for #87) -static PyObject* PyDrawable_get_visible(PyDrawableObject* self, void* closure) -{ - return PyBool_FromLong(self->data->visible); -} - -// Visible property setter (new for #87) -static int PyDrawable_set_visible(PyDrawableObject* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); - return -1; - } - - self->data->visible = (value == Py_True); - return 0; -} - -// Opacity property getter (new for #88) -static PyObject* PyDrawable_get_opacity(PyDrawableObject* self, void* closure) -{ - return PyFloat_FromDouble(self->data->opacity); -} - -// Opacity property setter (new for #88) -static int PyDrawable_set_opacity(PyDrawableObject* self, PyObject* value, void* closure) -{ - float val; - if (PyFloat_Check(value)) { - val = PyFloat_AsDouble(value); - } else if (PyLong_Check(value)) { - val = PyLong_AsLong(value); - } else { - PyErr_SetString(PyExc_TypeError, "opacity must be a number"); - return -1; - } - - // Clamp to valid range - if (val < 0.0f) val = 0.0f; - if (val > 1.0f) val = 1.0f; - - self->data->opacity = val; - return 0; -} - -// GetSetDef array for properties -static PyGetSetDef PyDrawable_getsetters[] = { - {"click", (getter)PyDrawable_get_click, (setter)PyDrawable_set_click, - "Callable executed when object is clicked", NULL}, - {"z_index", (getter)PyDrawable_get_z_index, (setter)PyDrawable_set_z_index, - "Z-order for rendering (lower values rendered first)", NULL}, - {"visible", (getter)PyDrawable_get_visible, (setter)PyDrawable_set_visible, - "Whether the object is visible", NULL}, - {"opacity", (getter)PyDrawable_get_opacity, (setter)PyDrawable_set_opacity, - "Opacity level (0.0 = transparent, 1.0 = opaque)", NULL}, - {NULL} // Sentinel -}; - -// get_bounds method implementation (#89) -static PyObject* PyDrawable_get_bounds(PyDrawableObject* self, PyObject* Py_UNUSED(args)) -{ - auto bounds = self->data->get_bounds(); - return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); -} - -// move method implementation (#98) -static PyObject* PyDrawable_move(PyDrawableObject* self, PyObject* args) -{ - float dx, dy; - if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { - return NULL; - } - - self->data->move(dx, dy); - Py_RETURN_NONE; -} - -// resize method implementation (#98) -static PyObject* PyDrawable_resize(PyDrawableObject* self, PyObject* args) -{ - float w, h; - if (!PyArg_ParseTuple(args, "ff", &w, &h)) { - return NULL; - } - - self->data->resize(w, h); - Py_RETURN_NONE; -} - -// Method definitions -static PyMethodDef PyDrawable_methods[] = { - {"get_bounds", (PyCFunction)PyDrawable_get_bounds, METH_NOARGS, - "Get bounding box as (x, y, width, height)"}, - {"move", (PyCFunction)PyDrawable_move, METH_VARARGS, - "Move by relative offset (dx, dy)"}, - {"resize", (PyCFunction)PyDrawable_resize, METH_VARARGS, - "Resize to new dimensions (width, height)"}, - {NULL} // Sentinel -}; - -// Type initialization -static int PyDrawable_init(PyDrawableObject* self, PyObject* args, PyObject* kwds) -{ - PyErr_SetString(PyExc_TypeError, "_Drawable is an abstract base class and cannot be instantiated directly"); - return -1; -} - -namespace mcrfpydef { - PyTypeObject PyDrawableType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy._Drawable", - .tp_basicsize = sizeof(PyDrawableObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)[](PyObject* self) { - PyDrawableObject* obj = (PyDrawableObject*)self; - obj->data.reset(); - Py_TYPE(self)->tp_free(self); - }, - .tp_flags = Py_TPFLAGS_DEFAULT, // | Py_TPFLAGS_BASETYPE, - .tp_doc = PyDoc_STR("Base class for all drawable UI elements"), - .tp_methods = PyDrawable_methods, - .tp_getset = PyDrawable_getsetters, - .tp_init = (initproc)PyDrawable_init, - .tp_new = PyType_GenericNew, - }; -} \ No newline at end of file diff --git a/src/PyDrawable.h b/src/PyDrawable.h deleted file mode 100644 index 7837a38..0000000 --- a/src/PyDrawable.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" -#include "UIDrawable.h" - -// Python object structure for UIDrawable base class -typedef struct { - PyObject_HEAD - std::shared_ptr data; -} PyDrawableObject; - -// Declare the Python type for _Drawable base class -namespace mcrfpydef { - extern PyTypeObject PyDrawableType; -} \ No newline at end of file diff --git a/src/PyPositionHelper.h b/src/PyPositionHelper.h deleted file mode 100644 index 1f46820..0000000 --- a/src/PyPositionHelper.h +++ /dev/null @@ -1,164 +0,0 @@ -#pragma once -#include "Python.h" -#include "PyVector.h" -#include "McRFPy_API.h" - -// Helper class for standardized position argument parsing across UI classes -class PyPositionHelper { -public: - // Template structure for parsing results - struct ParseResult { - float x = 0.0f; - float y = 0.0f; - bool has_position = false; - }; - - struct ParseResultInt { - int x = 0; - int y = 0; - bool has_position = false; - }; - - // Parse position from multiple formats for UI class constructors - // Supports: (x, y), x=x, y=y, ((x,y)), (pos=(x,y)), (Vector), pos=Vector - static ParseResult parse_position(PyObject* args, PyObject* kwds, - int* arg_index = nullptr) - { - ParseResult result; - float x = 0.0f, y = 0.0f; - PyObject* pos_obj = nullptr; - int start_index = arg_index ? *arg_index : 0; - - // Check for positional tuple (x, y) first - if (!kwds && PyTuple_Size(args) > start_index + 1) { - PyObject* first = PyTuple_GetItem(args, start_index); - PyObject* second = PyTuple_GetItem(args, start_index + 1); - - // Check if both are numbers - if ((PyFloat_Check(first) || PyLong_Check(first)) && - (PyFloat_Check(second) || PyLong_Check(second))) { - x = PyFloat_Check(first) ? PyFloat_AsDouble(first) : PyLong_AsLong(first); - y = PyFloat_Check(second) ? PyFloat_AsDouble(second) : PyLong_AsLong(second); - result.x = x; - result.y = y; - result.has_position = true; - if (arg_index) *arg_index += 2; - return result; - } - } - - // Check for single positional argument that might be tuple or Vector - if (!kwds && PyTuple_Size(args) > start_index) { - PyObject* first = PyTuple_GetItem(args, start_index); - PyVectorObject* vec = PyVector::from_arg(first); - if (vec) { - result.x = vec->data.x; - result.y = vec->data.y; - result.has_position = true; - if (arg_index) *arg_index += 1; - return result; - } - } - - // Try keyword arguments - if (kwds) { - PyObject* x_obj = PyDict_GetItemString(kwds, "x"); - PyObject* y_obj = PyDict_GetItemString(kwds, "y"); - PyObject* pos_kw = PyDict_GetItemString(kwds, "pos"); - - if (x_obj && y_obj) { - if ((PyFloat_Check(x_obj) || PyLong_Check(x_obj)) && - (PyFloat_Check(y_obj) || PyLong_Check(y_obj))) { - result.x = PyFloat_Check(x_obj) ? PyFloat_AsDouble(x_obj) : PyLong_AsLong(x_obj); - result.y = PyFloat_Check(y_obj) ? PyFloat_AsDouble(y_obj) : PyLong_AsLong(y_obj); - result.has_position = true; - return result; - } - } - - if (pos_kw) { - PyVectorObject* vec = PyVector::from_arg(pos_kw); - if (vec) { - result.x = vec->data.x; - result.y = vec->data.y; - result.has_position = true; - return result; - } - } - } - - return result; - } - - // Parse integer position for Grid.at() and similar - static ParseResultInt parse_position_int(PyObject* args, PyObject* kwds) - { - ParseResultInt result; - - // Check for positional tuple (x, y) first - if (!kwds && PyTuple_Size(args) >= 2) { - PyObject* first = PyTuple_GetItem(args, 0); - PyObject* second = PyTuple_GetItem(args, 1); - - if (PyLong_Check(first) && PyLong_Check(second)) { - result.x = PyLong_AsLong(first); - result.y = PyLong_AsLong(second); - result.has_position = true; - return result; - } - } - - // Check for single tuple argument - if (!kwds && PyTuple_Size(args) == 1) { - PyObject* first = PyTuple_GetItem(args, 0); - if (PyTuple_Check(first) && PyTuple_Size(first) == 2) { - PyObject* x_obj = PyTuple_GetItem(first, 0); - PyObject* y_obj = PyTuple_GetItem(first, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - result.x = PyLong_AsLong(x_obj); - result.y = PyLong_AsLong(y_obj); - result.has_position = true; - return result; - } - } - } - - // Try keyword arguments - if (kwds) { - PyObject* x_obj = PyDict_GetItemString(kwds, "x"); - PyObject* y_obj = PyDict_GetItemString(kwds, "y"); - PyObject* pos_obj = PyDict_GetItemString(kwds, "pos"); - - if (x_obj && y_obj && PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - result.x = PyLong_AsLong(x_obj); - result.y = PyLong_AsLong(y_obj); - result.has_position = true; - return result; - } - - if (pos_obj && PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) { - PyObject* x_val = PyTuple_GetItem(pos_obj, 0); - PyObject* y_val = PyTuple_GetItem(pos_obj, 1); - if (PyLong_Check(x_val) && PyLong_Check(y_val)) { - result.x = PyLong_AsLong(x_val); - result.y = PyLong_AsLong(y_val); - result.has_position = true; - return result; - } - } - } - - return result; - } - - // Error message helper - static void set_position_error() { - PyErr_SetString(PyExc_TypeError, - "Position can be specified as: (x, y), x=x, y=y, ((x,y)), pos=(x,y), or pos=Vector"); - } - - static void set_position_int_error() { - PyErr_SetString(PyExc_TypeError, - "Position must be specified as: (x, y), x=x, y=y, ((x,y)), or pos=(x,y) with integer values"); - } -}; \ No newline at end of file diff --git a/src/PyScene.cpp b/src/PyScene.cpp index 0c4919d..c5ae5d6 100644 --- a/src/PyScene.cpp +++ b/src/PyScene.cpp @@ -29,19 +29,26 @@ void PyScene::do_mouse_input(std::string button, std::string type) auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow()); auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos); - - // Create a sorted copy by z-index (highest first) - std::vector> sorted_elements(*ui_elements); - std::sort(sorted_elements.begin(), sorted_elements.end(), - [](const auto& a, const auto& b) { return a->z_index > b->z_index; }); - - // Check elements in z-order (top to bottom) - for (const auto& element : sorted_elements) { - if (!element->visible) continue; - - if (auto target = element->click_at(sf::Vector2f(mousepos))) { + UIDrawable* target; + for (auto d: *ui_elements) + { + target = d->click_at(sf::Vector2f(mousepos)); + if (target) + { + /* + PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), type.c_str()); + PyObject* retval = PyObject_Call(target->click_callable, args, NULL); + if (!retval) + { + std::cout << "click_callable has raised an exception. It's going to STDERR and being dropped:" << std::endl; + PyErr_Print(); + PyErr_Clear(); + } else if (retval != Py_None) + { + std::cout << "click_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl; + } + */ target->click_callable->call(mousepos, button, type); - return; // Stop after first handler } } } @@ -72,16 +79,8 @@ void PyScene::render() // Render in sorted order (no need to copy anymore) for (auto e: *ui_elements) { - if (e) { - // Track metrics - game->metrics.uiElements++; - if (e->visible) { - game->metrics.visibleElements++; - // Count this as a draw call (each visible element = 1+ draw calls) - game->metrics.drawCalls++; - } + if (e) e->render(); - } } // Display is handled by GameEngine diff --git a/src/PySceneObject.cpp b/src/PySceneObject.cpp deleted file mode 100644 index 491024e..0000000 --- a/src/PySceneObject.cpp +++ /dev/null @@ -1,268 +0,0 @@ -#include "PySceneObject.h" -#include "PyScene.h" -#include "GameEngine.h" -#include "McRFPy_API.h" -#include - -// Static map to store Python scene objects by name -static std::map python_scenes; - -PyObject* PySceneClass::__new__(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PySceneObject* self = (PySceneObject*)type->tp_alloc(type, 0); - if (self) { - self->initialized = false; - // Don't create C++ scene yet - wait for __init__ - } - return (PyObject*)self; -} - -int PySceneClass::__init__(PySceneObject* self, PyObject* args, PyObject* kwds) -{ - static const char* keywords[] = {"name", nullptr}; - const char* name = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast(keywords), &name)) { - return -1; - } - - // Check if scene with this name already exists - if (python_scenes.count(name) > 0) { - PyErr_Format(PyExc_ValueError, "Scene with name '%s' already exists", name); - return -1; - } - - self->name = name; - - // Create the C++ PyScene - McRFPy_API::game->createScene(name); - - // Get reference to the created scene - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - // Store this Python object in our registry - python_scenes[name] = self; - Py_INCREF(self); // Keep a reference - - // Create a Python function that routes to on_keypress - // We'll register this after the object is fully initialized - - self->initialized = true; - - return 0; -} - -void PySceneClass::__dealloc(PyObject* self_obj) -{ - PySceneObject* self = (PySceneObject*)self_obj; - - // Remove from registry - if (python_scenes.count(self->name) > 0 && python_scenes[self->name] == self) { - python_scenes.erase(self->name); - } - - // Call Python object destructor - Py_TYPE(self)->tp_free(self); -} - -PyObject* PySceneClass::__repr__(PySceneObject* self) -{ - return PyUnicode_FromFormat("", self->name.c_str()); -} - -PyObject* PySceneClass::activate(PySceneObject* self, PyObject* args) -{ - // Call the static method from McRFPy_API - PyObject* py_args = Py_BuildValue("(s)", self->name.c_str()); - PyObject* result = McRFPy_API::_setScene(NULL, py_args); - Py_DECREF(py_args); - return result; -} - -PyObject* PySceneClass::get_ui(PySceneObject* self, PyObject* args) -{ - // Call the static method from McRFPy_API - PyObject* py_args = Py_BuildValue("(s)", self->name.c_str()); - PyObject* result = McRFPy_API::_sceneUI(NULL, py_args); - Py_DECREF(py_args); - return result; -} - -PyObject* PySceneClass::register_keyboard(PySceneObject* self, PyObject* args) -{ - PyObject* callable; - if (!PyArg_ParseTuple(args, "O", &callable)) { - return NULL; - } - - if (!PyCallable_Check(callable)) { - PyErr_SetString(PyExc_TypeError, "Argument must be callable"); - return NULL; - } - - // Store the callable - Py_INCREF(callable); - - // Get the current scene and set its key_callable - GameEngine* game = McRFPy_API::game; - if (game) { - // We need to be on the right scene first - std::string old_scene = game->scene; - game->scene = self->name; - game->currentScene()->key_callable = std::make_unique(callable); - game->scene = old_scene; - } - - Py_DECREF(callable); - Py_RETURN_NONE; -} - -PyObject* PySceneClass::get_name(PySceneObject* self, void* closure) -{ - return PyUnicode_FromString(self->name.c_str()); -} - -PyObject* PySceneClass::get_active(PySceneObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - Py_RETURN_FALSE; - } - - return PyBool_FromLong(game->scene == self->name); -} - -// Lifecycle callbacks -void PySceneClass::call_on_enter(PySceneObject* self) -{ - PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_enter"); - if (method && PyCallable_Check(method)) { - PyObject* result = PyObject_CallNoArgs(method); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - } - Py_XDECREF(method); -} - -void PySceneClass::call_on_exit(PySceneObject* self) -{ - PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_exit"); - if (method && PyCallable_Check(method)) { - PyObject* result = PyObject_CallNoArgs(method); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - } - Py_XDECREF(method); -} - -void PySceneClass::call_on_keypress(PySceneObject* self, std::string key, 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()); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - } - Py_XDECREF(method); - - PyGILState_Release(gstate); -} - -void PySceneClass::call_update(PySceneObject* self, float dt) -{ - PyObject* method = PyObject_GetAttrString((PyObject*)self, "update"); - if (method && PyCallable_Check(method)) { - PyObject* result = PyObject_CallFunction(method, "f", dt); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - } - Py_XDECREF(method); -} - -void PySceneClass::call_on_resize(PySceneObject* self, int width, int height) -{ - PyObject* method = PyObject_GetAttrString((PyObject*)self, "on_resize"); - if (method && PyCallable_Check(method)) { - PyObject* result = PyObject_CallFunction(method, "ii", width, height); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - } - Py_XDECREF(method); -} - -// Properties -PyGetSetDef PySceneClass::getsetters[] = { - {"name", (getter)get_name, NULL, "Scene name", NULL}, - {"active", (getter)get_active, NULL, "Whether this scene is currently active", NULL}, - {NULL} -}; - -// Methods -PyMethodDef PySceneClass::methods[] = { - {"activate", (PyCFunction)activate, METH_NOARGS, - "Make this the active scene"}, - {"get_ui", (PyCFunction)get_ui, METH_NOARGS, - "Get the UI element collection for this scene"}, - {"register_keyboard", (PyCFunction)register_keyboard, METH_VARARGS, - "Register a keyboard handler function (alternative to overriding on_keypress)"}, - {NULL} -}; - -// Helper function to trigger lifecycle events -void McRFPy_API::triggerSceneChange(const std::string& from_scene, const std::string& to_scene) -{ - // Call on_exit for the old scene - if (!from_scene.empty() && python_scenes.count(from_scene) > 0) { - PySceneClass::call_on_exit(python_scenes[from_scene]); - } - - // Call on_enter for the new scene - if (!to_scene.empty() && python_scenes.count(to_scene) > 0) { - PySceneClass::call_on_enter(python_scenes[to_scene]); - } -} - -// Helper function to update Python scenes -void McRFPy_API::updatePythonScenes(float dt) -{ - GameEngine* game = McRFPy_API::game; - if (!game) return; - - // Only update the active scene - if (python_scenes.count(game->scene) > 0) { - PySceneClass::call_update(python_scenes[game->scene], dt); - } -} - -// Helper function to trigger resize events on Python scenes -void McRFPy_API::triggerResize(int width, int height) -{ - GameEngine* game = McRFPy_API::game; - if (!game) return; - - // Only notify the active scene - if (python_scenes.count(game->scene) > 0) { - PySceneClass::call_on_resize(python_scenes[game->scene], width, height); - } -} \ No newline at end of file diff --git a/src/PySceneObject.h b/src/PySceneObject.h deleted file mode 100644 index b504e5e..0000000 --- a/src/PySceneObject.h +++ /dev/null @@ -1,63 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" -#include -#include - -// Forward declarations -class PyScene; - -// Python object structure for Scene -typedef struct { - PyObject_HEAD - std::string name; - std::shared_ptr scene; // Reference to the C++ scene - bool initialized; -} PySceneObject; - -// C++ interface for Python Scene class -class PySceneClass -{ -public: - // Type methods - static PyObject* __new__(PyTypeObject* type, PyObject* args, PyObject* kwds); - static int __init__(PySceneObject* self, PyObject* args, PyObject* kwds); - static void __dealloc(PyObject* self); - static PyObject* __repr__(PySceneObject* self); - - // Scene methods - static PyObject* activate(PySceneObject* self, PyObject* args); - static PyObject* get_ui(PySceneObject* self, PyObject* args); - static PyObject* register_keyboard(PySceneObject* self, PyObject* args); - - // Properties - static PyObject* get_name(PySceneObject* self, void* closure); - static PyObject* get_active(PySceneObject* self, void* closure); - - // 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_update(PySceneObject* self, float dt); - static void call_on_resize(PySceneObject* self, int width, int height); - - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; -}; - -namespace mcrfpydef { - static PyTypeObject PySceneType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.Scene", - .tp_basicsize = sizeof(PySceneObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)PySceneClass::__dealloc, - .tp_repr = (reprfunc)PySceneClass::__repr__, - .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, // Allow subclassing - .tp_doc = PyDoc_STR("Base class for object-oriented scenes"), - .tp_methods = nullptr, // Set in McRFPy_API.cpp - .tp_getset = nullptr, // Set in McRFPy_API.cpp - .tp_init = (initproc)PySceneClass::__init__, - .tp_new = PySceneClass::__new__, - }; -} \ No newline at end of file diff --git a/src/PyTimer.cpp b/src/PyTimer.cpp deleted file mode 100644 index 7f780a3..0000000 --- a/src/PyTimer.cpp +++ /dev/null @@ -1,271 +0,0 @@ -#include "PyTimer.h" -#include "PyCallable.h" -#include "GameEngine.h" -#include "Resources.h" -#include - -PyObject* PyTimer::repr(PyObject* self) { - PyTimerObject* timer = (PyTimerObject*)self; - std::ostringstream oss; - oss << "data) { - oss << "interval=" << timer->data->getInterval() << "ms "; - oss << (timer->data->isPaused() ? "paused" : "active"); - } else { - oss << "uninitialized"; - } - oss << ">"; - - return PyUnicode_FromString(oss.str().c_str()); -} - -PyObject* PyTimer::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { - PyTimerObject* self = (PyTimerObject*)type->tp_alloc(type, 0); - if (self) { - new(&self->name) std::string(); // Placement new for std::string - self->data = nullptr; - } - return (PyObject*)self; -} - -int PyTimer::init(PyTimerObject* self, PyObject* args, PyObject* kwds) { - static char* kwlist[] = {"name", "callback", "interval", NULL}; - const char* name = nullptr; - PyObject* callback = nullptr; - int interval = 0; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOi", kwlist, - &name, &callback, &interval)) { - return -1; - } - - if (!PyCallable_Check(callback)) { - PyErr_SetString(PyExc_TypeError, "callback must be callable"); - return -1; - } - - if (interval <= 0) { - PyErr_SetString(PyExc_ValueError, "interval must be positive"); - return -1; - } - - self->name = name; - - // Get current time from game engine - int current_time = 0; - if (Resources::game) { - current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); - } - - // Create the timer callable - self->data = std::make_shared(callback, interval, current_time); - - // Register with game engine - if (Resources::game) { - Resources::game->timers[self->name] = self->data; - } - - return 0; -} - -void PyTimer::dealloc(PyTimerObject* self) { - // Remove from game engine if still registered - if (Resources::game && !self->name.empty()) { - auto it = Resources::game->timers.find(self->name); - if (it != Resources::game->timers.end() && it->second == self->data) { - Resources::game->timers.erase(it); - } - } - - // Explicitly destroy std::string - self->name.~basic_string(); - - // Clear shared_ptr - self->data.reset(); - - Py_TYPE(self)->tp_free((PyObject*)self); -} - -// Timer control methods -PyObject* PyTimer::pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - int current_time = 0; - if (Resources::game) { - current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); - } - - self->data->pause(current_time); - Py_RETURN_NONE; -} - -PyObject* PyTimer::resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - int current_time = 0; - if (Resources::game) { - current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); - } - - self->data->resume(current_time); - Py_RETURN_NONE; -} - -PyObject* PyTimer::cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - // Remove from game engine - if (Resources::game && !self->name.empty()) { - auto it = Resources::game->timers.find(self->name); - if (it != Resources::game->timers.end() && it->second == self->data) { - Resources::game->timers.erase(it); - } - } - - self->data->cancel(); - self->data.reset(); - Py_RETURN_NONE; -} - -PyObject* PyTimer::restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - int current_time = 0; - if (Resources::game) { - current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); - } - - self->data->restart(current_time); - Py_RETURN_NONE; -} - -// Property getters/setters -PyObject* PyTimer::get_interval(PyTimerObject* self, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - return PyLong_FromLong(self->data->getInterval()); -} - -int PyTimer::set_interval(PyTimerObject* self, PyObject* value, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return -1; - } - - if (!PyLong_Check(value)) { - PyErr_SetString(PyExc_TypeError, "interval must be an integer"); - return -1; - } - - long interval = PyLong_AsLong(value); - if (interval <= 0) { - PyErr_SetString(PyExc_ValueError, "interval must be positive"); - return -1; - } - - self->data->setInterval(interval); - return 0; -} - -PyObject* PyTimer::get_remaining(PyTimerObject* self, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - int current_time = 0; - if (Resources::game) { - current_time = Resources::game->runtime.getElapsedTime().asMilliseconds(); - } - - return PyLong_FromLong(self->data->getRemaining(current_time)); -} - -PyObject* PyTimer::get_paused(PyTimerObject* self, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - return PyBool_FromLong(self->data->isPaused()); -} - -PyObject* PyTimer::get_active(PyTimerObject* self, void* closure) { - if (!self->data) { - return Py_False; - } - - return PyBool_FromLong(self->data->isActive()); -} - -PyObject* PyTimer::get_callback(PyTimerObject* self, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return nullptr; - } - - PyObject* callback = self->data->getCallback(); - if (!callback) { - Py_RETURN_NONE; - } - - Py_INCREF(callback); - return callback; -} - -int PyTimer::set_callback(PyTimerObject* self, PyObject* value, void* closure) { - if (!self->data) { - PyErr_SetString(PyExc_RuntimeError, "Timer not initialized"); - return -1; - } - - if (!PyCallable_Check(value)) { - PyErr_SetString(PyExc_TypeError, "callback must be callable"); - return -1; - } - - self->data->setCallback(value); - return 0; -} - -PyGetSetDef PyTimer::getsetters[] = { - {"interval", (getter)PyTimer::get_interval, (setter)PyTimer::set_interval, - "Timer interval in milliseconds", NULL}, - {"remaining", (getter)PyTimer::get_remaining, NULL, - "Time remaining until next trigger in milliseconds", NULL}, - {"paused", (getter)PyTimer::get_paused, NULL, - "Whether the timer is paused", NULL}, - {"active", (getter)PyTimer::get_active, NULL, - "Whether the timer is active and not paused", NULL}, - {"callback", (getter)PyTimer::get_callback, (setter)PyTimer::set_callback, - "The callback function to be called", NULL}, - {NULL} -}; - -PyMethodDef PyTimer::methods[] = { - {"pause", (PyCFunction)PyTimer::pause, METH_NOARGS, - "Pause the timer"}, - {"resume", (PyCFunction)PyTimer::resume, METH_NOARGS, - "Resume a paused timer"}, - {"cancel", (PyCFunction)PyTimer::cancel, METH_NOARGS, - "Cancel the timer and remove it from the system"}, - {"restart", (PyCFunction)PyTimer::restart, METH_NOARGS, - "Restart the timer from the current time"}, - {NULL} -}; \ No newline at end of file diff --git a/src/PyTimer.h b/src/PyTimer.h deleted file mode 100644 index 16c4deb..0000000 --- a/src/PyTimer.h +++ /dev/null @@ -1,58 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" -#include -#include - -class PyTimerCallable; - -typedef struct { - PyObject_HEAD - std::shared_ptr data; - std::string name; -} PyTimerObject; - -class PyTimer -{ -public: - // Python type methods - static PyObject* repr(PyObject* self); - static int init(PyTimerObject* self, PyObject* args, PyObject* kwds); - static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); - static void dealloc(PyTimerObject* self); - - // Timer control methods - static PyObject* pause(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* resume(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* cancel(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* restart(PyTimerObject* self, PyObject* Py_UNUSED(ignored)); - - // Timer property getters - static PyObject* get_interval(PyTimerObject* self, void* closure); - static int set_interval(PyTimerObject* self, PyObject* value, void* closure); - static PyObject* get_remaining(PyTimerObject* self, void* closure); - static PyObject* get_paused(PyTimerObject* self, void* closure); - static PyObject* get_active(PyTimerObject* self, void* closure); - static PyObject* get_callback(PyTimerObject* self, void* closure); - static int set_callback(PyTimerObject* self, PyObject* value, void* closure); - - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; -}; - -namespace mcrfpydef { - static PyTypeObject PyTimerType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.Timer", - .tp_basicsize = sizeof(PyTimerObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)PyTimer::dealloc, - .tp_repr = PyTimer::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Timer object for scheduled callbacks"), - .tp_methods = PyTimer::methods, - .tp_getset = PyTimer::getsetters, - .tp_init = (initproc)PyTimer::init, - .tp_new = PyTimer::pynew, - }; -} \ No newline at end of file diff --git a/src/PyVector.cpp b/src/PyVector.cpp index 16acd51..83c243e 100644 --- a/src/PyVector.cpp +++ b/src/PyVector.cpp @@ -1,6 +1,5 @@ #include "PyVector.h" #include "PyObjectUtils.h" -#include PyGetSetDef PyVector::getsetters[] = { {"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0}, @@ -8,58 +7,6 @@ PyGetSetDef PyVector::getsetters[] = { {NULL} }; -PyMethodDef PyVector::methods[] = { - {"magnitude", (PyCFunction)PyVector::magnitude, METH_NOARGS, "Return the length of the vector"}, - {"magnitude_squared", (PyCFunction)PyVector::magnitude_squared, METH_NOARGS, "Return the squared length of the vector"}, - {"normalize", (PyCFunction)PyVector::normalize, METH_NOARGS, "Return a unit vector in the same direction"}, - {"dot", (PyCFunction)PyVector::dot, METH_O, "Return the dot product with another vector"}, - {"distance_to", (PyCFunction)PyVector::distance_to, METH_O, "Return the distance to another vector"}, - {"angle", (PyCFunction)PyVector::angle, METH_NOARGS, "Return the angle in radians from the positive X axis"}, - {"copy", (PyCFunction)PyVector::copy, METH_NOARGS, "Return a copy of this vector"}, - {NULL} -}; - -namespace mcrfpydef { - PyNumberMethods PyVector_as_number = { - .nb_add = PyVector::add, - .nb_subtract = PyVector::subtract, - .nb_multiply = PyVector::multiply, - .nb_remainder = 0, - .nb_divmod = 0, - .nb_power = 0, - .nb_negative = PyVector::negative, - .nb_positive = 0, - .nb_absolute = PyVector::absolute, - .nb_bool = PyVector::bool_check, - .nb_invert = 0, - .nb_lshift = 0, - .nb_rshift = 0, - .nb_and = 0, - .nb_xor = 0, - .nb_or = 0, - .nb_int = 0, - .nb_reserved = 0, - .nb_float = 0, - .nb_inplace_add = 0, - .nb_inplace_subtract = 0, - .nb_inplace_multiply = 0, - .nb_inplace_remainder = 0, - .nb_inplace_power = 0, - .nb_inplace_lshift = 0, - .nb_inplace_rshift = 0, - .nb_inplace_and = 0, - .nb_inplace_xor = 0, - .nb_inplace_or = 0, - .nb_floor_divide = 0, - .nb_true_divide = PyVector::divide, - .nb_inplace_floor_divide = 0, - .nb_inplace_true_divide = 0, - .nb_index = 0, - .nb_matrix_multiply = 0, - .nb_inplace_matrix_multiply = 0 - }; -} - PyVector::PyVector(sf::Vector2f target) :data(target) {} @@ -225,241 +172,3 @@ PyVectorObject* PyVector::from_arg(PyObject* args) return obj; } - -// Arithmetic operations -PyObject* PyVector::add(PyObject* left, PyObject* right) -{ - // Check if both operands are vectors - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - PyVectorObject* vec1 = nullptr; - PyVectorObject* vec2 = nullptr; - - if (PyObject_IsInstance(left, (PyObject*)type) && PyObject_IsInstance(right, (PyObject*)type)) { - vec1 = (PyVectorObject*)left; - vec2 = (PyVectorObject*)right; - } else { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - if (result) { - result->data = sf::Vector2f(vec1->data.x + vec2->data.x, vec1->data.y + vec2->data.y); - } - return (PyObject*)result; -} - -PyObject* PyVector::subtract(PyObject* left, PyObject* right) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - PyVectorObject* vec1 = nullptr; - PyVectorObject* vec2 = nullptr; - - if (PyObject_IsInstance(left, (PyObject*)type) && PyObject_IsInstance(right, (PyObject*)type)) { - vec1 = (PyVectorObject*)left; - vec2 = (PyVectorObject*)right; - } else { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - if (result) { - result->data = sf::Vector2f(vec1->data.x - vec2->data.x, vec1->data.y - vec2->data.y); - } - return (PyObject*)result; -} - -PyObject* PyVector::multiply(PyObject* left, PyObject* right) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - PyVectorObject* vec = nullptr; - double scalar = 0.0; - - // Check for Vector * scalar - if (PyObject_IsInstance(left, (PyObject*)type) && (PyFloat_Check(right) || PyLong_Check(right))) { - vec = (PyVectorObject*)left; - scalar = PyFloat_AsDouble(right); - } - // Check for scalar * Vector - else if ((PyFloat_Check(left) || PyLong_Check(left)) && PyObject_IsInstance(right, (PyObject*)type)) { - scalar = PyFloat_AsDouble(left); - vec = (PyVectorObject*)right; - } - else { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - if (result) { - result->data = sf::Vector2f(vec->data.x * scalar, vec->data.y * scalar); - } - return (PyObject*)result; -} - -PyObject* PyVector::divide(PyObject* left, PyObject* right) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - // Only support Vector / scalar - if (!PyObject_IsInstance(left, (PyObject*)type) || (!PyFloat_Check(right) && !PyLong_Check(right))) { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - PyVectorObject* vec = (PyVectorObject*)left; - double scalar = PyFloat_AsDouble(right); - - if (scalar == 0.0) { - PyErr_SetString(PyExc_ZeroDivisionError, "Vector division by zero"); - return NULL; - } - - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - if (result) { - result->data = sf::Vector2f(vec->data.x / scalar, vec->data.y / scalar); - } - return (PyObject*)result; -} - -PyObject* PyVector::negative(PyObject* self) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - PyVectorObject* vec = (PyVectorObject*)self; - - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - if (result) { - result->data = sf::Vector2f(-vec->data.x, -vec->data.y); - } - return (PyObject*)result; -} - -PyObject* PyVector::absolute(PyObject* self) -{ - PyVectorObject* vec = (PyVectorObject*)self; - return PyFloat_FromDouble(std::sqrt(vec->data.x * vec->data.x + vec->data.y * vec->data.y)); -} - -int PyVector::bool_check(PyObject* self) -{ - PyVectorObject* vec = (PyVectorObject*)self; - return (vec->data.x != 0.0f || vec->data.y != 0.0f) ? 1 : 0; -} - -PyObject* PyVector::richcompare(PyObject* left, PyObject* right, int op) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - if (!PyObject_IsInstance(left, (PyObject*)type) || !PyObject_IsInstance(right, (PyObject*)type)) { - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - PyVectorObject* vec1 = (PyVectorObject*)left; - PyVectorObject* vec2 = (PyVectorObject*)right; - - bool result = false; - - switch (op) { - case Py_EQ: - result = (vec1->data.x == vec2->data.x && vec1->data.y == vec2->data.y); - break; - case Py_NE: - result = (vec1->data.x != vec2->data.x || vec1->data.y != vec2->data.y); - break; - default: - Py_INCREF(Py_NotImplemented); - return Py_NotImplemented; - } - - if (result) - Py_RETURN_TRUE; - else - Py_RETURN_FALSE; -} - -// Vector-specific methods -PyObject* PyVector::magnitude(PyVectorObject* self, PyObject* Py_UNUSED(ignored)) -{ - float mag = std::sqrt(self->data.x * self->data.x + self->data.y * self->data.y); - return PyFloat_FromDouble(mag); -} - -PyObject* PyVector::magnitude_squared(PyVectorObject* self, PyObject* Py_UNUSED(ignored)) -{ - float mag_sq = self->data.x * self->data.x + self->data.y * self->data.y; - return PyFloat_FromDouble(mag_sq); -} - -PyObject* PyVector::normalize(PyVectorObject* self, PyObject* Py_UNUSED(ignored)) -{ - float mag = std::sqrt(self->data.x * self->data.x + self->data.y * self->data.y); - - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - - if (result) { - if (mag > 0.0f) { - result->data = sf::Vector2f(self->data.x / mag, self->data.y / mag); - } else { - // Zero vector remains zero - result->data = sf::Vector2f(0.0f, 0.0f); - } - } - - return (PyObject*)result; -} - -PyObject* PyVector::dot(PyVectorObject* self, PyObject* other) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - if (!PyObject_IsInstance(other, (PyObject*)type)) { - PyErr_SetString(PyExc_TypeError, "Argument must be a Vector"); - return NULL; - } - - PyVectorObject* vec2 = (PyVectorObject*)other; - float dot_product = self->data.x * vec2->data.x + self->data.y * vec2->data.y; - - return PyFloat_FromDouble(dot_product); -} - -PyObject* PyVector::distance_to(PyVectorObject* self, PyObject* other) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - - if (!PyObject_IsInstance(other, (PyObject*)type)) { - PyErr_SetString(PyExc_TypeError, "Argument must be a Vector"); - return NULL; - } - - PyVectorObject* vec2 = (PyVectorObject*)other; - float dx = self->data.x - vec2->data.x; - float dy = self->data.y - vec2->data.y; - float distance = std::sqrt(dx * dx + dy * dy); - - return PyFloat_FromDouble(distance); -} - -PyObject* PyVector::angle(PyVectorObject* self, PyObject* Py_UNUSED(ignored)) -{ - float angle_rad = std::atan2(self->data.y, self->data.x); - return PyFloat_FromDouble(angle_rad); -} - -PyObject* PyVector::copy(PyVectorObject* self, PyObject* Py_UNUSED(ignored)) -{ - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"); - auto result = (PyVectorObject*)type->tp_alloc(type, 0); - - if (result) { - result->data = self->data; - } - - return (PyObject*)result; -} diff --git a/src/PyVector.h b/src/PyVector.h index 0b4dc46..a949a5f 100644 --- a/src/PyVector.h +++ b/src/PyVector.h @@ -25,47 +25,19 @@ public: static int set_member(PyObject*, PyObject*, void*); static PyVectorObject* from_arg(PyObject*); - // Arithmetic operations - static PyObject* add(PyObject*, PyObject*); - static PyObject* subtract(PyObject*, PyObject*); - static PyObject* multiply(PyObject*, PyObject*); - static PyObject* divide(PyObject*, PyObject*); - static PyObject* negative(PyObject*); - static PyObject* absolute(PyObject*); - static int bool_check(PyObject*); - - // Comparison operations - static PyObject* richcompare(PyObject*, PyObject*, int); - - // Vector operations - static PyObject* magnitude(PyVectorObject*, PyObject*); - static PyObject* magnitude_squared(PyVectorObject*, PyObject*); - static PyObject* normalize(PyVectorObject*, PyObject*); - static PyObject* dot(PyVectorObject*, PyObject*); - static PyObject* distance_to(PyVectorObject*, PyObject*); - static PyObject* angle(PyVectorObject*, PyObject*); - static PyObject* copy(PyVectorObject*, PyObject*); - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; }; namespace mcrfpydef { - // Forward declare the PyNumberMethods structure - extern PyNumberMethods PyVector_as_number; - static PyTypeObject PyVectorType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, .tp_name = "mcrfpy.Vector", .tp_basicsize = sizeof(PyVectorObject), .tp_itemsize = 0, .tp_repr = PyVector::repr, - .tp_as_number = &PyVector_as_number, .tp_hash = PyVector::hash, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("SFML Vector Object"), - .tp_richcompare = PyVector::richcompare, - .tp_methods = PyVector::methods, .tp_getset = PyVector::getsetters, .tp_init = (initproc)PyVector::init, .tp_new = PyVector::pynew, diff --git a/src/PyWindow.cpp b/src/PyWindow.cpp deleted file mode 100644 index 4500f91..0000000 --- a/src/PyWindow.cpp +++ /dev/null @@ -1,433 +0,0 @@ -#include "PyWindow.h" -#include "GameEngine.h" -#include "McRFPy_API.h" -#include - -// Singleton instance - static variable, not a class member -static PyWindowObject* window_instance = nullptr; - -PyObject* PyWindow::get(PyObject* cls, PyObject* args) -{ - // Create singleton instance if it doesn't exist - if (!window_instance) { - // Use the class object passed as first argument - PyTypeObject* type = (PyTypeObject*)cls; - - if (!type->tp_alloc) { - PyErr_SetString(PyExc_RuntimeError, "Window type not properly initialized"); - return NULL; - } - - window_instance = (PyWindowObject*)type->tp_alloc(type, 0); - if (!window_instance) { - return NULL; - } - } - - Py_INCREF(window_instance); - return (PyObject*)window_instance; -} - -PyObject* PyWindow::repr(PyWindowObject* self) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - return PyUnicode_FromString(""); - } - - if (game->isHeadless()) { - return PyUnicode_FromString(""); - } - - auto& window = game->getWindow(); - auto size = window.getSize(); - - return PyUnicode_FromFormat("", size.x, size.y); -} - -// Property getters and setters - -PyObject* PyWindow::get_resolution(PyWindowObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - if (game->isHeadless()) { - // Return headless renderer size - return Py_BuildValue("(ii)", 1024, 768); // Default headless size - } - - auto& window = game->getWindow(); - auto size = window.getSize(); - return Py_BuildValue("(ii)", size.x, size.y); -} - -int PyWindow::set_resolution(PyWindowObject* self, PyObject* value, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - if (game->isHeadless()) { - PyErr_SetString(PyExc_RuntimeError, "Cannot change resolution in headless mode"); - return -1; - } - - int width, height; - if (!PyArg_ParseTuple(value, "ii", &width, &height)) { - PyErr_SetString(PyExc_TypeError, "Resolution must be a tuple of two integers (width, height)"); - return -1; - } - - if (width <= 0 || height <= 0) { - PyErr_SetString(PyExc_ValueError, "Resolution dimensions must be positive"); - return -1; - } - - auto& window = game->getWindow(); - - // Get current window settings - auto style = sf::Style::Titlebar | sf::Style::Close; - if (window.getSize() == sf::Vector2u(sf::VideoMode::getDesktopMode().width, - sf::VideoMode::getDesktopMode().height)) { - style = sf::Style::Fullscreen; - } - - // Recreate window with new size - window.create(sf::VideoMode(width, height), game->getWindowTitle(), style); - - // Restore vsync and framerate settings - // Note: We'll need to store these settings in GameEngine - window.setFramerateLimit(60); // Default for now - - return 0; -} - -PyObject* PyWindow::get_fullscreen(PyWindowObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - if (game->isHeadless()) { - Py_RETURN_FALSE; - } - - auto& window = game->getWindow(); - auto size = window.getSize(); - auto desktop = sf::VideoMode::getDesktopMode(); - - // Check if window size matches desktop size (rough fullscreen check) - bool fullscreen = (size.x == desktop.width && size.y == desktop.height); - - return PyBool_FromLong(fullscreen); -} - -int PyWindow::set_fullscreen(PyWindowObject* self, PyObject* value, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - if (game->isHeadless()) { - PyErr_SetString(PyExc_RuntimeError, "Cannot change fullscreen in headless mode"); - return -1; - } - - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "Fullscreen must be a boolean"); - return -1; - } - - bool fullscreen = PyObject_IsTrue(value); - auto& window = game->getWindow(); - - if (fullscreen) { - // Switch to fullscreen - auto desktop = sf::VideoMode::getDesktopMode(); - window.create(desktop, game->getWindowTitle(), sf::Style::Fullscreen); - } else { - // Switch to windowed mode - window.create(sf::VideoMode(1024, 768), game->getWindowTitle(), - sf::Style::Titlebar | sf::Style::Close); - } - - // Restore settings - window.setFramerateLimit(60); - - return 0; -} - -PyObject* PyWindow::get_vsync(PyWindowObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - return PyBool_FromLong(game->getVSync()); -} - -int PyWindow::set_vsync(PyWindowObject* self, PyObject* value, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - if (game->isHeadless()) { - PyErr_SetString(PyExc_RuntimeError, "Cannot change vsync in headless mode"); - return -1; - } - - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "vsync must be a boolean"); - return -1; - } - - bool vsync = PyObject_IsTrue(value); - game->setVSync(vsync); - - return 0; -} - -PyObject* PyWindow::get_title(PyWindowObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - return PyUnicode_FromString(game->getWindowTitle().c_str()); -} - -int PyWindow::set_title(PyWindowObject* self, PyObject* value, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - if (game->isHeadless()) { - // Silently ignore in headless mode - return 0; - } - - const char* title = PyUnicode_AsUTF8(value); - if (!title) { - PyErr_SetString(PyExc_TypeError, "Title must be a string"); - return -1; - } - - game->setWindowTitle(title); - - return 0; -} - -PyObject* PyWindow::get_visible(PyWindowObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - if (game->isHeadless()) { - Py_RETURN_FALSE; - } - - auto& window = game->getWindow(); - bool visible = window.isOpen(); // Best approximation - - return PyBool_FromLong(visible); -} - -int PyWindow::set_visible(PyWindowObject* self, PyObject* value, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - if (game->isHeadless()) { - // Silently ignore in headless mode - return 0; - } - - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); - return -1; - } - - bool visible = PyObject_IsTrue(value); - auto& window = game->getWindow(); - window.setVisible(visible); - - return 0; -} - -PyObject* PyWindow::get_framerate_limit(PyWindowObject* self, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - return PyLong_FromLong(game->getFramerateLimit()); -} - -int PyWindow::set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return -1; - } - - if (game->isHeadless()) { - // Silently ignore in headless mode - return 0; - } - - long limit = PyLong_AsLong(value); - if (PyErr_Occurred()) { - PyErr_SetString(PyExc_TypeError, "framerate_limit must be an integer"); - return -1; - } - - if (limit < 0) { - PyErr_SetString(PyExc_ValueError, "framerate_limit must be non-negative (0 for unlimited)"); - return -1; - } - - game->setFramerateLimit(limit); - - return 0; -} - -// Methods - -PyObject* PyWindow::center(PyWindowObject* self, PyObject* args) -{ - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - if (game->isHeadless()) { - PyErr_SetString(PyExc_RuntimeError, "Cannot center window in headless mode"); - return NULL; - } - - auto& window = game->getWindow(); - auto size = window.getSize(); - auto desktop = sf::VideoMode::getDesktopMode(); - - int x = (desktop.width - size.x) / 2; - int y = (desktop.height - size.y) / 2; - - window.setPosition(sf::Vector2i(x, y)); - - Py_RETURN_NONE; -} - -PyObject* PyWindow::screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds) -{ - static const char* keywords[] = {"filename", NULL}; - const char* filename = nullptr; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|s", const_cast(keywords), &filename)) { - return NULL; - } - - GameEngine* game = McRFPy_API::game; - if (!game) { - PyErr_SetString(PyExc_RuntimeError, "No game engine initialized"); - return NULL; - } - - // Get the render target pointer - sf::RenderTarget* target = game->getRenderTargetPtr(); - if (!target) { - PyErr_SetString(PyExc_RuntimeError, "No render target available"); - return NULL; - } - - sf::Image screenshot; - - // For RenderWindow - if (auto* window = dynamic_cast(target)) { - sf::Vector2u windowSize = window->getSize(); - sf::Texture texture; - texture.create(windowSize.x, windowSize.y); - texture.update(*window); - screenshot = texture.copyToImage(); - } - // For RenderTexture (headless mode) - else if (auto* renderTexture = dynamic_cast(target)) { - screenshot = renderTexture->getTexture().copyToImage(); - } - else { - PyErr_SetString(PyExc_RuntimeError, "Unknown render target type"); - return NULL; - } - - // Save to file if filename provided - if (filename) { - if (!screenshot.saveToFile(filename)) { - PyErr_SetString(PyExc_IOError, "Failed to save screenshot"); - return NULL; - } - Py_RETURN_NONE; - } - - // Otherwise return as bytes - auto pixels = screenshot.getPixelsPtr(); - auto size = screenshot.getSize(); - - return PyBytes_FromStringAndSize((const char*)pixels, size.x * size.y * 4); -} - -// Property definitions -PyGetSetDef PyWindow::getsetters[] = { - {"resolution", (getter)get_resolution, (setter)set_resolution, - "Window resolution as (width, height) tuple", NULL}, - {"fullscreen", (getter)get_fullscreen, (setter)set_fullscreen, - "Window fullscreen state", NULL}, - {"vsync", (getter)get_vsync, (setter)set_vsync, - "Vertical sync enabled state", NULL}, - {"title", (getter)get_title, (setter)set_title, - "Window title string", NULL}, - {"visible", (getter)get_visible, (setter)set_visible, - "Window visibility state", NULL}, - {"framerate_limit", (getter)get_framerate_limit, (setter)set_framerate_limit, - "Frame rate limit (0 for unlimited)", NULL}, - {NULL} -}; - -// Method definitions -PyMethodDef PyWindow::methods[] = { - {"get", (PyCFunction)PyWindow::get, METH_VARARGS | METH_CLASS, - "Get the Window singleton instance"}, - {"center", (PyCFunction)PyWindow::center, METH_NOARGS, - "Center the window on the screen"}, - {"screenshot", (PyCFunction)PyWindow::screenshot, METH_VARARGS | METH_KEYWORDS, - "Take a screenshot. Pass filename to save to file, or get raw bytes if no filename."}, - {NULL} -}; \ No newline at end of file diff --git a/src/PyWindow.h b/src/PyWindow.h deleted file mode 100644 index c1fce8f..0000000 --- a/src/PyWindow.h +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once -#include "Common.h" -#include "Python.h" - -// Forward declarations -class GameEngine; - -// Python object structure for Window singleton -typedef struct { - PyObject_HEAD - // No data - Window is a singleton that accesses GameEngine -} PyWindowObject; - -// C++ interface for the Window singleton -class PyWindow -{ -public: - // Static methods for Python type - static PyObject* get(PyObject* cls, PyObject* args); - static PyObject* repr(PyWindowObject* self); - - // Getters and setters for window properties - static PyObject* get_resolution(PyWindowObject* self, void* closure); - static int set_resolution(PyWindowObject* self, PyObject* value, void* closure); - static PyObject* get_fullscreen(PyWindowObject* self, void* closure); - static int set_fullscreen(PyWindowObject* self, PyObject* value, void* closure); - static PyObject* get_vsync(PyWindowObject* self, void* closure); - static int set_vsync(PyWindowObject* self, PyObject* value, void* closure); - static PyObject* get_title(PyWindowObject* self, void* closure); - static int set_title(PyWindowObject* self, PyObject* value, void* closure); - static PyObject* get_visible(PyWindowObject* self, void* closure); - static int set_visible(PyWindowObject* self, PyObject* value, void* closure); - static PyObject* get_framerate_limit(PyWindowObject* self, void* closure); - static int set_framerate_limit(PyWindowObject* self, PyObject* value, void* closure); - - // Methods - static PyObject* center(PyWindowObject* self, PyObject* args); - static PyObject* screenshot(PyWindowObject* self, PyObject* args, PyObject* kwds); - - static PyGetSetDef getsetters[]; - static PyMethodDef methods[]; - -}; - -namespace mcrfpydef { - static PyTypeObject PyWindowType = { - .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, - .tp_name = "mcrfpy.Window", - .tp_basicsize = sizeof(PyWindowObject), - .tp_itemsize = 0, - .tp_dealloc = (destructor)[](PyObject* self) { - // Don't delete the singleton instance - Py_TYPE(self)->tp_free(self); - }, - .tp_repr = (reprfunc)PyWindow::repr, - .tp_flags = Py_TPFLAGS_DEFAULT, - .tp_doc = PyDoc_STR("Window singleton for accessing and modifying the game window properties"), - .tp_methods = nullptr, // Set in McRFPy_API.cpp after definition - .tp_getset = nullptr, // Set in McRFPy_API.cpp after definition - .tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject* { - PyErr_SetString(PyExc_TypeError, "Cannot instantiate Window. Use Window.get() to access the singleton."); - return NULL; - } - }; -} \ No newline at end of file diff --git a/src/SceneTransition.cpp b/src/SceneTransition.cpp deleted file mode 100644 index 574f29c..0000000 --- a/src/SceneTransition.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "SceneTransition.h" - -void SceneTransition::start(TransitionType t, const std::string& from, const std::string& to, float dur) { - type = t; - fromScene = from; - toScene = to; - duration = dur; - elapsed = 0.0f; - - // Initialize render textures if needed - if (!oldSceneTexture) { - oldSceneTexture = std::make_unique(); - oldSceneTexture->create(1024, 768); - } - if (!newSceneTexture) { - newSceneTexture = std::make_unique(); - newSceneTexture->create(1024, 768); - } -} - -void SceneTransition::update(float dt) { - if (type == TransitionType::None) return; - elapsed += dt; -} - -void SceneTransition::render(sf::RenderTarget& target) { - if (type == TransitionType::None) return; - - float progress = getProgress(); - float easedProgress = easeInOut(progress); - - // Update sprites with current textures - oldSprite.setTexture(oldSceneTexture->getTexture()); - newSprite.setTexture(newSceneTexture->getTexture()); - - switch (type) { - case TransitionType::Fade: - // Fade out old scene, fade in new scene - oldSprite.setColor(sf::Color(255, 255, 255, 255 * (1.0f - easedProgress))); - newSprite.setColor(sf::Color(255, 255, 255, 255 * easedProgress)); - target.draw(oldSprite); - target.draw(newSprite); - break; - - case TransitionType::SlideLeft: - // Old scene slides out to left, new scene slides in from right - oldSprite.setPosition(-1024 * easedProgress, 0); - newSprite.setPosition(1024 * (1.0f - easedProgress), 0); - target.draw(oldSprite); - target.draw(newSprite); - break; - - case TransitionType::SlideRight: - // Old scene slides out to right, new scene slides in from left - oldSprite.setPosition(1024 * easedProgress, 0); - newSprite.setPosition(-1024 * (1.0f - easedProgress), 0); - target.draw(oldSprite); - target.draw(newSprite); - break; - - case TransitionType::SlideUp: - // Old scene slides up, new scene slides in from bottom - oldSprite.setPosition(0, -768 * easedProgress); - newSprite.setPosition(0, 768 * (1.0f - easedProgress)); - target.draw(oldSprite); - target.draw(newSprite); - break; - - case TransitionType::SlideDown: - // Old scene slides down, new scene slides in from top - oldSprite.setPosition(0, 768 * easedProgress); - newSprite.setPosition(0, -768 * (1.0f - easedProgress)); - target.draw(oldSprite); - target.draw(newSprite); - break; - - default: - break; - } -} - -float SceneTransition::easeInOut(float t) { - // Smooth ease-in-out curve - return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t; -} \ No newline at end of file diff --git a/src/SceneTransition.h b/src/SceneTransition.h deleted file mode 100644 index 7103323..0000000 --- a/src/SceneTransition.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once -#include "Common.h" -#include -#include -#include - -enum class TransitionType { - None, - Fade, - SlideLeft, - SlideRight, - SlideUp, - SlideDown -}; - -class SceneTransition { -public: - TransitionType type = TransitionType::None; - float duration = 0.0f; - float elapsed = 0.0f; - std::string fromScene; - std::string toScene; - - // Render textures for transition - std::unique_ptr oldSceneTexture; - std::unique_ptr newSceneTexture; - - // Sprites for rendering textures - sf::Sprite oldSprite; - sf::Sprite newSprite; - - SceneTransition() = default; - - void start(TransitionType t, const std::string& from, const std::string& to, float dur); - void update(float dt); - void render(sf::RenderTarget& target); - bool isComplete() const { return elapsed >= duration; } - float getProgress() const { return duration > 0 ? std::min(elapsed / duration, 1.0f) : 1.0f; } - - // Easing function for smooth transitions - static float easeInOut(float t); -}; \ No newline at end of file diff --git a/src/UIBase.h b/src/UIBase.h index c1707bf..70a5872 100644 --- a/src/UIBase.h +++ b/src/UIBase.h @@ -1,6 +1,4 @@ #pragma once -#include "Python.h" -#include class UIEntity; typedef struct { @@ -32,103 +30,3 @@ typedef struct { PyObject_HEAD std::shared_ptr data; } PyUISpriteObject; - -// Common Python method implementations for UIDrawable-derived classes -// These template functions provide shared functionality for Python bindings - -// get_bounds method implementation (#89) -template -static PyObject* UIDrawable_get_bounds(T* self, PyObject* Py_UNUSED(args)) -{ - auto bounds = self->data->get_bounds(); - return Py_BuildValue("(ffff)", bounds.left, bounds.top, bounds.width, bounds.height); -} - -// move method implementation (#98) -template -static PyObject* UIDrawable_move(T* self, PyObject* args) -{ - float dx, dy; - if (!PyArg_ParseTuple(args, "ff", &dx, &dy)) { - return NULL; - } - - self->data->move(dx, dy); - Py_RETURN_NONE; -} - -// resize method implementation (#98) -template -static PyObject* UIDrawable_resize(T* self, PyObject* args) -{ - float w, h; - if (!PyArg_ParseTuple(args, "ff", &w, &h)) { - return NULL; - } - - self->data->resize(w, h); - Py_RETURN_NONE; -} - -// Macro to add common UIDrawable methods to a method array -#define UIDRAWABLE_METHODS \ - {"get_bounds", (PyCFunction)UIDrawable_get_bounds, METH_NOARGS, \ - "Get bounding box as (x, y, width, height)"}, \ - {"move", (PyCFunction)UIDrawable_move, METH_VARARGS, \ - "Move by relative offset (dx, dy)"}, \ - {"resize", (PyCFunction)UIDrawable_resize, METH_VARARGS, \ - "Resize to new dimensions (width, height)"} - -// Property getters/setters for visible and opacity -template -static PyObject* UIDrawable_get_visible(T* self, void* closure) -{ - return PyBool_FromLong(self->data->visible); -} - -template -static int UIDrawable_set_visible(T* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); - return -1; - } - self->data->visible = PyObject_IsTrue(value); - return 0; -} - -template -static PyObject* UIDrawable_get_opacity(T* self, void* closure) -{ - return PyFloat_FromDouble(self->data->opacity); -} - -template -static int UIDrawable_set_opacity(T* self, PyObject* value, void* closure) -{ - float opacity; - if (PyFloat_Check(value)) { - opacity = PyFloat_AsDouble(value); - } else if (PyLong_Check(value)) { - opacity = PyLong_AsDouble(value); - } else { - PyErr_SetString(PyExc_TypeError, "opacity must be a number"); - return -1; - } - - // Clamp to valid range - if (opacity < 0.0f) opacity = 0.0f; - if (opacity > 1.0f) opacity = 1.0f; - - self->data->opacity = opacity; - return 0; -} - -// Macro to add common UIDrawable properties to a getsetters array -#define UIDRAWABLE_GETSETTERS \ - {"visible", (getter)UIDrawable_get_visible, (setter)UIDrawable_set_visible, \ - "Visibility flag", NULL}, \ - {"opacity", (getter)UIDrawable_get_opacity, (setter)UIDrawable_set_opacity, \ - "Opacity (0.0 = transparent, 1.0 = opaque)", NULL} - -// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete diff --git a/src/UICaption.cpp b/src/UICaption.cpp index fc57d6e..22b4787 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -3,21 +3,8 @@ #include "PyColor.h" #include "PyVector.h" #include "PyFont.h" -#include "PyPositionHelper.h" -// UIDrawable methods now in UIBase.h #include -UICaption::UICaption() -{ - // Initialize text with safe defaults - text.setString(""); - text.setPosition(0.0f, 0.0f); - text.setCharacterSize(12); - text.setFillColor(sf::Color::White); - text.setOutlineColor(sf::Color::Black); - text.setOutlineThickness(0.0f); -} - UIDrawable* UICaption::click_at(sf::Vector2f point) { if (click_callable) @@ -29,22 +16,10 @@ UIDrawable* UICaption::click_at(sf::Vector2f point) void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target) { - // Check visibility - if (!visible) return; - - // Apply opacity - auto color = text.getFillColor(); - color.a = static_cast(255 * opacity); - text.setFillColor(color); - text.move(offset); //Resources::game->getWindow().draw(text); target.draw(text); text.move(-offset); - - // Restore original alpha - color.a = 255; - text.setFillColor(color); } PyObjectsEnum UICaption::derived_type() @@ -52,23 +27,6 @@ PyObjectsEnum UICaption::derived_type() return PyObjectsEnum::UICAPTION; } -// Phase 1 implementations -sf::FloatRect UICaption::get_bounds() const -{ - return text.getGlobalBounds(); -} - -void UICaption::move(float dx, float dy) -{ - text.move(dx, dy); -} - -void UICaption::resize(float w, float h) -{ - // Caption doesn't support direct resizing - size is controlled by font size - // This is a no-op but required by the interface -} - PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure) { auto member_ptr = reinterpret_cast(closure); @@ -164,6 +122,7 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void* // get value from mcrfpy.Color instance auto c = ((PyColorObject*)value)->data; r = c.r; g = c.g; b = c.b; a = c.a; + std::cout << "got " << int(r) << ", " << int(g) << ", " << int(b) << ", " << int(a) << std::endl; } else if (!PyTuple_Check(value) || PyTuple_Size(value) < 3 || PyTuple_Size(value) > 4) { @@ -208,15 +167,6 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void* } -// Define the PyObjectType alias for the macros -typedef PyUICaptionObject PyObjectType; - -// Method definitions -PyMethodDef UICaption_methods[] = { - UIDRAWABLE_METHODS, - {NULL} // Sentinel -}; - //TODO: evaluate use of Resources::caption_buffer... can't I do this with a std::string? PyObject* UICaption::get_text(PyUICaptionObject* self, void* closure) { @@ -250,8 +200,6 @@ PyGetSetDef UICaption::getsetters[] = { {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION}, - {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UICAPTION}, - UIDRAWABLE_GETSETTERS, {NULL} }; @@ -277,92 +225,30 @@ PyObject* UICaption::repr(PyUICaptionObject* self) int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) { using namespace mcrfpydef; - - static const char* keywords[] = { "text", "x", "y", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr }; - float x = 0.0f, y = 0.0f, outline = 0.0f; - char* text = NULL; - PyObject* font = NULL; - PyObject* fill_color = NULL; - PyObject* outline_color = NULL; - PyObject* click_handler = NULL; - PyObject* pos_obj = NULL; - - // Handle different argument patterns - Py_ssize_t args_size = PyTuple_Size(args); - - if (args_size >= 2 && !PyUnicode_Check(PyTuple_GetItem(args, 0))) { - // Pattern 1: (x, y, text, ...) or ((x,y), text, ...) - PyObject* first_arg = PyTuple_GetItem(args, 0); - - // Check if first arg is a tuple/Vector (pos format) - if (PyTuple_Check(first_arg) || PyObject_IsInstance(first_arg, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector"))) { - // Pattern: ((x,y), text, ...) - static const char* pos_keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", "click", "x", "y", nullptr }; - PyObject* pos = NULL; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OzOOOfOff", - const_cast(pos_keywords), - &pos, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &x, &y)) - { - return -1; - } - - // Parse position - if (pos && pos != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } - } - else { - // Pattern: (x, y, text, ...) - static const char* xy_keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr }; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOfOO", - const_cast(xy_keywords), - &x, &y, &text, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) - { - return -1; - } - - // If pos was provided, it overrides x,y - if (pos_obj && pos_obj != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } - } - } - else { - // Pattern 2: (text, ...) with x, y as keywords - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|zffOOOfOO", - const_cast(keywords), - &text, &x, &y, &font, &fill_color, &outline_color, &outline, &click_handler, &pos_obj)) - { - return -1; - } - - // If pos was provided, it overrides x,y - if (pos_obj && pos_obj != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } + // Constructor switch to Vector position + //static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr }; + //float x = 0.0f, y = 0.0f, outline = 0.0f; + static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr }; + PyObject* pos; + float outline = 0.0f; + char* text; + PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL; + + //if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf", + // const_cast(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf", + const_cast(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline)) + { + return -1; } - self->data->text.setPosition(x, y); + PyVectorObject* pos_result = PyVector::from_arg(pos); + if (!pos_result) + { + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; + } + self->data->text.setPosition(pos_result->data); // check types for font, fill_color, outline_color //std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl; @@ -389,12 +275,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) } } - // Handle text - default to empty string if not provided - if (text && text != NULL) { - self->data->text.setString((std::string)text); - } else { - self->data->text.setString(""); - } + self->data->text.setString((std::string)text); self->data->text.setOutlineThickness(outline); if (fill_color) { auto fc = PyColor::from_arg(fill_color); @@ -420,15 +301,6 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds) self->data->text.setOutlineColor(sf::Color(128,128,128,255)); } - // Process click handler if provided - if (click_handler && click_handler != Py_None) { - if (!PyCallable_Check(click_handler)) { - PyErr_SetString(PyExc_TypeError, "click must be callable"); - return -1; - } - self->data->click_register(click_handler); - } - return 0; } diff --git a/src/UICaption.h b/src/UICaption.h index 7f00b22..60d8e13 100644 --- a/src/UICaption.h +++ b/src/UICaption.h @@ -7,16 +7,10 @@ class UICaption: public UIDrawable { public: sf::Text text; - UICaption(); // Default constructor with safe initialization void render(sf::Vector2f, sf::RenderTarget&) override final; PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; - // Phase 1 virtual method implementations - sf::FloatRect get_bounds() const override; - void move(float dx, float dy) override; - void resize(float w, float h) override; - // Property system for animations bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, const sf::Color& value) override; @@ -40,8 +34,6 @@ public: }; -extern PyMethodDef UICaption_methods[]; - namespace mcrfpydef { static PyTypeObject PyUICaptionType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, @@ -64,7 +56,7 @@ namespace mcrfpydef { //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("docstring"), - .tp_methods = UICaption_methods, + //.tp_methods = PyUIFrame_methods, //.tp_members = PyUIFrame_members, .tp_getset = UICaption::getsetters, //.tp_base = NULL, 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/src/UIDrawable.cpp b/src/UIDrawable.cpp index d62578f..553eaf5 100644 --- a/src/UIDrawable.cpp +++ b/src/UIDrawable.cpp @@ -25,28 +25,16 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) { switch (objtype) { case PyObjectsEnum::UIFRAME: - if (((PyUIFrameObject*)self)->data->click_callable) - ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow(); - else - ptr = NULL; + ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow(); break; case PyObjectsEnum::UICAPTION: - if (((PyUICaptionObject*)self)->data->click_callable) - ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow(); - else - ptr = NULL; + ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow(); break; case PyObjectsEnum::UISPRITE: - if (((PyUISpriteObject*)self)->data->click_callable) - ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow(); - else - ptr = NULL; + ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow(); break; case PyObjectsEnum::UIGRID: - if (((PyUIGridObject*)self)->data->click_callable) - ptr = ((PyUIGridObject*)self)->data->click_callable->borrow(); - else - ptr = NULL; + ptr = ((PyUIGridObject*)self)->data->click_callable->borrow(); break; default: PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click"); @@ -175,102 +163,3 @@ void UIDrawable::notifyZIndexChanged() { // For now, Frame children will need manual sorting or collection modification // to trigger a resort } - -PyObject* UIDrawable::get_name(PyObject* self, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = nullptr; - - switch (objtype) { - case PyObjectsEnum::UIFRAME: - drawable = ((PyUIFrameObject*)self)->data.get(); - break; - case PyObjectsEnum::UICAPTION: - drawable = ((PyUICaptionObject*)self)->data.get(); - break; - case PyObjectsEnum::UISPRITE: - drawable = ((PyUISpriteObject*)self)->data.get(); - break; - case PyObjectsEnum::UIGRID: - drawable = ((PyUIGridObject*)self)->data.get(); - break; - default: - PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); - return NULL; - } - - return PyUnicode_FromString(drawable->name.c_str()); -} - -int UIDrawable::set_name(PyObject* self, PyObject* value, void* closure) { - PyObjectsEnum objtype = static_cast(reinterpret_cast(closure)); - UIDrawable* drawable = nullptr; - - switch (objtype) { - case PyObjectsEnum::UIFRAME: - drawable = ((PyUIFrameObject*)self)->data.get(); - break; - case PyObjectsEnum::UICAPTION: - drawable = ((PyUICaptionObject*)self)->data.get(); - break; - case PyObjectsEnum::UISPRITE: - drawable = ((PyUISpriteObject*)self)->data.get(); - break; - case PyObjectsEnum::UIGRID: - drawable = ((PyUIGridObject*)self)->data.get(); - break; - default: - PyErr_SetString(PyExc_TypeError, "Invalid UIDrawable derived instance"); - return -1; - } - - if (value == NULL || value == Py_None) { - drawable->name = ""; - return 0; - } - - if (!PyUnicode_Check(value)) { - PyErr_SetString(PyExc_TypeError, "name must be a string"); - return -1; - } - - const char* name_str = PyUnicode_AsUTF8(value); - if (!name_str) { - return -1; - } - - drawable->name = name_str; - return 0; -} - -void UIDrawable::enableRenderTexture(unsigned int width, unsigned int height) { - // Create or recreate RenderTexture if size changed - if (!render_texture || render_texture->getSize().x != width || render_texture->getSize().y != height) { - render_texture = std::make_unique(); - if (!render_texture->create(width, height)) { - render_texture.reset(); - use_render_texture = false; - return; - } - render_sprite.setTexture(render_texture->getTexture()); - } - - use_render_texture = true; - render_dirty = true; -} - -void UIDrawable::updateRenderTexture() { - if (!use_render_texture || !render_texture) { - return; - } - - // Clear the RenderTexture - render_texture->clear(sf::Color::Transparent); - - // Render content to RenderTexture - // This will be overridden by derived classes - // For now, just display the texture - render_texture->display(); - - // Update the sprite - render_sprite.setTexture(render_texture->getTexture()); -} diff --git a/src/UIDrawable.h b/src/UIDrawable.h index 9d2a9f1..4ff470f 100644 --- a/src/UIDrawable.h +++ b/src/UIDrawable.h @@ -44,8 +44,6 @@ public: static int set_click(PyObject* self, PyObject* value, void* closure); static PyObject* get_int(PyObject* self, void* closure); static int set_int(PyObject* self, PyObject* value, void* closure); - static PyObject* get_name(PyObject* self, void* closure); - static int set_name(PyObject* self, PyObject* value, void* closure); // Z-order for rendering (lower values rendered first, higher values on top) int z_index = 0; @@ -53,18 +51,6 @@ public: // Notification for z_index changes void notifyZIndexChanged(); - // Name for finding elements - std::string name; - - // New properties for Phase 1 - bool visible = true; // #87 - visibility flag - float opacity = 1.0f; // #88 - opacity (0.0 = transparent, 1.0 = opaque) - - // New virtual methods for Phase 1 - virtual sf::FloatRect get_bounds() const = 0; // #89 - get bounding box - virtual void move(float dx, float dy) = 0; // #98 - move by offset - virtual void resize(float w, float h) = 0; // #98 - resize to dimensions - // Animation support virtual bool setProperty(const std::string& name, float value) { return false; } virtual bool setProperty(const std::string& name, int value) { return false; } @@ -77,21 +63,6 @@ public: virtual bool getProperty(const std::string& name, sf::Color& value) const { return false; } virtual bool getProperty(const std::string& name, sf::Vector2f& value) const { return false; } virtual bool getProperty(const std::string& name, std::string& value) const { return false; } - -protected: - // RenderTexture support (opt-in) - std::unique_ptr render_texture; - sf::Sprite render_sprite; - bool use_render_texture = false; - bool render_dirty = true; - - // Enable RenderTexture for this drawable - void enableRenderTexture(unsigned int width, unsigned int height); - void updateRenderTexture(); - -public: - // Mark this drawable as needing redraw - void markDirty() { render_dirty = true; } }; typedef struct { diff --git a/src/UIEntity.cpp b/src/UIEntity.cpp index 3ac98fe..41f10fa 100644 --- a/src/UIEntity.cpp +++ b/src/UIEntity.cpp @@ -1,20 +1,11 @@ #include "UIEntity.h" #include "UIGrid.h" #include "McRFPy_API.h" -#include #include "PyObjectUtils.h" #include "PyVector.h" -#include "PyPositionHelper.h" -// UIDrawable methods now in UIBase.h -#include "UIEntityPyMethods.h" -UIEntity::UIEntity() -: self(nullptr), grid(nullptr), position(0.0f, 0.0f), collision_pos(0, 0) -{ - // Initialize sprite with safe defaults (sprite has its own safe constructor now) - // gridstate vector starts empty since we don't know grid dimensions -} +UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it UIEntity::UIEntity(UIGrid& grid) : gridstate(grid.grid_x * grid.grid_y) @@ -73,52 +64,28 @@ PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) } int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", "pos", nullptr }; - float x = 0.0f, y = 0.0f; - int sprite_index = 0; // Default to sprite index 0 + //static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr }; + //float x = 0.0f, y = 0.0f, scale = 1.0f; + static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr }; + PyObject* pos; + float scale = 1.0f; + int sprite_index = -1; PyObject* texture = NULL; PyObject* grid = NULL; - PyObject* pos_obj = NULL; - // Try to parse all arguments with keywords - if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO", - const_cast(keywords), &x, &y, &texture, &sprite_index, &grid, &pos_obj)) + //if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O", + // const_cast(keywords), &x, &y, &texture, &sprite_index, &grid)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO", + const_cast(keywords), &pos, &texture, &sprite_index, &grid)) { - // If pos was provided, it overrides x,y - if (pos_obj && pos_obj != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } + return -1; } - else + + PyVectorObject* pos_result = PyVector::from_arg(pos); + if (!pos_result) { - PyErr_Clear(); - - // Try alternative: pos as first argument - static const char* alt_keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr }; - PyObject* pos = NULL; - - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOiO", - const_cast(alt_keywords), &pos, &texture, &sprite_index, &grid)) - { - return -1; - } - - // Parse position - if (pos && pos != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } + PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__"); + return -1; } // check types for texture @@ -137,11 +104,10 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { texture_ptr = McRFPy_API::default_texture; } - // Allow creation without texture for testing purposes - // if (!texture_ptr) { - // PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); - // return -1; - // } + if (!texture_ptr) { + PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available"); + return -1; + } if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance"); @@ -158,17 +124,8 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) { Py_INCREF(self); // TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers - if (texture_ptr) { - self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); - } else { - // Create an empty sprite for testing - self->data->sprite = UISprite(); - } - - // Set position - self->data->position = sf::Vector2f(x, y); - self->data->collision_pos = sf::Vector2i(static_cast(x), static_cast(y)); - + self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0); + self->data->position = pos_result->data; if (grid != NULL) { PyUIGridObject* pygrid = (PyUIGridObject*)grid; self->data->grid = pygrid->data; @@ -287,106 +244,18 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl return 0; } -PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure) -{ - auto member_ptr = reinterpret_cast(closure); - if (member_ptr == 0) // x - return PyFloat_FromDouble(self->data->position.x); - else if (member_ptr == 1) // y - return PyFloat_FromDouble(self->data->position.y); - else - { - PyErr_SetString(PyExc_AttributeError, "Invalid attribute"); - return nullptr; - } -} - -int UIEntity::set_float_member(PyUIEntityObject* self, PyObject* value, void* closure) -{ - float val; - auto member_ptr = reinterpret_cast(closure); - if (PyFloat_Check(value)) - { - val = PyFloat_AsDouble(value); - } - else if (PyLong_Check(value)) - { - val = PyLong_AsLong(value); - } - else - { - PyErr_SetString(PyExc_TypeError, "Value must be a floating point number."); - return -1; - } - if (member_ptr == 0) // x - { - self->data->position.x = val; - self->data->collision_pos.x = static_cast(val); - } - else if (member_ptr == 1) // y - { - self->data->position.y = val; - self->data->collision_pos.y = static_cast(val); - } - return 0; -} - -PyObject* UIEntity::die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) -{ - // Check if entity has a grid - if (!self->data || !self->data->grid) { - Py_RETURN_NONE; // Entity not on a grid, nothing to do - } - - // Remove entity from grid's entity list - auto grid = self->data->grid; - auto& entities = grid->entities; - - // Find and remove this entity from the list - auto it = std::find_if(entities->begin(), entities->end(), - [self](const std::shared_ptr& e) { - return e.get() == self->data.get(); - }); - - if (it != entities->end()) { - entities->erase(it); - // Clear the grid reference - self->data->grid.reset(); - } - - Py_RETURN_NONE; -} - PyMethodDef UIEntity::methods[] = { {"at", (PyCFunction)UIEntity::at, METH_O}, {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, - {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, {NULL, NULL, 0, NULL} }; -// Define the PyObjectType alias for the macros -typedef PyUIEntityObject PyObjectType; - -// Combine base methods with entity-specific methods -PyMethodDef UIEntity_all_methods[] = { - UIDRAWABLE_METHODS, - {"at", (PyCFunction)UIEntity::at, METH_O}, - {"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"}, - {"die", (PyCFunction)UIEntity::die, METH_NOARGS, "Remove this entity from its grid"}, - {NULL} // Sentinel -}; - PyGetSetDef UIEntity::getsetters[] = { {"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0}, {"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1}, {"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL}, {"sprite_index", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display", NULL}, {"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite index on the texture on the display (deprecated: use sprite_index)", NULL}, - {"x", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity x position", (void*)0}, - {"y", (getter)UIEntity::get_float_member, (setter)UIEntity::set_float_member, "Entity y position", (void*)1}, - {"visible", (getter)UIEntity_get_visible, (setter)UIEntity_set_visible, "Visibility flag", NULL}, - {"opacity", (getter)UIEntity_get_opacity, (setter)UIEntity_set_opacity, "Opacity (0.0 = transparent, 1.0 = opaque)", NULL}, - {"name", (getter)UIEntity_get_name, (setter)UIEntity_set_name, "Name for finding elements", NULL}, {NULL} /* Sentinel */ }; diff --git a/src/UIEntity.h b/src/UIEntity.h index 0ad7d88..16f3d3d 100644 --- a/src/UIEntity.h +++ b/src/UIEntity.h @@ -51,14 +51,8 @@ public: bool setProperty(const std::string& name, int value); bool getProperty(const std::string& name, float& value) const; - // Methods that delegate to sprite - sf::FloatRect get_bounds() const { return sprite.get_bounds(); } - void move(float dx, float dy) { sprite.move(dx, dy); position.x += dx; position.y += dy; } - void resize(float w, float h) { /* Entities don't support direct resizing */ } - static PyObject* at(PyUIEntityObject* self, PyObject* o); static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); - static PyObject* die(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)); static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds); static PyObject* get_position(PyUIEntityObject* self, void* closure); @@ -66,16 +60,11 @@ public: static PyObject* get_gridstate(PyUIEntityObject* self, void* closure); static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure); static int set_spritenumber(PyUIEntityObject* self, PyObject* value, void* closure); - static PyObject* get_float_member(PyUIEntityObject* self, void* closure); - static int set_float_member(PyUIEntityObject* self, PyObject* value, void* closure); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; static PyObject* repr(PyUIEntityObject* self); }; -// Forward declaration of methods array -extern PyMethodDef UIEntity_all_methods[]; - namespace mcrfpydef { static PyTypeObject PyUIEntityType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, @@ -85,7 +74,7 @@ namespace mcrfpydef { .tp_repr = (reprfunc)UIEntity::repr, .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, .tp_doc = "UIEntity objects", - .tp_methods = UIEntity_all_methods, + .tp_methods = UIEntity::methods, .tp_getset = UIEntity::getsetters, .tp_init = (initproc)UIEntity::init, .tp_new = PyType_GenericNew, diff --git a/src/UIEntityPyMethods.h b/src/UIEntityPyMethods.h deleted file mode 100644 index 53e5732..0000000 --- a/src/UIEntityPyMethods.h +++ /dev/null @@ -1,75 +0,0 @@ -#pragma once -#include "UIEntity.h" -#include "UIBase.h" - -// UIEntity-specific property implementations -// These delegate to the wrapped sprite member - -// Visible property -static PyObject* UIEntity_get_visible(PyUIEntityObject* self, void* closure) -{ - return PyBool_FromLong(self->data->sprite.visible); -} - -static int UIEntity_set_visible(PyUIEntityObject* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "visible must be a boolean"); - return -1; - } - self->data->sprite.visible = PyObject_IsTrue(value); - return 0; -} - -// Opacity property -static PyObject* UIEntity_get_opacity(PyUIEntityObject* self, void* closure) -{ - return PyFloat_FromDouble(self->data->sprite.opacity); -} - -static int UIEntity_set_opacity(PyUIEntityObject* self, PyObject* value, void* closure) -{ - float opacity; - if (PyFloat_Check(value)) { - opacity = PyFloat_AsDouble(value); - } else if (PyLong_Check(value)) { - opacity = PyLong_AsDouble(value); - } else { - PyErr_SetString(PyExc_TypeError, "opacity must be a number"); - return -1; - } - - // Clamp to valid range - if (opacity < 0.0f) opacity = 0.0f; - if (opacity > 1.0f) opacity = 1.0f; - - self->data->sprite.opacity = opacity; - return 0; -} - -// Name property - delegate to sprite -static PyObject* UIEntity_get_name(PyUIEntityObject* self, void* closure) -{ - return PyUnicode_FromString(self->data->sprite.name.c_str()); -} - -static int UIEntity_set_name(PyUIEntityObject* self, PyObject* value, void* closure) -{ - if (value == NULL || value == Py_None) { - self->data->sprite.name = ""; - return 0; - } - - if (!PyUnicode_Check(value)) { - PyErr_SetString(PyExc_TypeError, "name must be a string"); - return -1; - } - - const char* name_str = PyUnicode_AsUTF8(value); - if (!name_str) { - return -1; - } - - self->data->sprite.name = name_str; - return 0; -} \ No newline at end of file diff --git a/src/UIFrame.cpp b/src/UIFrame.cpp index 21bc6c3..f6f7fa7 100644 --- a/src/UIFrame.cpp +++ b/src/UIFrame.cpp @@ -2,40 +2,21 @@ #include "UICollection.h" #include "GameEngine.h" #include "PyVector.h" -#include "UICaption.h" -#include "UISprite.h" -#include "UIGrid.h" -#include "McRFPy_API.h" -#include "PyPositionHelper.h" -// UIDrawable methods now in UIBase.h UIDrawable* UIFrame::click_at(sf::Vector2f point) { - // Check bounds first (optimization) - float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y; - if (point.x < x || point.y < y || point.x >= x+w || point.y >= y+h) { - return nullptr; + for (auto e: *children) + { + auto p = e->click_at(point + box.getPosition()); + if (p) + return p; } - - // Transform to local coordinates for children - sf::Vector2f localPoint = point - box.getPosition(); - - // Check children in reverse order (top to bottom, highest z-index first) - for (auto it = children->rbegin(); it != children->rend(); ++it) { - auto& child = *it; - if (!child->visible) continue; - - if (auto target = child->click_at(localPoint)) { - return target; - } + if (click_callable) + { + float x = box.getPosition().x, y = box.getPosition().y, w = box.getSize().x, h = box.getSize().y; + if (point.x > x && point.y > y && point.x < x+w && point.y < y+h) return this; } - - // No child handled it, check if we have a handler - if (click_callable) { - return this; - } - - return nullptr; + return NULL; } UIFrame::UIFrame() @@ -64,95 +45,24 @@ PyObjectsEnum UIFrame::derived_type() return PyObjectsEnum::UIFRAME; } -// Phase 1 implementations -sf::FloatRect UIFrame::get_bounds() const -{ - auto pos = box.getPosition(); - auto size = box.getSize(); - return sf::FloatRect(pos.x, pos.y, size.x, size.y); -} - -void UIFrame::move(float dx, float dy) -{ - box.move(dx, dy); -} - -void UIFrame::resize(float w, float h) -{ - box.setSize(sf::Vector2f(w, h)); -} - void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target) { - // Check visibility - if (!visible) return; - - // TODO: Apply opacity when SFML supports it on shapes - - // Check if we need to use RenderTexture for clipping - if (clip_children && !children->empty()) { - // Enable RenderTexture if not already enabled - if (!use_render_texture) { - auto size = box.getSize(); - enableRenderTexture(static_cast(size.x), - static_cast(size.y)); - } - - // Update RenderTexture if dirty - if (use_render_texture && render_dirty) { - // Clear the RenderTexture - render_texture->clear(sf::Color::Transparent); - - // Draw the frame box to RenderTexture - box.setPosition(0, 0); // Render at origin in texture - render_texture->draw(box); - - // Sort children by z_index if needed - if (children_need_sort && !children->empty()) { - std::sort(children->begin(), children->end(), - [](const std::shared_ptr& a, const std::shared_ptr& b) { - return a->z_index < b->z_index; - }); - children_need_sort = false; - } - - // Render children to RenderTexture at local coordinates - for (auto drawable : *children) { - drawable->render(sf::Vector2f(0, 0), *render_texture); - } - - // Finalize the RenderTexture - render_texture->display(); - - // Update sprite - render_sprite.setTexture(render_texture->getTexture()); - - render_dirty = false; - } - - // Draw the RenderTexture sprite - if (use_render_texture) { - render_sprite.setPosition(offset + box.getPosition()); - target.draw(render_sprite); - } - } else { - // Standard rendering without clipping - box.move(offset); - target.draw(box); - box.move(-offset); + box.move(offset); + //Resources::game->getWindow().draw(box); + target.draw(box); + box.move(-offset); - // Sort children by z_index if needed - if (children_need_sort && !children->empty()) { - std::sort(children->begin(), children->end(), - [](const std::shared_ptr& a, const std::shared_ptr& b) { - return a->z_index < b->z_index; - }); - children_need_sort = false; - } + // Sort children by z_index if needed + if (children_need_sort && !children->empty()) { + std::sort(children->begin(), children->end(), + [](const std::shared_ptr& a, const std::shared_ptr& b) { + return a->z_index < b->z_index; + }); + children_need_sort = false; + } - for (auto drawable : *children) { - drawable->render(offset + box.getPosition(), target); - } + for (auto drawable : *children) { + drawable->render(offset + box.getPosition(), target); } } @@ -205,36 +115,16 @@ int UIFrame::set_float_member(PyUIFrameObject* self, PyObject* value, void* clos PyErr_SetString(PyExc_TypeError, "Value must be an integer."); return -1; } - if (member_ptr == 0) { //x + if (member_ptr == 0) //x self->data->box.setPosition(val, self->data->box.getPosition().y); - self->data->markDirty(); - } - else if (member_ptr == 1) { //y + else if (member_ptr == 1) //y self->data->box.setPosition(self->data->box.getPosition().x, val); - self->data->markDirty(); - } - else if (member_ptr == 2) { //w + else if (member_ptr == 2) //w self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y)); - if (self->data->use_render_texture) { - // Need to recreate RenderTexture with new size - self->data->enableRenderTexture(static_cast(self->data->box.getSize().x), - static_cast(self->data->box.getSize().y)); - } - self->data->markDirty(); - } - else if (member_ptr == 3) { //h + else if (member_ptr == 3) //h self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val)); - if (self->data->use_render_texture) { - // Need to recreate RenderTexture with new size - self->data->enableRenderTexture(static_cast(self->data->box.getSize().x), - static_cast(self->data->box.getSize().y)); - } - self->data->markDirty(); - } - else if (member_ptr == 4) { //outline + else if (member_ptr == 4) //outline self->data->box.setOutlineThickness(val); - self->data->markDirty(); - } return 0; } @@ -311,12 +201,10 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos if (member_ptr == 0) { self->data->box.setFillColor(sf::Color(r, g, b, a)); - self->data->markDirty(); } else if (member_ptr == 1) { self->data->box.setOutlineColor(sf::Color(r, g, b, a)); - self->data->markDirty(); } else { @@ -346,40 +234,9 @@ int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure) return -1; } self->data->box.setPosition(vec->data); - self->data->markDirty(); return 0; } -PyObject* UIFrame::get_clip_children(PyUIFrameObject* self, void* closure) -{ - return PyBool_FromLong(self->data->clip_children); -} - -int UIFrame::set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure) -{ - if (!PyBool_Check(value)) { - PyErr_SetString(PyExc_TypeError, "clip_children must be a boolean"); - return -1; - } - - bool new_clip = PyObject_IsTrue(value); - if (new_clip != self->data->clip_children) { - self->data->clip_children = new_clip; - self->data->markDirty(); // Mark as needing redraw - } - - return 0; -} - -// Define the PyObjectType alias for the macros -typedef PyUIFrameObject PyObjectType; - -// Method definitions -PyMethodDef UIFrame_methods[] = { - UIDRAWABLE_METHODS, - {NULL} // Sentinel -}; - PyGetSetDef UIFrame::getsetters[] = { {"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0}, {"y", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Y coordinate of top-left corner", (void*)1}, @@ -391,10 +248,7 @@ PyGetSetDef UIFrame::getsetters[] = { {"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME}, - {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIFRAME}, {"pos", (getter)UIFrame::get_pos, (setter)UIFrame::set_pos, "Position as a Vector", NULL}, - {"clip_children", (getter)UIFrame::get_clip_children, (setter)UIFrame::set_clip_children, "Whether to clip children to frame bounds", NULL}, - UIDRAWABLE_GETSETTERS, {NULL} }; @@ -420,56 +274,35 @@ PyObject* UIFrame::repr(PyUIFrameObject* self) int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) { - // Parse position using the standardized helper - auto pos_result = PyPositionHelper::parse_position(args, kwds); - - const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", "children", "click", "pos", nullptr }; + //std::cout << "Init called\n"; + const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", nullptr }; float x = 0.0f, y = 0.0f, w = 0.0f, h=0.0f, outline=0.0f; PyObject* fill_color = 0; PyObject* outline_color = 0; - PyObject* children_arg = 0; - PyObject* click_handler = 0; - PyObject* pos_obj = 0; - // Try to parse all arguments including x, y - if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOO", const_cast(keywords), - &x, &y, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler, &pos_obj)) - { - // If pos was provided, it overrides x,y - if (pos_obj && pos_obj != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } - } - else + // First try to parse as (x, y, w, h, ...) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline)) { PyErr_Clear(); // Clear the error // Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...) - const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", "children", "click", nullptr }; - PyObject* pos_arg = nullptr; + PyObject* pos_obj = nullptr; + const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OffOOfOO", const_cast(alt_keywords), - &pos_arg, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast(alt_keywords), + &pos_obj, &w, &h, &fill_color, &outline_color, &outline)) { return -1; } - // Convert position argument to x, y if provided - if (pos_arg && pos_arg != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_arg); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; + // Convert position argument to x, y + PyVectorObject* vec = PyVector::from_arg(pos_obj); + if (!vec) { + PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately"); + return -1; } + x = vec->data.x; + y = vec->data.y; } self->data->box.setPosition(sf::Vector2f(x, y)); @@ -483,70 +316,6 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) if (outline_color && outline_color != Py_None) err_val = UIFrame::set_color_member(self, outline_color, (void*)1); else self->data->box.setOutlineColor(sf::Color(128,128,128,255)); if (err_val) return err_val; - - // Process children argument if provided - if (children_arg && children_arg != Py_None) { - if (!PySequence_Check(children_arg)) { - PyErr_SetString(PyExc_TypeError, "children must be a sequence"); - return -1; - } - - Py_ssize_t len = PySequence_Length(children_arg); - for (Py_ssize_t i = 0; i < len; i++) { - PyObject* child = PySequence_GetItem(children_arg, i); - if (!child) return -1; - - // Check if it's a UIDrawable (Frame, Caption, Sprite, or Grid) - PyObject* frame_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"); - PyObject* caption_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"); - PyObject* sprite_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"); - PyObject* grid_type = PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"); - - if (!PyObject_IsInstance(child, frame_type) && - !PyObject_IsInstance(child, caption_type) && - !PyObject_IsInstance(child, sprite_type) && - !PyObject_IsInstance(child, grid_type)) { - Py_DECREF(child); - PyErr_SetString(PyExc_TypeError, "children must contain only Frame, Caption, Sprite, or Grid objects"); - return -1; - } - - // Get the shared_ptr and add to children - std::shared_ptr drawable = nullptr; - if (PyObject_IsInstance(child, frame_type)) { - drawable = ((PyUIFrameObject*)child)->data; - } else if (PyObject_IsInstance(child, caption_type)) { - drawable = ((PyUICaptionObject*)child)->data; - } else if (PyObject_IsInstance(child, sprite_type)) { - drawable = ((PyUISpriteObject*)child)->data; - } else if (PyObject_IsInstance(child, grid_type)) { - drawable = ((PyUIGridObject*)child)->data; - } - - // Clean up type references - Py_DECREF(frame_type); - Py_DECREF(caption_type); - Py_DECREF(sprite_type); - Py_DECREF(grid_type); - - if (drawable) { - self->data->children->push_back(drawable); - self->data->children_need_sort = true; - } - - Py_DECREF(child); - } - } - - // Process click handler if provided - if (click_handler && click_handler != Py_None) { - if (!PyCallable_Check(click_handler)) { - PyErr_SetString(PyExc_TypeError, "click must be callable"); - return -1; - } - self->data->click_register(click_handler); - } - return 0; } @@ -554,81 +323,58 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds) bool UIFrame::setProperty(const std::string& name, float value) { if (name == "x") { box.setPosition(sf::Vector2f(value, box.getPosition().y)); - markDirty(); return true; } else if (name == "y") { box.setPosition(sf::Vector2f(box.getPosition().x, value)); - markDirty(); return true; } else if (name == "w") { box.setSize(sf::Vector2f(value, box.getSize().y)); - if (use_render_texture) { - // Need to recreate RenderTexture with new size - enableRenderTexture(static_cast(box.getSize().x), - static_cast(box.getSize().y)); - } - markDirty(); return true; } else if (name == "h") { box.setSize(sf::Vector2f(box.getSize().x, value)); - if (use_render_texture) { - // Need to recreate RenderTexture with new size - enableRenderTexture(static_cast(box.getSize().x), - static_cast(box.getSize().y)); - } - markDirty(); return true; } else if (name == "outline") { box.setOutlineThickness(value); - markDirty(); return true; } else if (name == "fill_color.r") { auto color = box.getFillColor(); color.r = std::clamp(static_cast(value), 0, 255); box.setFillColor(color); - markDirty(); return true; } else if (name == "fill_color.g") { auto color = box.getFillColor(); color.g = std::clamp(static_cast(value), 0, 255); box.setFillColor(color); - markDirty(); return true; } else if (name == "fill_color.b") { auto color = box.getFillColor(); color.b = std::clamp(static_cast(value), 0, 255); box.setFillColor(color); - markDirty(); return true; } else if (name == "fill_color.a") { auto color = box.getFillColor(); color.a = std::clamp(static_cast(value), 0, 255); box.setFillColor(color); - markDirty(); return true; } else if (name == "outline_color.r") { auto color = box.getOutlineColor(); color.r = std::clamp(static_cast(value), 0, 255); box.setOutlineColor(color); - markDirty(); return true; } else if (name == "outline_color.g") { auto color = box.getOutlineColor(); color.g = std::clamp(static_cast(value), 0, 255); box.setOutlineColor(color); - markDirty(); return true; } else if (name == "outline_color.b") { auto color = box.getOutlineColor(); color.b = std::clamp(static_cast(value), 0, 255); box.setOutlineColor(color); - markDirty(); return true; } else if (name == "outline_color.a") { auto color = box.getOutlineColor(); color.a = std::clamp(static_cast(value), 0, 255); box.setOutlineColor(color); - markDirty(); return true; } return false; @@ -637,11 +383,9 @@ bool UIFrame::setProperty(const std::string& name, float value) { bool UIFrame::setProperty(const std::string& name, const sf::Color& value) { if (name == "fill_color") { box.setFillColor(value); - markDirty(); return true; } else if (name == "outline_color") { box.setOutlineColor(value); - markDirty(); return true; } return false; @@ -650,16 +394,9 @@ bool UIFrame::setProperty(const std::string& name, const sf::Color& value) { bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) { if (name == "position") { box.setPosition(value); - markDirty(); return true; } else if (name == "size") { box.setSize(value); - if (use_render_texture) { - // Need to recreate RenderTexture with new size - enableRenderTexture(static_cast(value.x), - static_cast(value.y)); - } - markDirty(); return true; } return false; diff --git a/src/UIFrame.h b/src/UIFrame.h index 2d4d23e..a296928 100644 --- a/src/UIFrame.h +++ b/src/UIFrame.h @@ -29,16 +29,10 @@ public: float outline; std::shared_ptr>> children; bool children_need_sort = true; // Dirty flag for z_index sorting optimization - bool clip_children = false; // Whether to clip children to frame bounds void render(sf::Vector2f, sf::RenderTarget&) override final; void move(sf::Vector2f); PyObjectsEnum derived_type() override final; virtual UIDrawable* click_at(sf::Vector2f point) override final; - - // Phase 1 virtual method implementations - sf::FloatRect get_bounds() const override; - void move(float dx, float dy) override; - void resize(float w, float h) override; static PyObject* get_children(PyUIFrameObject* self, void* closure); @@ -48,8 +42,6 @@ public: static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure); static PyObject* get_pos(PyUIFrameObject* self, void* closure); static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure); - static PyObject* get_clip_children(PyUIFrameObject* self, void* closure); - static int set_clip_children(PyUIFrameObject* self, PyObject* value, void* closure); static PyGetSetDef getsetters[]; static PyObject* repr(PyUIFrameObject* self); static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds); @@ -64,9 +56,6 @@ public: bool getProperty(const std::string& name, sf::Vector2f& value) const override; }; -// Forward declaration of methods array -extern PyMethodDef UIFrame_methods[]; - namespace mcrfpydef { static PyTypeObject PyUIFrameType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, @@ -85,7 +74,7 @@ namespace mcrfpydef { //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("docstring"), - .tp_methods = UIFrame_methods, + //.tp_methods = PyUIFrame_methods, //.tp_members = PyUIFrame_members, .tp_getset = UIFrame::getsetters, //.tp_base = NULL, diff --git a/src/UIGrid.cpp b/src/UIGrid.cpp index 2858cea..2a12531 100644 --- a/src/UIGrid.cpp +++ b/src/UIGrid.cpp @@ -1,38 +1,14 @@ #include "UIGrid.h" #include "GameEngine.h" #include "McRFPy_API.h" -#include "PyPositionHelper.h" #include -// UIDrawable methods now in UIBase.h -UIGrid::UIGrid() -: grid_x(0), grid_y(0), zoom(1.0f), center_x(0.0f), center_y(0.0f), ptex(nullptr), - background_color(8, 8, 8, 255) // Default dark gray background -{ - // Initialize entities list - entities = std::make_shared>>(); - - // Initialize box with safe defaults - box.setSize(sf::Vector2f(0, 0)); - box.setPosition(sf::Vector2f(0, 0)); - box.setFillColor(sf::Color(0, 0, 0, 0)); - - // Initialize render texture (small default size) - renderTexture.create(1, 1); - - // Initialize output sprite - output.setTextureRect(sf::IntRect(0, 0, 0, 0)); - output.setPosition(0, 0); - output.setTexture(renderTexture.getTexture()); - - // Points vector starts empty (grid_x * grid_y = 0) -} +UIGrid::UIGrid() {} UIGrid::UIGrid(int gx, int gy, std::shared_ptr _ptex, sf::Vector2f _xy, sf::Vector2f _wh) : grid_x(gx), grid_y(gy), zoom(1.0f), - ptex(_ptex), points(gx * gy), - background_color(8, 8, 8, 255) // Default dark gray background + ptex(_ptex), points(gx * gy) { // Use texture dimensions if available, otherwise use defaults int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH; @@ -68,17 +44,12 @@ void UIGrid::update() {} void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) { - // Check visibility - if (!visible) return; - - // TODO: Apply opacity to output sprite - output.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing // output size can change; update size when drawing output.setTextureRect( sf::IntRect(0, 0, box.getSize().x, box.getSize().y)); - renderTexture.clear(background_color); + renderTexture.clear(sf::Color(8, 8, 8, 255)); // TODO - UIGrid needs a "background color" field // Get cell dimensions - use texture if available, otherwise defaults int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH; @@ -142,13 +113,7 @@ void UIGrid::render(sf::Vector2f offset, sf::RenderTarget& target) // middle layer - entities // disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window) for (auto e : *entities) { - // Skip out-of-bounds entities for performance - // Check if entity is within visible bounds (with 1 cell margin for partially visible entities) - if (e->position.x < left_edge - 1 || e->position.x >= left_edge + width_sq + 1 || - e->position.y < top_edge - 1 || e->position.y >= top_edge + height_sq + 1) { - continue; // Skip this entity as it's not visible - } - + // TODO skip out-of-bounds entities (grid square not visible at all, check for partially on visible grid squares / floating point grid position) //auto drawent = e->cGrid->indexsprite.drawable(); auto& drawent = e->sprite; //drawent.setScale(zoom, zoom); @@ -237,29 +202,6 @@ PyObjectsEnum UIGrid::derived_type() return PyObjectsEnum::UIGRID; } -// Phase 1 implementations -sf::FloatRect UIGrid::get_bounds() const -{ - auto pos = box.getPosition(); - auto size = box.getSize(); - return sf::FloatRect(pos.x, pos.y, size.x, size.y); -} - -void UIGrid::move(float dx, float dy) -{ - box.move(dx, dy); -} - -void UIGrid::resize(float w, float h) -{ - box.setSize(sf::Vector2f(w, h)); - // Recreate render texture with new size - if (w > 0 && h > 0) { - renderTexture.create(static_cast(w), static_cast(h)); - output.setTexture(renderTexture.getTexture()); - } -} - std::shared_ptr UIGrid::getTexture() { return ptex; @@ -267,110 +209,24 @@ std::shared_ptr UIGrid::getTexture() UIDrawable* UIGrid::click_at(sf::Vector2f point) { - // Check grid bounds first - if (!box.getGlobalBounds().contains(point)) { - return nullptr; + if (click_callable) + { + if(box.getGlobalBounds().contains(point)) return this; } - - // Transform to local coordinates - sf::Vector2f localPoint = point - box.getPosition(); - - // Get cell dimensions - int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH; - int cell_height = ptex ? ptex->sprite_height : DEFAULT_CELL_HEIGHT; - - // Calculate visible area parameters (from render function) - float center_x_sq = center_x / cell_width; - float center_y_sq = center_y / cell_height; - float width_sq = box.getSize().x / (cell_width * zoom); - float height_sq = box.getSize().y / (cell_height * zoom); - - 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; - - // 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 - if (entities) { - for (auto it = entities->rbegin(); it != entities->rend(); ++it) { - auto& entity = *it; - if (!entity || !entity->sprite.visible) continue; - - // Check if click is within entity's grid cell - // Entities occupy a 1x1 grid cell centered on their position - float dx = grid_x - entity->position.x; - float dy = grid_y - entity->position.y; - - if (dx >= -0.5f && dx < 0.5f && dy >= -0.5f && dy < 0.5f) { - // Click is within the entity's cell - // Check if entity sprite has a click handler - // For now, we return the entity's sprite as the click target - // Note: UIEntity doesn't derive from UIDrawable, so we check its sprite - if (entity->sprite.click_callable) { - return &entity->sprite; - } - } - } - } - - // No entity handled it, check if grid itself has handler - if (click_callable) { - return this; - } - - return nullptr; + return NULL; } int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) { - int grid_x = 0, grid_y = 0; // Default to 0x0 grid + int grid_x, grid_y; PyObject* textureObj = Py_None; + //float box_x, box_y, box_w, box_h; PyObject* pos = NULL; PyObject* size = NULL; - PyObject* grid_size_obj = NULL; - - static const char* keywords[] = {"grid_x", "grid_y", "texture", "pos", "size", "grid_size", NULL}; - // First try parsing with keywords - if (PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", const_cast(keywords), - &grid_x, &grid_y, &textureObj, &pos, &size, &grid_size_obj)) { - // If grid_size is provided, use it to override grid_x and grid_y - if (grid_size_obj && grid_size_obj != Py_None) { - if (PyTuple_Check(grid_size_obj) && PyTuple_Size(grid_size_obj) == 2) { - PyObject* x_obj = PyTuple_GetItem(grid_size_obj, 0); - PyObject* y_obj = PyTuple_GetItem(grid_size_obj, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - grid_x = PyLong_AsLong(x_obj); - grid_y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "grid_size tuple must contain integers"); - return -1; - } - } else if (PyList_Check(grid_size_obj) && PyList_Size(grid_size_obj) == 2) { - PyObject* x_obj = PyList_GetItem(grid_size_obj, 0); - PyObject* y_obj = PyList_GetItem(grid_size_obj, 1); - if (PyLong_Check(x_obj) && PyLong_Check(y_obj)) { - grid_x = PyLong_AsLong(x_obj); - grid_y = PyLong_AsLong(y_obj); - } else { - PyErr_SetString(PyExc_TypeError, "grid_size list must contain integers"); - return -1; - } - } else { - PyErr_SetString(PyExc_TypeError, "grid_size must be a tuple or list of two integers"); - return -1; - } - } - } else { - // Clear error and try parsing without keywords (backward compatibility) - PyErr_Clear(); - if (!PyArg_ParseTuple(args, "|iiOOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { - return -1; // If parsing fails, return an error - } + //if (!PyArg_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) { + if (!PyArg_ParseTuple(args, "ii|OOO", &grid_x, &grid_y, &textureObj, &pos, &size)) { + return -1; // If parsing fails, return an error } // Default position and size if not provided @@ -619,20 +475,13 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) { return (PyObject*)obj; } -PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) +PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* o) { - // Use the standardized position parser - auto result = PyPositionHelper::parse_position_int(args, kwds); - - if (!result.has_position) { - PyPositionHelper::set_position_int_error(); + int x, y; + if (!PyArg_ParseTuple(o, "ii", &x, &y)) { + PyErr_SetString(PyExc_TypeError, "UIGrid.at requires two integer arguments: (x, y)"); return NULL; } - - int x = result.x; - int y = result.y; - - // Range validation if (x < 0 || x >= self->data->grid_x) { PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)"); return NULL; @@ -651,43 +500,11 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds) return (PyObject*)obj; } -PyObject* UIGrid::get_background_color(PyUIGridObject* self, void* closure) -{ - auto& color = self->data->background_color; - auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"); - PyObject* args = Py_BuildValue("(iiii)", color.r, color.g, color.b, color.a); - PyObject* obj = PyObject_CallObject((PyObject*)type, args); - Py_DECREF(args); - Py_DECREF(type); - return obj; -} - -int UIGrid::set_background_color(PyUIGridObject* self, PyObject* value, void* closure) -{ - if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Color"))) { - PyErr_SetString(PyExc_TypeError, "background_color must be a Color object"); - return -1; - } - - PyColorObject* color = (PyColorObject*)value; - self->data->background_color = color->data; - return 0; -} - PyMethodDef UIGrid::methods[] = { - {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, + {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS}, {NULL, NULL, 0, NULL} }; -// Define the PyObjectType alias for the macros -typedef PyUIGridObject PyObjectType; - -// Combined methods array -PyMethodDef UIGrid_all_methods[] = { - UIDRAWABLE_METHODS, - {"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS}, - {NULL} // Sentinel -}; PyGetSetDef UIGrid::getsetters[] = { @@ -712,10 +529,7 @@ PyGetSetDef UIGrid::getsetters[] = { {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIGRID}, {"texture", (getter)UIGrid::get_texture, NULL, "Texture of the grid", NULL}, //TODO 7DRL-day2-item5 - {"background_color", (getter)UIGrid::get_background_color, (setter)UIGrid::set_background_color, "Background color of the grid", NULL}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIGRID}, - {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UIGRID}, - UIDRAWABLE_GETSETTERS, {NULL} /* Sentinel */ }; @@ -1026,6 +840,184 @@ PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, P return (PyObject*)self; } +int UIEntityCollection::setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value) { + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); + return -1; + } + + // Handle negative indexing + while (index < 0) index += list->size(); + + // Bounds check + if (index >= list->size()) { + PyErr_SetString(PyExc_IndexError, "EntityCollection assignment index out of range"); + return -1; + } + + // Get iterator to the target position + auto it = list->begin(); + std::advance(it, index); + + // Handle deletion + if (value == NULL) { + // Clear grid reference from the entity being removed + (*it)->grid = nullptr; + list->erase(it); + return 0; + } + + // Type checking - must be an Entity + if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { + PyErr_SetString(PyExc_TypeError, "EntityCollection can only contain Entity objects"); + return -1; + } + + // Get the C++ object from the Python object + PyUIEntityObject* entity = (PyUIEntityObject*)value; + if (!entity->data) { + PyErr_SetString(PyExc_RuntimeError, "Invalid Entity object"); + return -1; + } + + // Clear grid reference from the old entity + (*it)->grid = nullptr; + + // Replace the element and set grid reference + *it = entity->data; + entity->data->grid = self->grid; + + return 0; +} + +int UIEntityCollection::contains(PyUIEntityCollectionObject* self, PyObject* value) { + auto list = self->data.get(); + if (!list) { + PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer"); + return -1; + } + + // Type checking - must be an Entity + if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { + // Not an Entity, so it can't be in the collection + return 0; + } + + // Get the C++ object from the Python object + PyUIEntityObject* entity = (PyUIEntityObject*)value; + if (!entity->data) { + return 0; + } + + // Search for the object by comparing C++ pointers + for (const auto& ent : *list) { + if (ent.get() == entity->data.get()) { + return 1; // Found + } + } + + return 0; // Not found +} + +PyObject* UIEntityCollection::concat(PyUIEntityCollectionObject* self, PyObject* other) { + // Create a new Python list containing elements from both collections + if (!PySequence_Check(other)) { + PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection"); + return NULL; + } + + Py_ssize_t self_len = self->data->size(); + Py_ssize_t other_len = PySequence_Length(other); + if (other_len == -1) { + return NULL; // Error already set + } + + PyObject* result_list = PyList_New(self_len + other_len); + if (!result_list) { + return NULL; + } + + // Add all elements from self + Py_ssize_t idx = 0; + for (const auto& entity : *self->data) { + auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"); + auto obj = (PyUIEntityObject*)type->tp_alloc(type, 0); + if (obj) { + obj->data = entity; + PyList_SET_ITEM(result_list, idx, (PyObject*)obj); // Steals reference + } else { + Py_DECREF(result_list); + Py_DECREF(type); + return NULL; + } + Py_DECREF(type); + idx++; + } + + // Add all elements from other + for (Py_ssize_t i = 0; i < other_len; i++) { + PyObject* item = PySequence_GetItem(other, i); + if (!item) { + Py_DECREF(result_list); + return NULL; + } + PyList_SET_ITEM(result_list, self_len + i, item); // Steals reference + } + + return result_list; +} + +PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, PyObject* other) { + if (!PySequence_Check(other)) { + PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to EntityCollection"); + return NULL; + } + + // First, validate ALL items in the sequence before modifying anything + Py_ssize_t other_len = PySequence_Length(other); + if (other_len == -1) { + return NULL; // Error already set + } + + // Validate all items first + for (Py_ssize_t i = 0; i < other_len; i++) { + PyObject* item = PySequence_GetItem(other, i); + if (!item) { + return NULL; + } + + // Type check + if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Entity"))) { + Py_DECREF(item); + PyErr_Format(PyExc_TypeError, + "EntityCollection can only contain Entity objects; " + "got %s at index %zd", Py_TYPE(item)->tp_name, i); + return NULL; + } + Py_DECREF(item); + } + + // All items validated, now we can safely add them + for (Py_ssize_t i = 0; i < other_len; i++) { + PyObject* item = PySequence_GetItem(other, i); + if (!item) { + return NULL; // Shouldn't happen, but be safe + } + + // Use the existing append method which handles grid references + PyObject* result = append(self, item); + Py_DECREF(item); + + if (!result) { + return NULL; // append() failed + } + Py_DECREF(result); // append returns Py_None + } + + Py_INCREF(self); + return (PyObject*)self; +} PySequenceMethods UIEntityCollection::sqmethods = { .sq_length = (lenfunc)UIEntityCollection::len, @@ -1481,22 +1473,6 @@ bool UIGrid::setProperty(const std::string& name, float value) { z_index = static_cast(value); return true; } - else if (name == "background_color.r") { - background_color.r = static_cast(std::max(0.0f, std::min(255.0f, value))); - return true; - } - else if (name == "background_color.g") { - background_color.g = static_cast(std::max(0.0f, std::min(255.0f, value))); - return true; - } - else if (name == "background_color.b") { - background_color.b = static_cast(std::max(0.0f, std::min(255.0f, value))); - return true; - } - else if (name == "background_color.a") { - background_color.a = static_cast(std::max(0.0f, std::min(255.0f, value))); - return true; - } return false; } @@ -1552,22 +1528,6 @@ bool UIGrid::getProperty(const std::string& name, float& value) const { value = static_cast(z_index); return true; } - else if (name == "background_color.r") { - value = static_cast(background_color.r); - return true; - } - else if (name == "background_color.g") { - value = static_cast(background_color.g); - return true; - } - else if (name == "background_color.b") { - value = static_cast(background_color.b); - return true; - } - else if (name == "background_color.a") { - value = static_cast(background_color.a); - return true; - } return false; } diff --git a/src/UIGrid.h b/src/UIGrid.h index 8d46fbd..a167c0b 100644 --- a/src/UIGrid.h +++ b/src/UIGrid.h @@ -34,11 +34,6 @@ public: PyObjectsEnum derived_type() override final; //void setSprite(int); virtual UIDrawable* click_at(sf::Vector2f point) override final; - - // Phase 1 virtual method implementations - sf::FloatRect get_bounds() const override; - void move(float dx, float dy) override; - void resize(float w, float h) override; int grid_x, grid_y; //int grid_size; // grid sizes are implied by IndexTexture now @@ -51,9 +46,6 @@ public: std::vector points; std::shared_ptr>> entities; - // Background rendering - sf::Color background_color; - // Property system for animations bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, const sf::Vector2f& value) override; @@ -73,9 +65,7 @@ public: static PyObject* get_float_member(PyUIGridObject* self, void* closure); static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure); static PyObject* get_texture(PyUIGridObject* self, void* closure); - static PyObject* get_background_color(PyUIGridObject* self, void* closure); - static int set_background_color(PyUIGridObject* self, PyObject* value, void* closure); - static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds); + static PyObject* py_at(PyUIGridObject* self, PyObject* o); static PyMethodDef methods[]; static PyGetSetDef getsetters[]; static PyObject* get_children(PyUIGridObject* self, void* closure); @@ -128,9 +118,6 @@ public: }; -// Forward declaration of methods array -extern PyMethodDef UIGrid_all_methods[]; - namespace mcrfpydef { static PyTypeObject PyUIGridType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, @@ -150,7 +137,7 @@ namespace mcrfpydef { //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("docstring"), - .tp_methods = UIGrid_all_methods, + .tp_methods = UIGrid::methods, //.tp_members = UIGrid::members, .tp_getset = UIGrid::getsetters, //.tp_base = NULL, diff --git a/src/UISprite.cpp b/src/UISprite.cpp index 33cc4f2..e69d37e 100644 --- a/src/UISprite.cpp +++ b/src/UISprite.cpp @@ -1,8 +1,6 @@ #include "UISprite.h" #include "GameEngine.h" #include "PyVector.h" -#include "PyPositionHelper.h" -// UIDrawable methods now in UIBase.h UIDrawable* UISprite::click_at(sf::Vector2f point) { @@ -13,13 +11,7 @@ UIDrawable* UISprite::click_at(sf::Vector2f point) return NULL; } -UISprite::UISprite() -: sprite_index(0), ptex(nullptr) -{ - // Initialize sprite to safe defaults - sprite.setPosition(0.0f, 0.0f); - sprite.setScale(1.0f, 1.0f); -} +UISprite::UISprite() {} UISprite::UISprite(std::shared_ptr _ptex, int _sprite_index, sf::Vector2f _pos, float _scale) : ptex(_ptex), sprite_index(_sprite_index) @@ -38,21 +30,9 @@ void UISprite::render(sf::Vector2f offset) void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target) { - // Check visibility - if (!visible) return; - - // Apply opacity - auto color = sprite.getColor(); - color.a = static_cast(255 * opacity); - sprite.setColor(color); - sprite.move(offset); target.draw(sprite); sprite.move(-offset); - - // Restore original alpha - color.a = 255; - sprite.setColor(color); } void UISprite::setPosition(sf::Vector2f pos) @@ -104,28 +84,6 @@ PyObjectsEnum UISprite::derived_type() return PyObjectsEnum::UISPRITE; } -// Phase 1 implementations -sf::FloatRect UISprite::get_bounds() const -{ - return sprite.getGlobalBounds(); -} - -void UISprite::move(float dx, float dy) -{ - sprite.move(dx, dy); -} - -void UISprite::resize(float w, float h) -{ - // Calculate scale factors to achieve target size - auto bounds = sprite.getLocalBounds(); - if (bounds.width > 0 && bounds.height > 0) { - float scaleX = w / bounds.width; - float scaleY = h / bounds.height; - sprite.setScale(scaleX, scaleY); - } -} - PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure) { auto member_ptr = reinterpret_cast(closure); @@ -268,15 +226,6 @@ int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure) return 0; } -// Define the PyObjectType alias for the macros -typedef PyUISpriteObject PyObjectType; - -// Method definitions -PyMethodDef UISprite_methods[] = { - UIDRAWABLE_METHODS, - {NULL} // Sentinel -}; - PyGetSetDef UISprite::getsetters[] = { {"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0}, {"y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Y coordinate of top-left corner", (void*)1}, @@ -288,9 +237,7 @@ PyGetSetDef UISprite::getsetters[] = { {"texture", (getter)UISprite::get_texture, (setter)UISprite::set_texture, "Texture object", NULL}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UISPRITE}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE}, - {"name", (getter)UIDrawable::get_name, (setter)UIDrawable::set_name, "Name for finding elements", (void*)PyObjectsEnum::UISPRITE}, {"pos", (getter)UISprite::get_pos, (setter)UISprite::set_pos, "Position as a Vector", NULL}, - UIDRAWABLE_GETSETTERS, {NULL} }; @@ -310,47 +257,33 @@ PyObject* UISprite::repr(PyUISpriteObject* self) int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) { - static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", "click", "pos", nullptr }; + //std::cout << "Init called\n"; + static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", nullptr }; float x = 0.0f, y = 0.0f, scale = 1.0f; int sprite_index = 0; PyObject* texture = NULL; - PyObject* click_handler = NULL; - PyObject* pos_obj = NULL; - // Try to parse all arguments with keywords - if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO", - const_cast(keywords), &x, &y, &texture, &sprite_index, &scale, &click_handler, &pos_obj)) - { - // If pos was provided, it overrides x,y - if (pos_obj && pos_obj != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos_obj); - if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); - return -1; - } - x = vec->data.x; - y = vec->data.y; - } - } - else + // First try to parse as (x, y, texture, ...) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif", + const_cast(keywords), &x, &y, &texture, &sprite_index, &scale)) { PyErr_Clear(); // Clear the error - // Try alternative: first arg is pos tuple/Vector - const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", "click", nullptr }; - PyObject* pos = NULL; + // Try to parse as ((x,y), texture, ...) or (Vector, texture, ...) + PyObject* pos_obj = nullptr; + const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr }; - if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifO", const_cast(alt_keywords), - &pos, &texture, &sprite_index, &scale, &click_handler)) + if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast(alt_keywords), + &pos_obj, &texture, &sprite_index, &scale)) { return -1; } // Convert position argument to x, y - if (pos && pos != Py_None) { - PyVectorObject* vec = PyVector::from_arg(pos); + if (pos_obj) { + PyVectorObject* vec = PyVector::from_arg(pos_obj); if (!vec) { - PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)"); + PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately"); return -1; } x = vec->data.x; @@ -379,15 +312,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds) self->data = std::make_shared(texture_ptr, sprite_index, sf::Vector2f(x, y), scale); self->data->setPosition(sf::Vector2f(x, y)); - // Process click handler if provided - if (click_handler && click_handler != Py_None) { - if (!PyCallable_Check(click_handler)) { - PyErr_SetString(PyExc_TypeError, "click must be callable"); - return -1; - } - self->data->click_register(click_handler); - } - return 0; } diff --git a/src/UISprite.h b/src/UISprite.h index 8043282..060b2c2 100644 --- a/src/UISprite.h +++ b/src/UISprite.h @@ -42,11 +42,6 @@ public: PyObjectsEnum derived_type() override final; - // Phase 1 virtual method implementations - sf::FloatRect get_bounds() const override; - void move(float dx, float dy) override; - void resize(float w, float h) override; - // Property system for animations bool setProperty(const std::string& name, float value) override; bool setProperty(const std::string& name, int value) override; @@ -68,9 +63,6 @@ public: }; -// Forward declaration of methods array -extern PyMethodDef UISprite_methods[]; - namespace mcrfpydef { static PyTypeObject PyUISpriteType = { .ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0}, @@ -91,7 +83,7 @@ namespace mcrfpydef { //.tp_iternext .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("docstring"), - .tp_methods = UISprite_methods, + //.tp_methods = PyUIFrame_methods, //.tp_members = PyUIFrame_members, .tp_getset = UISprite::getsetters, //.tp_base = NULL, diff --git a/src/main.cpp b/src/main.cpp index df6aaf3..e0e9835 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -41,9 +41,6 @@ int run_game_engine(const McRogueFaceConfig& config) { GameEngine g(config); g.run(); - if (Py_IsInitialized()) { - McRFPy_API::api_shutdown(); - } return 0; } @@ -105,7 +102,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv // Continue to interactive mode below } else { int result = PyRun_SimpleString(config.python_command.c_str()); - McRFPy_API::api_shutdown(); + Py_Finalize(); delete engine; return result; } @@ -124,7 +121,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv run_module_code += "runpy.run_module('" + config.python_module + "', run_name='__main__', alter_sys=True)\n"; int result = PyRun_SimpleString(run_module_code.c_str()); - McRFPy_API::api_shutdown(); + Py_Finalize(); delete engine; return result; } @@ -182,7 +179,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv // Run the game engine after script execution engine->run(); - McRFPy_API::api_shutdown(); + Py_Finalize(); delete engine; return result; } @@ -190,14 +187,14 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv // Interactive Python interpreter (only if explicitly requested with -i) Py_InspectFlag = 1; PyRun_InteractiveLoop(stdin, ""); - McRFPy_API::api_shutdown(); + Py_Finalize(); delete engine; return 0; } else if (!config.exec_scripts.empty()) { // With --exec, run the game engine after scripts execute engine->run(); - McRFPy_API::api_shutdown(); + Py_Finalize(); delete engine; return 0; } diff --git a/tests/grid_at_argument_test.py b/tests/grid_at_argument_test.py deleted file mode 100644 index 14e9485..0000000 --- a/tests/grid_at_argument_test.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 -"""Test Grid.at() method with various argument formats""" - -import mcrfpy -import sys - -def test_grid_at_arguments(): - """Test that Grid.at() accepts all required argument formats""" - print("Testing Grid.at() argument formats...") - - # Create a test scene - mcrfpy.createScene("test") - - # Create a grid - grid = mcrfpy.Grid(10, 10) - ui = mcrfpy.sceneUI("test") - ui.append(grid) - - success_count = 0 - total_tests = 4 - - # Test 1: Two positional arguments (x, y) - try: - point1 = grid.at(5, 5) - print("✓ Test 1 PASSED: grid.at(5, 5)") - success_count += 1 - except Exception as e: - print(f"✗ Test 1 FAILED: grid.at(5, 5) - {e}") - - # Test 2: Single tuple argument (x, y) - try: - point2 = grid.at((3, 3)) - print("✓ Test 2 PASSED: grid.at((3, 3))") - success_count += 1 - except Exception as e: - print(f"✗ Test 2 FAILED: grid.at((3, 3)) - {e}") - - # Test 3: Keyword arguments x=x, y=y - try: - point3 = grid.at(x=7, y=2) - print("✓ Test 3 PASSED: grid.at(x=7, y=2)") - success_count += 1 - except Exception as e: - print(f"✗ Test 3 FAILED: grid.at(x=7, y=2) - {e}") - - # Test 4: pos keyword argument pos=(x, y) - try: - point4 = grid.at(pos=(1, 8)) - print("✓ Test 4 PASSED: grid.at(pos=(1, 8))") - success_count += 1 - except Exception as e: - print(f"✗ Test 4 FAILED: grid.at(pos=(1, 8)) - {e}") - - # Test error cases - print("\nTesting error cases...") - - # Test 5: Invalid - mixing pos with x/y - try: - grid.at(x=1, pos=(2, 2)) - print("✗ Test 5 FAILED: Should have raised error for mixing pos and x/y") - except TypeError as e: - print(f"✓ Test 5 PASSED: Correctly rejected mixing pos and x/y - {e}") - - # Test 6: Invalid - out of range - try: - grid.at(15, 15) - print("✗ Test 6 FAILED: Should have raised error for out of range") - except ValueError as e: - print(f"✓ Test 6 PASSED: Correctly rejected out of range - {e}") - - # Test 7: Verify all points are valid GridPoint objects - try: - # Check that we can set walkable on all returned points - if 'point1' in locals(): - point1.walkable = True - if 'point2' in locals(): - point2.walkable = False - if 'point3' in locals(): - point3.color = mcrfpy.Color(255, 0, 0) - if 'point4' in locals(): - point4.tilesprite = 5 - print("✓ All returned GridPoint objects are valid") - except Exception as e: - print(f"✗ GridPoint objects validation failed: {e}") - - print(f"\nSummary: {success_count}/{total_tests} tests passed") - - if success_count == total_tests: - print("ALL TESTS PASSED!") - sys.exit(0) - else: - print("SOME TESTS FAILED!") - sys.exit(1) - -# Run timer callback to execute tests after render loop starts -def run_test(elapsed): - test_grid_at_arguments() - -# Set a timer to run the test -mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh deleted file mode 100755 index 85e7c7f..0000000 --- a/tests/run_all_tests.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# Run all tests and check for failures - -TESTS=( - "test_click_init.py" - "test_drawable_base.py" - "test_frame_children.py" - "test_sprite_texture_swap.py" - "test_timer_object.py" - "test_timer_object_fixed.py" -) - -echo "Running all tests..." -echo "====================" - -failed=0 -passed=0 - -for test in "${TESTS[@]}"; do - echo -n "Running $test... " - if timeout 5 ./mcrogueface --headless --exec ../tests/$test > /tmp/test_output.txt 2>&1; then - if grep -q "FAIL\|✗" /tmp/test_output.txt; then - echo "FAILED" - echo "Output:" - cat /tmp/test_output.txt | grep -E "✗|FAIL|Error|error" | head -10 - ((failed++)) - else - echo "PASSED" - ((passed++)) - fi - else - echo "TIMEOUT/CRASH" - ((failed++)) - fi -done - -echo "====================" -echo "Total: $((passed + failed)) tests" -echo "Passed: $passed" -echo "Failed: $failed" - -exit $failed \ No newline at end of file diff --git a/tests/test_frame_clipping.py b/tests/test_frame_clipping.py deleted file mode 100644 index 48cad99..0000000 --- a/tests/test_frame_clipping.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env python3 -"""Test UIFrame clipping functionality""" - -import mcrfpy -from mcrfpy import Color, Frame, Caption, Vector -import sys - -def test_clipping(runtime): - """Test that clip_children property works correctly""" - mcrfpy.delTimer("test_clipping") - - print("Testing UIFrame clipping functionality...") - - # Create test scene - scene = mcrfpy.sceneUI("test") - - # Create parent frame with clipping disabled (default) - parent1 = Frame(50, 50, 200, 150, - fill_color=Color(100, 100, 200), - outline_color=Color(255, 255, 255), - outline=2) - parent1.name = "parent1" - scene.append(parent1) - - # Create parent frame with clipping enabled - parent2 = Frame(300, 50, 200, 150, - fill_color=Color(200, 100, 100), - outline_color=Color(255, 255, 255), - outline=2) - parent2.name = "parent2" - parent2.clip_children = True - scene.append(parent2) - - # Add captions to both frames - caption1 = Caption(10, 10, "This text should overflow the frame bounds") - caption1.font_size = 16 - caption1.fill_color = Color(255, 255, 255) - parent1.children.append(caption1) - - caption2 = Caption(10, 10, "This text should be clipped to frame bounds") - caption2.font_size = 16 - caption2.fill_color = Color(255, 255, 255) - parent2.children.append(caption2) - - # Add child frames that extend beyond parent bounds - child1 = Frame(150, 100, 100, 100, - fill_color=Color(50, 255, 50), - outline_color=Color(0, 0, 0), - outline=1) - parent1.children.append(child1) - - child2 = Frame(150, 100, 100, 100, - fill_color=Color(50, 255, 50), - outline_color=Color(0, 0, 0), - outline=1) - parent2.children.append(child2) - - # Add caption to show clip state - status = Caption(50, 250, - f"Left frame: clip_children={parent1.clip_children}\n" - f"Right frame: clip_children={parent2.clip_children}") - status.font_size = 14 - status.fill_color = Color(255, 255, 255) - scene.append(status) - - # Add instructions - instructions = Caption(50, 300, - "Left: Children should overflow (no clipping)\n" - "Right: Children should be clipped to frame bounds\n" - "Press 'c' to toggle clipping on left frame") - instructions.font_size = 12 - instructions.fill_color = Color(200, 200, 200) - scene.append(instructions) - - # Take screenshot - from mcrfpy import Window, automation - automation.screenshot("frame_clipping_test.png") - - print(f"Parent1 clip_children: {parent1.clip_children}") - print(f"Parent2 clip_children: {parent2.clip_children}") - - # Test toggling clip_children - parent1.clip_children = True - print(f"After toggle - Parent1 clip_children: {parent1.clip_children}") - - # Verify the property setter works - try: - parent1.clip_children = "not a bool" # Should raise TypeError - print("ERROR: clip_children accepted non-boolean value") - except TypeError as e: - print(f"PASS: clip_children correctly rejected non-boolean: {e}") - - # Test with animations - def animate_frames(runtime): - mcrfpy.delTimer("animate") - # Animate child frames to show clipping in action - # Note: For now, just move the frames manually to demonstrate clipping - parent1.children[1].x = 50 # Move child frame - parent2.children[1].x = 50 # Move child frame - - # Take another screenshot after starting animation - mcrfpy.setTimer("screenshot2", take_second_screenshot, 500) - - def take_second_screenshot(runtime): - mcrfpy.delTimer("screenshot2") - automation.screenshot("frame_clipping_animated.png") - print("\nTest completed successfully!") - print("Screenshots saved:") - print(" - frame_clipping_test.png (initial state)") - print(" - frame_clipping_animated.png (with animation)") - sys.exit(0) - - # Start animation after a short delay - mcrfpy.setTimer("animate", animate_frames, 100) - -# Main execution -print("Creating test scene...") -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Set up keyboard handler to toggle clipping -def handle_keypress(key, modifiers): - if key == "c": - scene = mcrfpy.sceneUI("test") - parent1 = scene[0] # First frame - parent1.clip_children = not parent1.clip_children - print(f"Toggled parent1 clip_children to: {parent1.clip_children}") - -mcrfpy.keypressScene(handle_keypress) - -# Schedule the test -mcrfpy.setTimer("test_clipping", test_clipping, 100) - -print("Test scheduled, running...") \ No newline at end of file diff --git a/tests/test_frame_clipping_advanced.py b/tests/test_frame_clipping_advanced.py deleted file mode 100644 index 3c3d324..0000000 --- a/tests/test_frame_clipping_advanced.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python3 -"""Advanced test for UIFrame clipping with nested frames""" - -import mcrfpy -from mcrfpy import Color, Frame, Caption, Vector -import sys - -def test_nested_clipping(runtime): - """Test nested frames with clipping""" - mcrfpy.delTimer("test_nested_clipping") - - print("Testing advanced UIFrame clipping with nested frames...") - - # Create test scene - scene = mcrfpy.sceneUI("test") - - # Create outer frame with clipping enabled - outer = Frame(50, 50, 400, 300, - fill_color=Color(50, 50, 150), - outline_color=Color(255, 255, 255), - outline=3) - outer.name = "outer" - outer.clip_children = True - scene.append(outer) - - # Create inner frame that extends beyond outer bounds - inner = Frame(200, 150, 300, 200, - fill_color=Color(150, 50, 50), - outline_color=Color(255, 255, 0), - outline=2) - inner.name = "inner" - inner.clip_children = True # Also enable clipping on inner frame - outer.children.append(inner) - - # Add content to inner frame that extends beyond its bounds - for i in range(5): - caption = Caption(10, 30 * i, f"Line {i+1}: This text should be double-clipped") - caption.font_size = 14 - caption.fill_color = Color(255, 255, 255) - inner.children.append(caption) - - # Add a child frame to inner that extends way out - deeply_nested = Frame(250, 100, 200, 150, - fill_color=Color(50, 150, 50), - outline_color=Color(255, 0, 255), - outline=2) - deeply_nested.name = "deeply_nested" - inner.children.append(deeply_nested) - - # Add status text - status = Caption(50, 380, - "Nested clipping test:\n" - "- Blue outer frame clips red inner frame\n" - "- Red inner frame clips green deeply nested frame\n" - "- All text should be clipped to frame bounds") - status.font_size = 12 - status.fill_color = Color(200, 200, 200) - scene.append(status) - - # Test render texture size handling - print(f"Outer frame size: {outer.w}x{outer.h}") - print(f"Inner frame size: {inner.w}x{inner.h}") - - # Dynamically resize frames to test RenderTexture recreation - def resize_test(runtime): - mcrfpy.delTimer("resize_test") - print("Resizing frames to test RenderTexture recreation...") - outer.w = 450 - outer.h = 350 - inner.w = 350 - inner.h = 250 - print(f"New outer frame size: {outer.w}x{outer.h}") - print(f"New inner frame size: {inner.w}x{inner.h}") - - # Take screenshot after resize - mcrfpy.setTimer("screenshot_resize", take_resize_screenshot, 500) - - def take_resize_screenshot(runtime): - mcrfpy.delTimer("screenshot_resize") - from mcrfpy import automation - automation.screenshot("frame_clipping_resized.png") - print("\nAdvanced test completed!") - print("Screenshots saved:") - print(" - frame_clipping_resized.png (after resize)") - sys.exit(0) - - # Take initial screenshot - from mcrfpy import automation - automation.screenshot("frame_clipping_nested.png") - print("Initial screenshot saved: frame_clipping_nested.png") - - # Schedule resize test - mcrfpy.setTimer("resize_test", resize_test, 1000) - -# Main execution -print("Creating advanced test scene...") -mcrfpy.createScene("test") -mcrfpy.setScene("test") - -# Schedule the test -mcrfpy.setTimer("test_nested_clipping", test_nested_clipping, 100) - -print("Advanced test scheduled, running...") \ No newline at end of file diff --git a/tests/test_grid_background.py b/tests/test_grid_background.py deleted file mode 100644 index c79cf8e..0000000 --- a/tests/test_grid_background.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -"""Test Grid background color functionality""" - -import mcrfpy -import sys - -def test_grid_background(): - """Test Grid background color property""" - print("Testing Grid Background Color...") - - # Create a test scene - mcrfpy.createScene("test") - ui = mcrfpy.sceneUI("test") - - # Create a grid with default background - grid = mcrfpy.Grid(20, 15, grid_size=(20, 15)) - grid.x = 50 - grid.y = 50 - grid.w = 400 - grid.h = 300 - ui.append(grid) - - # Add some tiles to see the background better - for x in range(5, 15): - for y in range(5, 10): - point = grid.at(x, y) - point.color = mcrfpy.Color(100, 150, 100) - - # Add UI to show current background color - info_frame = mcrfpy.Frame(500, 50, 200, 150, - fill_color=mcrfpy.Color(40, 40, 40), - outline_color=mcrfpy.Color(200, 200, 200), - outline=2) - ui.append(info_frame) - - color_caption = mcrfpy.Caption(510, 60, "Background Color:") - color_caption.font_size = 14 - color_caption.fill_color = mcrfpy.Color(255, 255, 255) - info_frame.children.append(color_caption) - - color_display = mcrfpy.Caption(510, 80, "") - color_display.font_size = 12 - color_display.fill_color = mcrfpy.Color(200, 200, 200) - info_frame.children.append(color_display) - - # Activate the scene - mcrfpy.setScene("test") - - def run_tests(dt): - """Run background color tests""" - mcrfpy.delTimer("run_tests") - - print("\nTest 1: Default background color") - default_color = grid.background_color - print(f"Default: R={default_color.r}, G={default_color.g}, B={default_color.b}, A={default_color.a}") - color_display.text = f"R:{default_color.r} G:{default_color.g} B:{default_color.b}" - - def test_set_color(dt): - mcrfpy.delTimer("test_set") - print("\nTest 2: Set background to blue") - grid.background_color = mcrfpy.Color(20, 40, 100) - new_color = grid.background_color - print(f"✓ Set to: R={new_color.r}, G={new_color.g}, B={new_color.b}") - color_display.text = f"R:{new_color.r} G:{new_color.g} B:{new_color.b}" - - def test_animation(dt): - mcrfpy.delTimer("test_anim") - print("\nTest 3: Manual color cycling") - # Manually change color to test property is working - colors = [ - mcrfpy.Color(200, 20, 20), # Red - mcrfpy.Color(20, 200, 20), # Green - mcrfpy.Color(20, 20, 200), # Blue - ] - - color_index = [0] # Use list to allow modification in nested function - - def cycle_red(dt): - mcrfpy.delTimer("cycle_0") - grid.background_color = colors[0] - c = grid.background_color - color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" - print(f"✓ Set to Red: R={c.r}, G={c.g}, B={c.b}") - - def cycle_green(dt): - mcrfpy.delTimer("cycle_1") - grid.background_color = colors[1] - c = grid.background_color - color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" - print(f"✓ Set to Green: R={c.r}, G={c.g}, B={c.b}") - - def cycle_blue(dt): - mcrfpy.delTimer("cycle_2") - grid.background_color = colors[2] - c = grid.background_color - color_display.text = f"R:{c.r} G:{c.g} B:{c.b}" - print(f"✓ Set to Blue: R={c.r}, G={c.g}, B={c.b}") - - # Cycle through colors - mcrfpy.setTimer("cycle_0", cycle_red, 100) - mcrfpy.setTimer("cycle_1", cycle_green, 400) - mcrfpy.setTimer("cycle_2", cycle_blue, 700) - - def test_complete(dt): - mcrfpy.delTimer("complete") - print("\nTest 4: Final color check") - final_color = grid.background_color - print(f"Final: R={final_color.r}, G={final_color.g}, B={final_color.b}") - - print("\n✓ Grid background color tests completed!") - print("- Default background color works") - print("- Setting background color works") - print("- Color cycling works") - - sys.exit(0) - - # Schedule tests - mcrfpy.setTimer("test_set", test_set_color, 1000) - mcrfpy.setTimer("test_anim", test_animation, 2000) - mcrfpy.setTimer("complete", test_complete, 4500) - - # Start tests - mcrfpy.setTimer("run_tests", run_tests, 100) - -if __name__ == "__main__": - test_grid_background() \ No newline at end of file diff --git a/tests/unified_click_example.cpp b/tests/unified_click_example.cpp deleted file mode 100644 index 1c7fa1d..0000000 --- a/tests/unified_click_example.cpp +++ /dev/null @@ -1,101 +0,0 @@ -// Example of how UIFrame would implement unified click handling -// -// Click Priority Example: -// - Dialog Frame (has click handler to drag window) -// - Title Caption (no click handler) -// - Button Frame (has click handler) -// - Button Caption "OK" (no click handler) -// - Close X Sprite (has click handler) -// -// Clicking on: -// - "OK" text -> Button Frame gets the click (deepest parent with handler) -// - Close X -> Close sprite gets the click -// - Title bar -> Dialog Frame gets the click (no child has handler there) -// - Outside dialog -> nullptr (bounds check fails) - -class UIFrame : public UIDrawable, protected RectangularContainer { -private: - // Implementation of container interface - sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override { - // Children use same coordinate system as frame's local coordinates - return localPoint; - } - - UIDrawable* getClickHandler() override { - return click_callable ? this : nullptr; - } - - std::vector getClickableChildren() override { - std::vector result; - for (auto& child : *children) { - result.push_back(child.get()); - } - return result; - } - -public: - UIDrawable* click_at(sf::Vector2f point) override { - // Update bounds from box - bounds = sf::FloatRect(box.getPosition().x, box.getPosition().y, - box.getSize().x, box.getSize().y); - - // Use unified handler - return handleClick(point); - } -}; - -// Example for UIGrid with entity coordinate transformation -class UIGrid : public UIDrawable, protected RectangularContainer { -private: - sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override { - // For entities, we need to transform from pixel coordinates to grid coordinates - // This is where the grid's special coordinate system is handled - - // Assuming entity positions are in grid cells, not pixels - // We pass pixel coordinates relative to the grid's rendering area - return localPoint; // Entities will handle their own sprite positioning - } - - std::vector getClickableChildren() override { - std::vector result; - - // Only check entities that are visible on screen - float left_edge = center_x - (box.getSize().x / 2.0f) / (grid_size * zoom); - float top_edge = center_y - (box.getSize().y / 2.0f) / (grid_size * zoom); - float right_edge = left_edge + (box.getSize().x / (grid_size * zoom)); - float bottom_edge = top_edge + (box.getSize().y / (grid_size * zoom)); - - for (auto& entity : entities) { - // Check if entity is within visible bounds - if (entity->position.x >= left_edge - 1 && entity->position.x < right_edge + 1 && - entity->position.y >= top_edge - 1 && entity->position.y < bottom_edge + 1) { - result.push_back(&entity->sprite); - } - } - return result; - } -}; - -// For Scene, which has no coordinate transformation -class PyScene : protected UIContainerBase { -private: - sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override { - // Scene uses window coordinates directly - return point; - } - - sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const override { - // Top-level drawables use window coordinates - return localPoint; - } - - bool containsPoint(sf::Vector2f localPoint) const override { - // Scene contains all points (full window) - return true; - } - - UIDrawable* getClickHandler() override { - // Scene itself doesn't handle clicks - return nullptr; - } -}; \ No newline at end of file