Compare commits
No commits in common. "93256b96c6551c63dd575f3cd9e888fccf654b57" and "cd0bd5468beda8edaacef937463b216a79493fc5" have entirely different histories.
93256b96c6
...
cd0bd5468b
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB |
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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)
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB |
|
|
@ -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)
|
|
||||||
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 31 KiB |
|
|
@ -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)
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -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.
|
|
||||||
|
|
@ -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<sf::RenderTexture> 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.
|
|
||||||
524
ROADMAP.md
|
|
@ -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*
|
|
||||||
|
|
@ -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<sf::Event::Closed>()) {
|
|
||||||
window.close();
|
|
||||||
}
|
|
||||||
else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>()) {
|
|
||||||
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.
|
|
||||||
|
|
@ -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.
|
|
||||||
|
|
@ -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.
|
|
||||||
16
_test.py
|
|
@ -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")
|
|
||||||
|
|
||||||
|
|
@ -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!")
|
|
||||||
|
|
@ -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")
|
|
||||||
33
clean.sh
|
|
@ -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}"
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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")
|
|
||||||
|
|
@ -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<std::filesystem::path> 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
|
|
||||||
102
gitea_issues.py
|
|
@ -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()
|
|
||||||
|
|
@ -26,7 +26,7 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||||
render_target = &headless_renderer->getRenderTarget();
|
render_target = &headless_renderer->getRenderTarget();
|
||||||
} else {
|
} else {
|
||||||
window = std::make_unique<sf::RenderWindow>();
|
window = std::make_unique<sf::RenderWindow>();
|
||||||
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);
|
window->setFramerateLimit(60);
|
||||||
render_target = window.get();
|
render_target = window.get();
|
||||||
}
|
}
|
||||||
|
|
@ -73,81 +73,19 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||||
|
|
||||||
GameEngine::~GameEngine()
|
GameEngine::~GameEngine()
|
||||||
{
|
{
|
||||||
cleanup();
|
|
||||||
for (auto& [name, scene] : scenes) {
|
for (auto& [name, scene] : scenes) {
|
||||||
delete scene;
|
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]; }
|
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
||||||
void GameEngine::changeScene(std::string s)
|
void GameEngine::changeScene(std::string s)
|
||||||
{
|
{
|
||||||
changeScene(s, TransitionType::None, 0.0f);
|
/*std::cout << "Current scene is now '" << s << "'\n";*/
|
||||||
}
|
if (scenes.find(s) != scenes.end())
|
||||||
|
scene = s;
|
||||||
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);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
std::cout << "Attempted to change to a scene that doesn't exist (`" << s << "`)" << std::endl;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
void GameEngine::quit() { running = false; }
|
void GameEngine::quit() { running = false; }
|
||||||
void GameEngine::setPause(bool p) { paused = p; }
|
void GameEngine::setPause(bool p) { paused = p; }
|
||||||
|
|
@ -181,15 +119,9 @@ void GameEngine::run()
|
||||||
clock.restart();
|
clock.restart();
|
||||||
while (running)
|
while (running)
|
||||||
{
|
{
|
||||||
// Reset per-frame metrics
|
|
||||||
metrics.resetPerFrame();
|
|
||||||
|
|
||||||
currentScene()->update();
|
currentScene()->update();
|
||||||
testTimers();
|
testTimers();
|
||||||
|
|
||||||
// Update Python scenes
|
|
||||||
McRFPy_API::updatePythonScenes(frameTime);
|
|
||||||
|
|
||||||
// Update animations (only if frameTime is valid)
|
// Update animations (only if frameTime is valid)
|
||||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
if (frameTime > 0.0f && frameTime < 1.0f) {
|
||||||
AnimationManager::getInstance().update(frameTime);
|
AnimationManager::getInstance().update(frameTime);
|
||||||
|
|
@ -201,33 +133,7 @@ void GameEngine::run()
|
||||||
if (!paused)
|
if (!paused)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
currentScene()->render();
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display the frame
|
// Display the frame
|
||||||
if (headless) {
|
if (headless) {
|
||||||
|
|
@ -244,12 +150,8 @@ void GameEngine::run()
|
||||||
currentFrame++;
|
currentFrame++;
|
||||||
frameTime = clock.restart().asSeconds();
|
frameTime = clock.restart().asSeconds();
|
||||||
fps = 1 / frameTime;
|
fps = 1 / frameTime;
|
||||||
|
int whole_fps = (int)fps;
|
||||||
// Update profiling metrics
|
int tenth_fps = int(fps * 100) % 10;
|
||||||
metrics.updateFrameTime(frameTime * 1000.0f); // Convert to milliseconds
|
|
||||||
|
|
||||||
int whole_fps = metrics.fps;
|
|
||||||
int tenth_fps = (metrics.fps * 10) % 10;
|
|
||||||
|
|
||||||
if (!headless && window) {
|
if (!headless && window) {
|
||||||
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||||
|
|
@ -260,18 +162,6 @@ void GameEngine::run()
|
||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up before exiting the run loop
|
|
||||||
cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<PyTimerCallable> 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)
|
void GameEngine::manageTimer(std::string name, PyObject* target, int interval)
|
||||||
|
|
@ -318,15 +208,9 @@ void GameEngine::processEvent(const sf::Event& event)
|
||||||
int actionCode = 0;
|
int actionCode = 0;
|
||||||
|
|
||||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
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) {
|
else if (event.type == sf::Event::Resized) {
|
||||||
// Update the view to match the new window size
|
return; // 7DRL short circuit. Resizing manually disabled
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
|
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<std::vector<std::shared_ptr<UIDrawable>>> GameEngine::scene_ui(s
|
||||||
if (scenes.count(target) == 0) return NULL;
|
if (scenes.count(target) == 0) return NULL;
|
||||||
return scenes[target]->ui_elements;
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
#include "PyCallable.h"
|
#include "PyCallable.h"
|
||||||
#include "McRogueFaceConfig.h"
|
#include "McRogueFaceConfig.h"
|
||||||
#include "HeadlessRenderer.h"
|
#include "HeadlessRenderer.h"
|
||||||
#include "SceneTransition.h"
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class GameEngine
|
class GameEngine
|
||||||
|
|
@ -29,63 +28,19 @@ class GameEngine
|
||||||
|
|
||||||
bool headless = false;
|
bool headless = false;
|
||||||
McRogueFaceConfig config;
|
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;
|
sf::Clock runtime;
|
||||||
//std::map<std::string, Timer> timers;
|
//std::map<std::string, Timer> timers;
|
||||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||||
|
void testTimers();
|
||||||
|
|
||||||
|
public:
|
||||||
std::string scene;
|
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<int>(1000.0f / avgFrameTime) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void resetPerFrame() {
|
|
||||||
drawCalls = 0;
|
|
||||||
uiElements = 0;
|
|
||||||
visibleElements = 0;
|
|
||||||
}
|
|
||||||
} metrics;
|
|
||||||
GameEngine();
|
GameEngine();
|
||||||
GameEngine(const McRogueFaceConfig& cfg);
|
GameEngine(const McRogueFaceConfig& cfg);
|
||||||
~GameEngine();
|
~GameEngine();
|
||||||
Scene* currentScene();
|
Scene* currentScene();
|
||||||
void changeScene(std::string);
|
void changeScene(std::string);
|
||||||
void changeScene(std::string sceneName, TransitionType transitionType, float duration);
|
|
||||||
void createScene(std::string);
|
void createScene(std::string);
|
||||||
void quit();
|
void quit();
|
||||||
void setPause(bool);
|
void setPause(bool);
|
||||||
|
|
@ -95,23 +50,13 @@ public:
|
||||||
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
|
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
|
||||||
void run();
|
void run();
|
||||||
void sUserInput();
|
void sUserInput();
|
||||||
void cleanup(); // Clean up Python references before destruction
|
|
||||||
int getFrame() { return currentFrame; }
|
int getFrame() { return currentFrame; }
|
||||||
float getFrameTime() { return frameTime; }
|
float getFrameTime() { return frameTime; }
|
||||||
sf::View getView() { return visible; }
|
sf::View getView() { return visible; }
|
||||||
void manageTimer(std::string, PyObject*, int);
|
void manageTimer(std::string, PyObject*, int);
|
||||||
std::shared_ptr<PyTimerCallable> getTimer(const std::string& name);
|
|
||||||
void setWindowScale(float);
|
void setWindowScale(float);
|
||||||
bool isHeadless() const { return headless; }
|
bool isHeadless() const { return headless; }
|
||||||
void processEvent(const sf::Event& event);
|
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
|
// global textures for scripts to access
|
||||||
std::vector<IndexTexture> textures;
|
std::vector<IndexTexture> textures;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,6 @@
|
||||||
#include "McRFPy_Automation.h"
|
#include "McRFPy_Automation.h"
|
||||||
#include "platform.h"
|
#include "platform.h"
|
||||||
#include "PyAnimation.h"
|
#include "PyAnimation.h"
|
||||||
#include "PyDrawable.h"
|
|
||||||
#include "PyTimer.h"
|
|
||||||
#include "PyWindow.h"
|
|
||||||
#include "PySceneObject.h"
|
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "UI.h"
|
#include "UI.h"
|
||||||
#include "Resources.h"
|
#include "Resources.h"
|
||||||
|
|
@ -13,9 +9,9 @@
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
std::vector<sf::SoundBuffer>* McRFPy_API::soundbuffers = nullptr;
|
std::vector<sf::SoundBuffer> McRFPy_API::soundbuffers;
|
||||||
sf::Music* McRFPy_API::music = nullptr;
|
sf::Music McRFPy_API::music;
|
||||||
sf::Sound* McRFPy_API::sfx = nullptr;
|
sf::Sound McRFPy_API::sfx;
|
||||||
|
|
||||||
std::shared_ptr<PyFont> McRFPy_API::default_font;
|
std::shared_ptr<PyFont> McRFPy_API::default_font;
|
||||||
std::shared_ptr<PyTexture> McRFPy_API::default_texture;
|
std::shared_ptr<PyTexture> 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"},
|
{"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"},
|
{"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"},
|
{"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"},
|
{"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`"},
|
{"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"},
|
{"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)"},
|
{"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}
|
{NULL, NULL, 0, NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -79,9 +69,6 @@ PyObject* PyInit_mcrfpy()
|
||||||
/*SFML exposed types*/
|
/*SFML exposed types*/
|
||||||
&PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType,
|
&PyColorType, /*&PyLinkedColorType,*/ &PyFontType, &PyTextureType, &PyVectorType,
|
||||||
|
|
||||||
/*Base classes*/
|
|
||||||
&PyDrawableType,
|
|
||||||
|
|
||||||
/*UI widgets*/
|
/*UI widgets*/
|
||||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
||||||
|
|
||||||
|
|
@ -94,26 +81,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
|
|
||||||
/*animation*/
|
/*animation*/
|
||||||
&PyAnimationType,
|
&PyAnimationType,
|
||||||
|
|
||||||
/*timer*/
|
|
||||||
&PyTimerType,
|
|
||||||
|
|
||||||
/*window singleton*/
|
|
||||||
&PyWindowType,
|
|
||||||
|
|
||||||
/*scene class*/
|
|
||||||
&PySceneType,
|
|
||||||
|
|
||||||
nullptr};
|
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;
|
int i = 0;
|
||||||
auto t = pytypes[i];
|
auto t = pytypes[i];
|
||||||
while (t != nullptr)
|
while (t != nullptr)
|
||||||
|
|
@ -132,7 +100,8 @@ PyObject* PyInit_mcrfpy()
|
||||||
// Add default_font and default_texture to module
|
// Add default_font and default_texture to module
|
||||||
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
|
McRFPy_API::default_font = std::make_shared<PyFont>("assets/JetbrainsMono.ttf");
|
||||||
McRFPy_API::default_texture = std::make_shared<PyTexture>("assets/kenney_tinydungeon.png", 16, 16);
|
McRFPy_API::default_texture = std::make_shared<PyTexture>("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_font", Py_None);
|
||||||
PyModule_AddObject(m, "default_texture", Py_None);
|
PyModule_AddObject(m, "default_texture", Py_None);
|
||||||
|
|
||||||
|
|
@ -168,11 +137,6 @@ PyStatus init_python(const char *program_name)
|
||||||
PyConfig config;
|
PyConfig config;
|
||||||
PyConfig_InitIsolatedConfig(&config);
|
PyConfig_InitIsolatedConfig(&config);
|
||||||
config.dev_mode = 0;
|
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,
|
PyConfig_SetBytesString(&config, &config.home,
|
||||||
narrow_string(executable_path() + L"/lib/Python").c_str());
|
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 pyconfig;
|
||||||
PyConfig_InitIsolatedConfig(&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
|
// CRITICAL: Pass actual command line arguments to Python
|
||||||
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
|
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
|
||||||
if (PyStatus_Exception(status)) {
|
if (PyStatus_Exception(status)) {
|
||||||
|
|
@ -380,23 +339,6 @@ void McRFPy_API::executeScript(std::string filename)
|
||||||
|
|
||||||
void McRFPy_API::api_shutdown()
|
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();
|
Py_Finalize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,29 +373,25 @@ PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
|
||||||
PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
|
PyObject* McRFPy_API::_createSoundBuffer(PyObject* self, PyObject* args) {
|
||||||
const char *fn_cstr;
|
const char *fn_cstr;
|
||||||
if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
|
if (!PyArg_ParseTuple(args, "s", &fn_cstr)) return NULL;
|
||||||
// Initialize soundbuffers if needed
|
|
||||||
if (!McRFPy_API::soundbuffers) {
|
|
||||||
McRFPy_API::soundbuffers = new std::vector<sf::SoundBuffer>();
|
|
||||||
}
|
|
||||||
auto b = sf::SoundBuffer();
|
auto b = sf::SoundBuffer();
|
||||||
b.loadFromFile(fn_cstr);
|
b.loadFromFile(fn_cstr);
|
||||||
McRFPy_API::soundbuffers->push_back(b);
|
McRFPy_API::soundbuffers.push_back(b);
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return Py_None;
|
return Py_None;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
|
PyObject* McRFPy_API::_loadMusic(PyObject* self, PyObject* args) {
|
||||||
const char *fn_cstr;
|
const char *fn_cstr;
|
||||||
PyObject* loop_obj = Py_False;
|
PyObject* loop_obj;
|
||||||
if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
|
if (!PyArg_ParseTuple(args, "s|O", &fn_cstr, &loop_obj)) return NULL;
|
||||||
// Initialize music if needed
|
McRFPy_API::music.stop();
|
||||||
if (!McRFPy_API::music) {
|
// get params for sf::Music initialization
|
||||||
McRFPy_API::music = new sf::Music();
|
//sf::InputSoundFile file;
|
||||||
}
|
//file.openFromFile(fn_cstr);
|
||||||
McRFPy_API::music->stop();
|
McRFPy_API::music.openFromFile(fn_cstr);
|
||||||
McRFPy_API::music->openFromFile(fn_cstr);
|
McRFPy_API::music.setLoop(PyObject_IsTrue(loop_obj));
|
||||||
McRFPy_API::music->setLoop(PyObject_IsTrue(loop_obj));
|
//McRFPy_API::music.initialize(file.getChannelCount(), file.getSampleRate());
|
||||||
McRFPy_API::music->play();
|
McRFPy_API::music.play();
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return 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) {
|
PyObject* McRFPy_API::_setMusicVolume(PyObject* self, PyObject* args) {
|
||||||
int vol;
|
int vol;
|
||||||
if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
|
if (!PyArg_ParseTuple(args, "i", &vol)) return NULL;
|
||||||
if (!McRFPy_API::music) {
|
McRFPy_API::music.setVolume(vol);
|
||||||
McRFPy_API::music = new sf::Music();
|
|
||||||
}
|
|
||||||
McRFPy_API::music->setVolume(vol);
|
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return 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) {
|
PyObject* McRFPy_API::_setSoundVolume(PyObject* self, PyObject* args) {
|
||||||
float vol;
|
float vol;
|
||||||
if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
|
if (!PyArg_ParseTuple(args, "f", &vol)) return NULL;
|
||||||
if (!McRFPy_API::sfx) {
|
McRFPy_API::sfx.setVolume(vol);
|
||||||
McRFPy_API::sfx = new sf::Sound();
|
|
||||||
}
|
|
||||||
McRFPy_API::sfx->setVolume(vol);
|
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return 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) {
|
PyObject* McRFPy_API::_playSound(PyObject* self, PyObject* args) {
|
||||||
float index;
|
float index;
|
||||||
if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
|
if (!PyArg_ParseTuple(args, "f", &index)) return NULL;
|
||||||
if (!McRFPy_API::soundbuffers || index >= McRFPy_API::soundbuffers->size()) return NULL;
|
if (index >= McRFPy_API::soundbuffers.size()) return NULL;
|
||||||
if (!McRFPy_API::sfx) {
|
McRFPy_API::sfx.stop();
|
||||||
McRFPy_API::sfx = new sf::Sound();
|
McRFPy_API::sfx.setBuffer(McRFPy_API::soundbuffers[index]);
|
||||||
}
|
McRFPy_API::sfx.play();
|
||||||
McRFPy_API::sfx->stop();
|
|
||||||
McRFPy_API::sfx->setBuffer((*McRFPy_API::soundbuffers)[index]);
|
|
||||||
McRFPy_API::sfx->play();
|
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return Py_None;
|
return Py_None;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
|
PyObject* McRFPy_API::_getMusicVolume(PyObject* self, PyObject* args) {
|
||||||
if (!McRFPy_API::music) {
|
return Py_BuildValue("f", McRFPy_API::music.getVolume());
|
||||||
return Py_BuildValue("f", 0.0f);
|
|
||||||
}
|
|
||||||
return Py_BuildValue("f", McRFPy_API::music->getVolume());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
|
PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
|
||||||
if (!McRFPy_API::sfx) {
|
return Py_BuildValue("f", McRFPy_API::sfx.getVolume());
|
||||||
return Py_BuildValue("f", 0.0f);
|
|
||||||
}
|
|
||||||
return Py_BuildValue("f", McRFPy_API::sfx->getVolume());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Removed deprecated player_input, computerTurn, playerTurn functions
|
// 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) {
|
PyObject* McRFPy_API::_setScene(PyObject* self, PyObject* args) {
|
||||||
const char* newscene;
|
const char* newscene;
|
||||||
const char* transition_str = nullptr;
|
if (!PyArg_ParseTuple(args, "s", &newscene)) return NULL;
|
||||||
float duration = 0.0f;
|
game->changeScene(newscene);
|
||||||
|
|
||||||
// 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);
|
|
||||||
Py_INCREF(Py_None);
|
Py_INCREF(Py_None);
|
||||||
return 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<std::shared_ptr<UIDrawable>>* 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<UIFrame>(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<UICaption>(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<UISprite>(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<UIGrid>(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<UIFrame>(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<std::vector<std::shared_ptr<UIDrawable>>> 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<UIGrid>(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<std::vector<std::shared_ptr<UIDrawable>>> 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<UIGrid>(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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -36,9 +36,9 @@ public:
|
||||||
static void REPL_device(FILE * fp, const char *filename);
|
static void REPL_device(FILE * fp, const char *filename);
|
||||||
static void REPL();
|
static void REPL();
|
||||||
|
|
||||||
static std::vector<sf::SoundBuffer>* soundbuffers;
|
static std::vector<sf::SoundBuffer> soundbuffers;
|
||||||
static sf::Music* music;
|
static sf::Music music;
|
||||||
static sf::Sound* sfx;
|
static sf::Sound sfx;
|
||||||
|
|
||||||
|
|
||||||
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
|
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
|
||||||
|
|
@ -73,16 +73,4 @@ public:
|
||||||
|
|
||||||
// Helper to mark scenes as needing z_index resort
|
// Helper to mark scenes as needing z_index resort
|
||||||
static void markSceneNeedsSort();
|
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);
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,24 +16,21 @@ PyObject* PyCallable::call(PyObject* args, PyObject* kwargs)
|
||||||
return PyObject_Call(target, args, kwargs);
|
return PyObject_Call(target, args, kwargs);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PyCallable::isNone() const
|
bool PyCallable::isNone()
|
||||||
{
|
{
|
||||||
return (target == Py_None || target == NULL);
|
return (target == Py_None || target == NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
|
PyTimerCallable::PyTimerCallable(PyObject* _target, int _interval, int now)
|
||||||
: PyCallable(_target), interval(_interval), last_ran(now),
|
: PyCallable(_target), interval(_interval), last_ran(now)
|
||||||
paused(false), pause_start_time(0), total_paused_time(0)
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
PyTimerCallable::PyTimerCallable()
|
PyTimerCallable::PyTimerCallable()
|
||||||
: PyCallable(Py_None), interval(0), last_ran(0),
|
: PyCallable(Py_None), interval(0), last_ran(0)
|
||||||
paused(false), pause_start_time(0), total_paused_time(0)
|
|
||||||
{}
|
{}
|
||||||
|
|
||||||
bool PyTimerCallable::hasElapsed(int now)
|
bool PyTimerCallable::hasElapsed(int now)
|
||||||
{
|
{
|
||||||
if (paused) return false;
|
|
||||||
return now >= last_ran + interval;
|
return now >= last_ran + interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,62 +60,6 @@ bool PyTimerCallable::test(int now)
|
||||||
return false;
|
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)
|
PyClickCallable::PyClickCallable(PyObject* _target)
|
||||||
: PyCallable(_target)
|
: PyCallable(_target)
|
||||||
{}
|
{}
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ protected:
|
||||||
~PyCallable();
|
~PyCallable();
|
||||||
PyObject* call(PyObject*, PyObject*);
|
PyObject* call(PyObject*, PyObject*);
|
||||||
public:
|
public:
|
||||||
bool isNone() const;
|
bool isNone();
|
||||||
};
|
};
|
||||||
|
|
||||||
class PyTimerCallable: public PyCallable
|
class PyTimerCallable: public PyCallable
|
||||||
|
|
@ -19,32 +19,11 @@ private:
|
||||||
int interval;
|
int interval;
|
||||||
int last_ran;
|
int last_ran;
|
||||||
void call(int);
|
void call(int);
|
||||||
|
|
||||||
// Pause/resume support
|
|
||||||
bool paused;
|
|
||||||
int pause_start_time;
|
|
||||||
int total_paused_time;
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool hasElapsed(int);
|
bool hasElapsed(int);
|
||||||
bool test(int);
|
bool test(int);
|
||||||
PyTimerCallable(PyObject*, int, int);
|
PyTimerCallable(PyObject*, int, int);
|
||||||
PyTimerCallable();
|
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
|
class PyClickCallable: public PyCallable
|
||||||
|
|
|
||||||
111
src/PyColor.cpp
|
|
@ -2,8 +2,6 @@
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PyRAII.h"
|
#include "PyRAII.h"
|
||||||
#include <string>
|
|
||||||
#include <cstdio>
|
|
||||||
|
|
||||||
PyGetSetDef PyColor::getsetters[] = {
|
PyGetSetDef PyColor::getsetters[] = {
|
||||||
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
|
{"r", (getter)PyColor::get_member, (setter)PyColor::set_member, "Red component", (void*)0},
|
||||||
|
|
@ -13,13 +11,6 @@ PyGetSetDef PyColor::getsetters[] = {
|
||||||
{NULL}
|
{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)
|
PyColor::PyColor(sf::Color target)
|
||||||
:data(target) {}
|
:data(target) {}
|
||||||
|
|
||||||
|
|
@ -226,105 +217,3 @@ PyColorObject* PyColor::from_arg(PyObject* args)
|
||||||
// Release ownership and return
|
// Release ownership and return
|
||||||
return (PyColorObject*)obj.release();
|
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<sf::Uint8>(self->data.r + (other->data.r - self->data.r) * t);
|
|
||||||
sf::Uint8 g = static_cast<sf::Uint8>(self->data.g + (other->data.g - self->data.g) * t);
|
|
||||||
sf::Uint8 b = static_cast<sf::Uint8>(self->data.b + (other->data.b - self->data.b) * t);
|
|
||||||
sf::Uint8 a = static_cast<sf::Uint8>(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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,7 @@ public:
|
||||||
static PyObject* get_member(PyObject*, void*);
|
static PyObject* get_member(PyObject*, void*);
|
||||||
static int set_member(PyObject*, 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 PyGetSetDef getsetters[];
|
||||||
static PyMethodDef methods[];
|
|
||||||
static PyColorObject* from_arg(PyObject*);
|
static PyColorObject* from_arg(PyObject*);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,7 +42,6 @@ namespace mcrfpydef {
|
||||||
.tp_hash = PyColor::hash,
|
.tp_hash = PyColor::hash,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("SFML Color Object"),
|
.tp_doc = PyDoc_STR("SFML Color Object"),
|
||||||
.tp_methods = PyColor::methods,
|
|
||||||
.tp_getset = PyColor::getsetters,
|
.tp_getset = PyColor::getsetters,
|
||||||
.tp_init = (initproc)PyColor::init,
|
.tp_init = (initproc)PyColor::init,
|
||||||
.tp_new = PyColor::pynew,
|
.tp_new = PyColor::pynew,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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<UIDrawable> data;
|
|
||||||
} PyDrawableObject;
|
|
||||||
|
|
||||||
// Declare the Python type for _Drawable base class
|
|
||||||
namespace mcrfpydef {
|
|
||||||
extern PyTypeObject PyDrawableType;
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -29,19 +29,26 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
||||||
|
|
||||||
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
||||||
auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos);
|
auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos);
|
||||||
|
UIDrawable* target;
|
||||||
// Create a sorted copy by z-index (highest first)
|
for (auto d: *ui_elements)
|
||||||
std::vector<std::shared_ptr<UIDrawable>> sorted_elements(*ui_elements);
|
{
|
||||||
std::sort(sorted_elements.begin(), sorted_elements.end(),
|
target = d->click_at(sf::Vector2f(mousepos));
|
||||||
[](const auto& a, const auto& b) { return a->z_index > b->z_index; });
|
if (target)
|
||||||
|
{
|
||||||
// Check elements in z-order (top to bottom)
|
/*
|
||||||
for (const auto& element : sorted_elements) {
|
PyObject* args = Py_BuildValue("(iiss)", (int)mousepos.x, (int)mousepos.y, button.c_str(), type.c_str());
|
||||||
if (!element->visible) continue;
|
PyObject* retval = PyObject_Call(target->click_callable, args, NULL);
|
||||||
|
if (!retval)
|
||||||
if (auto target = element->click_at(sf::Vector2f(mousepos))) {
|
{
|
||||||
|
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);
|
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)
|
// Render in sorted order (no need to copy anymore)
|
||||||
for (auto e: *ui_elements)
|
for (auto e: *ui_elements)
|
||||||
{
|
{
|
||||||
if (e) {
|
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++;
|
|
||||||
}
|
|
||||||
e->render();
|
e->render();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display is handled by GameEngine
|
// Display is handled by GameEngine
|
||||||
|
|
|
||||||
|
|
@ -1,268 +0,0 @@
|
||||||
#include "PySceneObject.h"
|
|
||||||
#include "PyScene.h"
|
|
||||||
#include "GameEngine.h"
|
|
||||||
#include "McRFPy_API.h"
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
// Static map to store Python scene objects by name
|
|
||||||
static std::map<std::string, PySceneObject*> 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<char**>(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("<Scene '%s'>", 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<PyKeyCallable>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "Common.h"
|
|
||||||
#include "Python.h"
|
|
||||||
#include <string>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
// Forward declarations
|
|
||||||
class PyScene;
|
|
||||||
|
|
||||||
// Python object structure for Scene
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD
|
|
||||||
std::string name;
|
|
||||||
std::shared_ptr<PyScene> 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__,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
271
src/PyTimer.cpp
|
|
@ -1,271 +0,0 @@
|
||||||
#include "PyTimer.h"
|
|
||||||
#include "PyCallable.h"
|
|
||||||
#include "GameEngine.h"
|
|
||||||
#include "Resources.h"
|
|
||||||
#include <sstream>
|
|
||||||
|
|
||||||
PyObject* PyTimer::repr(PyObject* self) {
|
|
||||||
PyTimerObject* timer = (PyTimerObject*)self;
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "<Timer name='" << timer->name << "' ";
|
|
||||||
|
|
||||||
if (timer->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<PyTimerCallable>(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}
|
|
||||||
};
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "Common.h"
|
|
||||||
#include "Python.h"
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class PyTimerCallable;
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
PyObject_HEAD
|
|
||||||
std::shared_ptr<PyTimerCallable> 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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
291
src/PyVector.cpp
|
|
@ -1,6 +1,5 @@
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include <cmath>
|
|
||||||
|
|
||||||
PyGetSetDef PyVector::getsetters[] = {
|
PyGetSetDef PyVector::getsetters[] = {
|
||||||
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
|
{"x", (getter)PyVector::get_member, (setter)PyVector::set_member, "X/horizontal component", (void*)0},
|
||||||
|
|
@ -8,58 +7,6 @@ PyGetSetDef PyVector::getsetters[] = {
|
||||||
{NULL}
|
{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)
|
PyVector::PyVector(sf::Vector2f target)
|
||||||
:data(target) {}
|
:data(target) {}
|
||||||
|
|
||||||
|
|
@ -225,241 +172,3 @@ PyVectorObject* PyVector::from_arg(PyObject* args)
|
||||||
|
|
||||||
return obj;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -25,47 +25,19 @@ public:
|
||||||
static int set_member(PyObject*, PyObject*, void*);
|
static int set_member(PyObject*, PyObject*, void*);
|
||||||
static PyVectorObject* from_arg(PyObject*);
|
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 PyGetSetDef getsetters[];
|
||||||
static PyMethodDef methods[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
namespace mcrfpydef {
|
namespace mcrfpydef {
|
||||||
// Forward declare the PyNumberMethods structure
|
|
||||||
extern PyNumberMethods PyVector_as_number;
|
|
||||||
|
|
||||||
static PyTypeObject PyVectorType = {
|
static PyTypeObject PyVectorType = {
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
.tp_name = "mcrfpy.Vector",
|
.tp_name = "mcrfpy.Vector",
|
||||||
.tp_basicsize = sizeof(PyVectorObject),
|
.tp_basicsize = sizeof(PyVectorObject),
|
||||||
.tp_itemsize = 0,
|
.tp_itemsize = 0,
|
||||||
.tp_repr = PyVector::repr,
|
.tp_repr = PyVector::repr,
|
||||||
.tp_as_number = &PyVector_as_number,
|
|
||||||
.tp_hash = PyVector::hash,
|
.tp_hash = PyVector::hash,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("SFML Vector Object"),
|
.tp_doc = PyDoc_STR("SFML Vector Object"),
|
||||||
.tp_richcompare = PyVector::richcompare,
|
|
||||||
.tp_methods = PyVector::methods,
|
|
||||||
.tp_getset = PyVector::getsetters,
|
.tp_getset = PyVector::getsetters,
|
||||||
.tp_init = (initproc)PyVector::init,
|
.tp_init = (initproc)PyVector::init,
|
||||||
.tp_new = PyVector::pynew,
|
.tp_new = PyVector::pynew,
|
||||||
|
|
|
||||||
433
src/PyWindow.cpp
|
|
@ -1,433 +0,0 @@
|
||||||
#include "PyWindow.h"
|
|
||||||
#include "GameEngine.h"
|
|
||||||
#include "McRFPy_API.h"
|
|
||||||
#include <SFML/Graphics.hpp>
|
|
||||||
|
|
||||||
// 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("<Window [no game engine]>");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game->isHeadless()) {
|
|
||||||
return PyUnicode_FromString("<Window [headless mode]>");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto& window = game->getWindow();
|
|
||||||
auto size = window.getSize();
|
|
||||||
|
|
||||||
return PyUnicode_FromFormat("<Window %dx%d>", 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<char**>(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<sf::RenderWindow*>(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<sf::RenderTexture*>(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}
|
|
||||||
};
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -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<sf::RenderTexture>();
|
|
||||||
oldSceneTexture->create(1024, 768);
|
|
||||||
}
|
|
||||||
if (!newSceneTexture) {
|
|
||||||
newSceneTexture = std::make_unique<sf::RenderTexture>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "Common.h"
|
|
||||||
#include <SFML/Graphics.hpp>
|
|
||||||
#include <string>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
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<sf::RenderTexture> oldSceneTexture;
|
|
||||||
std::unique_ptr<sf::RenderTexture> 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);
|
|
||||||
};
|
|
||||||
102
src/UIBase.h
|
|
@ -1,6 +1,4 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "Python.h"
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
class UIEntity;
|
class UIEntity;
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
@ -32,103 +30,3 @@ typedef struct {
|
||||||
PyObject_HEAD
|
PyObject_HEAD
|
||||||
std::shared_ptr<UISprite> data;
|
std::shared_ptr<UISprite> data;
|
||||||
} PyUISpriteObject;
|
} PyUISpriteObject;
|
||||||
|
|
||||||
// Common Python method implementations for UIDrawable-derived classes
|
|
||||||
// These template functions provide shared functionality for Python bindings
|
|
||||||
|
|
||||||
// get_bounds method implementation (#89)
|
|
||||||
template<typename T>
|
|
||||||
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<typename T>
|
|
||||||
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<typename T>
|
|
||||||
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<PyObjectType>, METH_NOARGS, \
|
|
||||||
"Get bounding box as (x, y, width, height)"}, \
|
|
||||||
{"move", (PyCFunction)UIDrawable_move<PyObjectType>, METH_VARARGS, \
|
|
||||||
"Move by relative offset (dx, dy)"}, \
|
|
||||||
{"resize", (PyCFunction)UIDrawable_resize<PyObjectType>, METH_VARARGS, \
|
|
||||||
"Resize to new dimensions (width, height)"}
|
|
||||||
|
|
||||||
// Property getters/setters for visible and opacity
|
|
||||||
template<typename T>
|
|
||||||
static PyObject* UIDrawable_get_visible(T* self, void* closure)
|
|
||||||
{
|
|
||||||
return PyBool_FromLong(self->data->visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
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<typename T>
|
|
||||||
static PyObject* UIDrawable_get_opacity(T* self, void* closure)
|
|
||||||
{
|
|
||||||
return PyFloat_FromDouble(self->data->opacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
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<PyObjectType>, (setter)UIDrawable_set_visible<PyObjectType>, \
|
|
||||||
"Visibility flag", NULL}, \
|
|
||||||
{"opacity", (getter)UIDrawable_get_opacity<PyObjectType>, (setter)UIDrawable_set_opacity<PyObjectType>, \
|
|
||||||
"Opacity (0.0 = transparent, 1.0 = opaque)", NULL}
|
|
||||||
|
|
||||||
// UIEntity specializations are defined in UIEntity.cpp after UIEntity class is complete
|
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,8 @@
|
||||||
#include "PyColor.h"
|
#include "PyColor.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyFont.h"
|
#include "PyFont.h"
|
||||||
#include "PyPositionHelper.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
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)
|
UIDrawable* UICaption::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
if (click_callable)
|
if (click_callable)
|
||||||
|
|
@ -29,22 +16,10 @@ UIDrawable* UICaption::click_at(sf::Vector2f point)
|
||||||
|
|
||||||
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
void UICaption::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
{
|
{
|
||||||
// Check visibility
|
|
||||||
if (!visible) return;
|
|
||||||
|
|
||||||
// Apply opacity
|
|
||||||
auto color = text.getFillColor();
|
|
||||||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
|
||||||
text.setFillColor(color);
|
|
||||||
|
|
||||||
text.move(offset);
|
text.move(offset);
|
||||||
//Resources::game->getWindow().draw(text);
|
//Resources::game->getWindow().draw(text);
|
||||||
target.draw(text);
|
target.draw(text);
|
||||||
text.move(-offset);
|
text.move(-offset);
|
||||||
|
|
||||||
// Restore original alpha
|
|
||||||
color.a = 255;
|
|
||||||
text.setFillColor(color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObjectsEnum UICaption::derived_type()
|
PyObjectsEnum UICaption::derived_type()
|
||||||
|
|
@ -52,23 +27,6 @@ PyObjectsEnum UICaption::derived_type()
|
||||||
return PyObjectsEnum::UICAPTION;
|
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)
|
PyObject* UICaption::get_float_member(PyUICaptionObject* self, void* closure)
|
||||||
{
|
{
|
||||||
auto member_ptr = reinterpret_cast<long>(closure);
|
auto member_ptr = reinterpret_cast<long>(closure);
|
||||||
|
|
@ -164,6 +122,7 @@ int UICaption::set_color_member(PyUICaptionObject* self, PyObject* value, void*
|
||||||
// get value from mcrfpy.Color instance
|
// get value from mcrfpy.Color instance
|
||||||
auto c = ((PyColorObject*)value)->data;
|
auto c = ((PyColorObject*)value)->data;
|
||||||
r = c.r; g = c.g; b = c.b; a = c.a;
|
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)
|
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?
|
//TODO: evaluate use of Resources::caption_buffer... can't I do this with a std::string?
|
||||||
PyObject* UICaption::get_text(PyUICaptionObject* self, void* closure)
|
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},
|
{"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},
|
{"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},
|
{"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}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -277,92 +225,30 @@ PyObject* UICaption::repr(PyUICaptionObject* self)
|
||||||
int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
using namespace mcrfpydef;
|
using namespace mcrfpydef;
|
||||||
|
// Constructor switch to Vector position
|
||||||
static const char* keywords[] = { "text", "x", "y", "font", "fill_color", "outline_color", "outline", "click", "pos", nullptr };
|
//static const char* keywords[] = { "x", "y", "text", "font", "fill_color", "outline_color", "outline", nullptr };
|
||||||
float x = 0.0f, y = 0.0f, outline = 0.0f;
|
//float x = 0.0f, y = 0.0f, outline = 0.0f;
|
||||||
char* text = NULL;
|
static const char* keywords[] = { "pos", "text", "font", "fill_color", "outline_color", "outline", nullptr };
|
||||||
PyObject* font = NULL;
|
PyObject* pos;
|
||||||
PyObject* fill_color = NULL;
|
float outline = 0.0f;
|
||||||
PyObject* outline_color = NULL;
|
char* text;
|
||||||
PyObject* click_handler = NULL;
|
PyObject* font=NULL, *fill_color=NULL, *outline_color=NULL;
|
||||||
PyObject* pos_obj = NULL;
|
|
||||||
|
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf",
|
||||||
// Handle different argument patterns
|
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
|
||||||
Py_ssize_t args_size = PyTuple_Size(args);
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf",
|
||||||
|
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline))
|
||||||
if (args_size >= 2 && !PyUnicode_Check(PyTuple_GetItem(args, 0))) {
|
{
|
||||||
// Pattern 1: (x, y, text, ...) or ((x,y), text, ...)
|
return -1;
|
||||||
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<char**>(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<char**>(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<char**>(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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// check types for font, fill_color, outline_color
|
||||||
|
|
||||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
|
//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
|
self->data->text.setString((std::string)text);
|
||||||
if (text && text != NULL) {
|
|
||||||
self->data->text.setString((std::string)text);
|
|
||||||
} else {
|
|
||||||
self->data->text.setString("");
|
|
||||||
}
|
|
||||||
self->data->text.setOutlineThickness(outline);
|
self->data->text.setOutlineThickness(outline);
|
||||||
if (fill_color) {
|
if (fill_color) {
|
||||||
auto fc = PyColor::from_arg(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));
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,16 +7,10 @@ class UICaption: public UIDrawable
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
sf::Text text;
|
sf::Text text;
|
||||||
UICaption(); // Default constructor with safe initialization
|
|
||||||
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||||
PyObjectsEnum derived_type() override final;
|
PyObjectsEnum derived_type() override final;
|
||||||
virtual UIDrawable* click_at(sf::Vector2f point) 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
|
// Property system for animations
|
||||||
bool setProperty(const std::string& name, float value) override;
|
bool setProperty(const std::string& name, float value) override;
|
||||||
bool setProperty(const std::string& name, const sf::Color& value) override;
|
bool setProperty(const std::string& name, const sf::Color& value) override;
|
||||||
|
|
@ -40,8 +34,6 @@ public:
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
extern PyMethodDef UICaption_methods[];
|
|
||||||
|
|
||||||
namespace mcrfpydef {
|
namespace mcrfpydef {
|
||||||
static PyTypeObject PyUICaptionType = {
|
static PyTypeObject PyUICaptionType = {
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
|
@ -64,7 +56,7 @@ namespace mcrfpydef {
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("docstring"),
|
.tp_doc = PyDoc_STR("docstring"),
|
||||||
.tp_methods = UICaption_methods,
|
//.tp_methods = PyUIFrame_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
.tp_getset = UICaption::getsetters,
|
.tp_getset = UICaption::getsetters,
|
||||||
//.tp_base = NULL,
|
//.tp_base = NULL,
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include "UIDrawable.h"
|
|
||||||
#include <vector>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
// Base class for UI containers that provides common click handling logic
|
|
||||||
class UIContainerBase {
|
|
||||||
protected:
|
|
||||||
// Transform a point from parent coordinates to this container's local coordinates
|
|
||||||
virtual sf::Vector2f toLocalCoordinates(sf::Vector2f point) const = 0;
|
|
||||||
|
|
||||||
// Transform a point from this container's local coordinates to child coordinates
|
|
||||||
virtual sf::Vector2f toChildCoordinates(sf::Vector2f localPoint, int childIndex) const = 0;
|
|
||||||
|
|
||||||
// Get the bounds of this container in parent coordinates
|
|
||||||
virtual sf::FloatRect getBounds() const = 0;
|
|
||||||
|
|
||||||
// Check if a local point is within this container's bounds
|
|
||||||
virtual bool containsPoint(sf::Vector2f localPoint) const = 0;
|
|
||||||
|
|
||||||
// Get click handler if this container has one
|
|
||||||
virtual UIDrawable* getClickHandler() = 0;
|
|
||||||
|
|
||||||
// Get children to check for clicks (can be empty)
|
|
||||||
virtual std::vector<UIDrawable*> getClickableChildren() = 0;
|
|
||||||
|
|
||||||
public:
|
|
||||||
// Standard click handling algorithm for all containers
|
|
||||||
// Returns the deepest UIDrawable that has a click handler and contains the point
|
|
||||||
UIDrawable* handleClick(sf::Vector2f point) {
|
|
||||||
// Transform to local coordinates
|
|
||||||
sf::Vector2f localPoint = toLocalCoordinates(point);
|
|
||||||
|
|
||||||
// Check if point is within our bounds
|
|
||||||
if (!containsPoint(localPoint)) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check children in reverse z-order (top-most first)
|
|
||||||
// This ensures that elements rendered on top get first chance at clicks
|
|
||||||
auto children = getClickableChildren();
|
|
||||||
|
|
||||||
// TODO: Sort by z-index if not already sorted
|
|
||||||
// std::sort(children.begin(), children.end(),
|
|
||||||
// [](UIDrawable* a, UIDrawable* b) { return a->z_index > b->z_index; });
|
|
||||||
|
|
||||||
for (int i = children.size() - 1; i >= 0; --i) {
|
|
||||||
if (!children[i]->visible) continue;
|
|
||||||
|
|
||||||
sf::Vector2f childPoint = toChildCoordinates(localPoint, i);
|
|
||||||
if (auto target = children[i]->click_at(childPoint)) {
|
|
||||||
// Child (or its descendant) handled the click
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
// If child didn't handle it, continue checking other children
|
|
||||||
// This allows click-through for elements without handlers
|
|
||||||
}
|
|
||||||
|
|
||||||
// No child consumed the click
|
|
||||||
// Now check if WE have a click handler
|
|
||||||
return getClickHandler();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Helper for containers with simple box bounds
|
|
||||||
class RectangularContainer : public UIContainerBase {
|
|
||||||
protected:
|
|
||||||
sf::FloatRect bounds;
|
|
||||||
|
|
||||||
sf::Vector2f toLocalCoordinates(sf::Vector2f point) const override {
|
|
||||||
return point - sf::Vector2f(bounds.left, bounds.top);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool containsPoint(sf::Vector2f localPoint) const override {
|
|
||||||
return localPoint.x >= 0 && localPoint.y >= 0 &&
|
|
||||||
localPoint.x < bounds.width && localPoint.y < bounds.height;
|
|
||||||
}
|
|
||||||
|
|
||||||
sf::FloatRect getBounds() const override {
|
|
||||||
return bounds;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -25,28 +25,16 @@ PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
|
||||||
switch (objtype)
|
switch (objtype)
|
||||||
{
|
{
|
||||||
case PyObjectsEnum::UIFRAME:
|
case PyObjectsEnum::UIFRAME:
|
||||||
if (((PyUIFrameObject*)self)->data->click_callable)
|
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
|
||||||
ptr = ((PyUIFrameObject*)self)->data->click_callable->borrow();
|
|
||||||
else
|
|
||||||
ptr = NULL;
|
|
||||||
break;
|
break;
|
||||||
case PyObjectsEnum::UICAPTION:
|
case PyObjectsEnum::UICAPTION:
|
||||||
if (((PyUICaptionObject*)self)->data->click_callable)
|
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
|
||||||
ptr = ((PyUICaptionObject*)self)->data->click_callable->borrow();
|
|
||||||
else
|
|
||||||
ptr = NULL;
|
|
||||||
break;
|
break;
|
||||||
case PyObjectsEnum::UISPRITE:
|
case PyObjectsEnum::UISPRITE:
|
||||||
if (((PyUISpriteObject*)self)->data->click_callable)
|
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
|
||||||
ptr = ((PyUISpriteObject*)self)->data->click_callable->borrow();
|
|
||||||
else
|
|
||||||
ptr = NULL;
|
|
||||||
break;
|
break;
|
||||||
case PyObjectsEnum::UIGRID:
|
case PyObjectsEnum::UIGRID:
|
||||||
if (((PyUIGridObject*)self)->data->click_callable)
|
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
|
||||||
ptr = ((PyUIGridObject*)self)->data->click_callable->borrow();
|
|
||||||
else
|
|
||||||
ptr = NULL;
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
PyErr_SetString(PyExc_TypeError, "no idea how you did that; invalid UIDrawable derived instance for _get_click");
|
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
|
// For now, Frame children will need manual sorting or collection modification
|
||||||
// to trigger a resort
|
// to trigger a resort
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIDrawable::get_name(PyObject* self, void* closure) {
|
|
||||||
PyObjectsEnum objtype = static_cast<PyObjectsEnum>(reinterpret_cast<long>(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<PyObjectsEnum>(reinterpret_cast<long>(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<sf::RenderTexture>();
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -44,8 +44,6 @@ public:
|
||||||
static int set_click(PyObject* self, PyObject* value, void* closure);
|
static int set_click(PyObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_int(PyObject* self, void* closure);
|
static PyObject* get_int(PyObject* self, void* closure);
|
||||||
static int set_int(PyObject* self, PyObject* value, 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)
|
// Z-order for rendering (lower values rendered first, higher values on top)
|
||||||
int z_index = 0;
|
int z_index = 0;
|
||||||
|
|
@ -53,18 +51,6 @@ public:
|
||||||
// Notification for z_index changes
|
// Notification for z_index changes
|
||||||
void notifyZIndexChanged();
|
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
|
// Animation support
|
||||||
virtual bool setProperty(const std::string& name, float value) { return false; }
|
virtual bool setProperty(const std::string& name, float value) { return false; }
|
||||||
virtual bool setProperty(const std::string& name, int 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::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, sf::Vector2f& value) const { return false; }
|
||||||
virtual bool getProperty(const std::string& name, std::string& 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<sf::RenderTexture> 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 {
|
typedef struct {
|
||||||
|
|
|
||||||
177
src/UIEntity.cpp
|
|
@ -1,20 +1,11 @@
|
||||||
#include "UIEntity.h"
|
#include "UIEntity.h"
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include <algorithm>
|
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyPositionHelper.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
|
||||||
#include "UIEntityPyMethods.h"
|
|
||||||
|
|
||||||
|
|
||||||
UIEntity::UIEntity()
|
UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it
|
||||||
: 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(UIGrid& grid)
|
UIEntity::UIEntity(UIGrid& grid)
|
||||||
: gridstate(grid.grid_x * grid.grid_y)
|
: 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) {
|
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", "pos", nullptr };
|
//static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr };
|
||||||
float x = 0.0f, y = 0.0f;
|
//float x = 0.0f, y = 0.0f, scale = 1.0f;
|
||||||
int sprite_index = 0; // Default to sprite index 0
|
static const char* keywords[] = { "pos", "texture", "sprite_index", "grid", nullptr };
|
||||||
|
PyObject* pos;
|
||||||
|
float scale = 1.0f;
|
||||||
|
int sprite_index = -1;
|
||||||
PyObject* texture = NULL;
|
PyObject* texture = NULL;
|
||||||
PyObject* grid = NULL;
|
PyObject* grid = NULL;
|
||||||
PyObject* pos_obj = NULL;
|
|
||||||
|
|
||||||
// Try to parse all arguments with keywords
|
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
|
||||||
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffOiOO",
|
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
|
||||||
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid, &pos_obj))
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
|
||||||
|
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
|
||||||
{
|
{
|
||||||
// If pos was provided, it overrides x,y
|
return -1;
|
||||||
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
|
|
||||||
|
PyVectorObject* pos_result = PyVector::from_arg(pos);
|
||||||
|
if (!pos_result)
|
||||||
{
|
{
|
||||||
PyErr_Clear();
|
PyErr_SetString(PyExc_TypeError, "pos must be a mcrfpy.Vector instance or arguments to mcrfpy.Vector.__init__");
|
||||||
|
return -1;
|
||||||
// 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<char**>(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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check types for texture
|
// check types for texture
|
||||||
|
|
@ -137,11 +104,10 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||||
texture_ptr = McRFPy_API::default_texture;
|
texture_ptr = McRFPy_API::default_texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow creation without texture for testing purposes
|
if (!texture_ptr) {
|
||||||
// if (!texture_ptr) {
|
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
|
||||||
// PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
|
return -1;
|
||||||
// return -1;
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
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);
|
Py_INCREF(self);
|
||||||
|
|
||||||
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
|
// 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);
|
||||||
self->data->sprite = UISprite(texture_ptr, sprite_index, sf::Vector2f(0,0), 1.0);
|
self->data->position = pos_result->data;
|
||||||
} 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<int>(x), static_cast<int>(y));
|
|
||||||
|
|
||||||
if (grid != NULL) {
|
if (grid != NULL) {
|
||||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
|
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
|
||||||
self->data->grid = pygrid->data;
|
self->data->grid = pygrid->data;
|
||||||
|
|
@ -287,106 +244,18 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
PyObject* UIEntity::get_float_member(PyUIEntityObject* self, void* closure)
|
|
||||||
{
|
|
||||||
auto member_ptr = reinterpret_cast<long>(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<long>(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<int>(val);
|
|
||||||
}
|
|
||||||
else if (member_ptr == 1) // y
|
|
||||||
{
|
|
||||||
self->data->position.y = val;
|
|
||||||
self->data->collision_pos.y = static_cast<int>(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<UIEntity>& 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[] = {
|
PyMethodDef UIEntity::methods[] = {
|
||||||
{"at", (PyCFunction)UIEntity::at, METH_O},
|
{"at", (PyCFunction)UIEntity::at, METH_O},
|
||||||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
{"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}
|
{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[] = {
|
PyGetSetDef UIEntity::getsetters[] = {
|
||||||
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
|
{"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},
|
{"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},
|
{"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_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},
|
{"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 */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -51,14 +51,8 @@ public:
|
||||||
bool setProperty(const std::string& name, int value);
|
bool setProperty(const std::string& name, int value);
|
||||||
bool getProperty(const std::string& name, float& value) const;
|
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* at(PyUIEntityObject* self, PyObject* o);
|
||||||
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
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 int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
static PyObject* get_position(PyUIEntityObject* self, void* closure);
|
static PyObject* get_position(PyUIEntityObject* self, void* closure);
|
||||||
|
|
@ -66,16 +60,11 @@ public:
|
||||||
static PyObject* get_gridstate(PyUIEntityObject* self, void* closure);
|
static PyObject* get_gridstate(PyUIEntityObject* self, void* closure);
|
||||||
static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure);
|
static PyObject* get_spritenumber(PyUIEntityObject* self, void* closure);
|
||||||
static int set_spritenumber(PyUIEntityObject* self, PyObject* value, 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 PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
static PyObject* repr(PyUIEntityObject* self);
|
static PyObject* repr(PyUIEntityObject* self);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration of methods array
|
|
||||||
extern PyMethodDef UIEntity_all_methods[];
|
|
||||||
|
|
||||||
namespace mcrfpydef {
|
namespace mcrfpydef {
|
||||||
static PyTypeObject PyUIEntityType = {
|
static PyTypeObject PyUIEntityType = {
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
.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_repr = (reprfunc)UIEntity::repr,
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
|
||||||
.tp_doc = "UIEntity objects",
|
.tp_doc = "UIEntity objects",
|
||||||
.tp_methods = UIEntity_all_methods,
|
.tp_methods = UIEntity::methods,
|
||||||
.tp_getset = UIEntity::getsetters,
|
.tp_getset = UIEntity::getsetters,
|
||||||
.tp_init = (initproc)UIEntity::init,
|
.tp_init = (initproc)UIEntity::init,
|
||||||
.tp_new = PyType_GenericNew,
|
.tp_new = PyType_GenericNew,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
351
src/UIFrame.cpp
|
|
@ -2,40 +2,21 @@
|
||||||
#include "UICollection.h"
|
#include "UICollection.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "PyVector.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)
|
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
// Check bounds first (optimization)
|
for (auto e: *children)
|
||||||
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) {
|
auto p = e->click_at(point + box.getPosition());
|
||||||
return nullptr;
|
if (p)
|
||||||
|
return p;
|
||||||
}
|
}
|
||||||
|
if (click_callable)
|
||||||
// Transform to local coordinates for children
|
{
|
||||||
sf::Vector2f localPoint = point - box.getPosition();
|
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;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return NULL;
|
||||||
// No child handled it, check if we have a handler
|
|
||||||
if (click_callable) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UIFrame::UIFrame()
|
UIFrame::UIFrame()
|
||||||
|
|
@ -64,95 +45,24 @@ PyObjectsEnum UIFrame::derived_type()
|
||||||
return PyObjectsEnum::UIFRAME;
|
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)
|
void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
{
|
{
|
||||||
// Check visibility
|
box.move(offset);
|
||||||
if (!visible) return;
|
//Resources::game->getWindow().draw(box);
|
||||||
|
target.draw(box);
|
||||||
// TODO: Apply opacity when SFML supports it on shapes
|
box.move(-offset);
|
||||||
|
|
||||||
// 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<unsigned int>(size.x),
|
|
||||||
static_cast<unsigned int>(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<UIDrawable>& a, const std::shared_ptr<UIDrawable>& 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);
|
|
||||||
|
|
||||||
// Sort children by z_index if needed
|
// Sort children by z_index if needed
|
||||||
if (children_need_sort && !children->empty()) {
|
if (children_need_sort && !children->empty()) {
|
||||||
std::sort(children->begin(), children->end(),
|
std::sort(children->begin(), children->end(),
|
||||||
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
|
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
|
||||||
return a->z_index < b->z_index;
|
return a->z_index < b->z_index;
|
||||||
});
|
});
|
||||||
children_need_sort = false;
|
children_need_sort = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (auto drawable : *children) {
|
for (auto drawable : *children) {
|
||||||
drawable->render(offset + box.getPosition(), target);
|
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.");
|
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
if (member_ptr == 0) { //x
|
if (member_ptr == 0) //x
|
||||||
self->data->box.setPosition(val, self->data->box.getPosition().y);
|
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->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));
|
self->data->box.setSize(sf::Vector2f(val, self->data->box.getSize().y));
|
||||||
if (self->data->use_render_texture) {
|
else if (member_ptr == 3) //h
|
||||||
// Need to recreate RenderTexture with new size
|
|
||||||
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
|
|
||||||
static_cast<unsigned int>(self->data->box.getSize().y));
|
|
||||||
}
|
|
||||||
self->data->markDirty();
|
|
||||||
}
|
|
||||||
else if (member_ptr == 3) { //h
|
|
||||||
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
|
self->data->box.setSize(sf::Vector2f(self->data->box.getSize().x, val));
|
||||||
if (self->data->use_render_texture) {
|
else if (member_ptr == 4) //outline
|
||||||
// Need to recreate RenderTexture with new size
|
|
||||||
self->data->enableRenderTexture(static_cast<unsigned int>(self->data->box.getSize().x),
|
|
||||||
static_cast<unsigned int>(self->data->box.getSize().y));
|
|
||||||
}
|
|
||||||
self->data->markDirty();
|
|
||||||
}
|
|
||||||
else if (member_ptr == 4) { //outline
|
|
||||||
self->data->box.setOutlineThickness(val);
|
self->data->box.setOutlineThickness(val);
|
||||||
self->data->markDirty();
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -311,12 +201,10 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
|
||||||
if (member_ptr == 0)
|
if (member_ptr == 0)
|
||||||
{
|
{
|
||||||
self->data->box.setFillColor(sf::Color(r, g, b, a));
|
self->data->box.setFillColor(sf::Color(r, g, b, a));
|
||||||
self->data->markDirty();
|
|
||||||
}
|
}
|
||||||
else if (member_ptr == 1)
|
else if (member_ptr == 1)
|
||||||
{
|
{
|
||||||
self->data->box.setOutlineColor(sf::Color(r, g, b, a));
|
self->data->box.setOutlineColor(sf::Color(r, g, b, a));
|
||||||
self->data->markDirty();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -346,40 +234,9 @@ int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
self->data->box.setPosition(vec->data);
|
self->data->box.setPosition(vec->data);
|
||||||
self->data->markDirty();
|
|
||||||
return 0;
|
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[] = {
|
PyGetSetDef UIFrame::getsetters[] = {
|
||||||
{"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"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}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -420,56 +274,35 @@ PyObject* UIFrame::repr(PyUIFrameObject* self)
|
||||||
|
|
||||||
int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
{
|
{
|
||||||
// Parse position using the standardized helper
|
//std::cout << "Init called\n";
|
||||||
auto pos_result = PyPositionHelper::parse_position(args, kwds);
|
const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", nullptr };
|
||||||
|
|
||||||
const char* keywords[] = { "x", "y", "w", "h", "fill_color", "outline_color", "outline", "children", "click", "pos", nullptr };
|
|
||||||
float x = 0.0f, y = 0.0f, w = 0.0f, h=0.0f, outline=0.0f;
|
float x = 0.0f, y = 0.0f, w = 0.0f, h=0.0f, outline=0.0f;
|
||||||
PyObject* fill_color = 0;
|
PyObject* fill_color = 0;
|
||||||
PyObject* outline_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
|
// First try to parse as (x, y, w, h, ...)
|
||||||
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffffOOfOOO", const_cast<char**>(keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast<char**>(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline))
|
||||||
&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
|
|
||||||
{
|
{
|
||||||
PyErr_Clear(); // Clear the error
|
PyErr_Clear(); // Clear the error
|
||||||
|
|
||||||
// Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...)
|
// 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_obj = nullptr;
|
||||||
PyObject* pos_arg = nullptr;
|
const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr };
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OffOOfOO", const_cast<char**>(alt_keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast<char**>(alt_keywords),
|
||||||
&pos_arg, &w, &h, &fill_color, &outline_color, &outline, &children_arg, &click_handler))
|
&pos_obj, &w, &h, &fill_color, &outline_color, &outline))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert position argument to x, y if provided
|
// Convert position argument to x, y
|
||||||
if (pos_arg && pos_arg != Py_None) {
|
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||||
PyVectorObject* vec = PyVector::from_arg(pos_arg);
|
if (!vec) {
|
||||||
if (!vec) {
|
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
|
||||||
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or tuple (x, y)");
|
return -1;
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
x = vec->data.x;
|
|
||||||
y = vec->data.y;
|
|
||||||
}
|
}
|
||||||
|
x = vec->data.x;
|
||||||
|
y = vec->data.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
self->data->box.setPosition(sf::Vector2f(x, 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);
|
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));
|
else self->data->box.setOutlineColor(sf::Color(128,128,128,255));
|
||||||
if (err_val) return err_val;
|
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<UIDrawable> 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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -554,81 +323,58 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
||||||
bool UIFrame::setProperty(const std::string& name, float value) {
|
bool UIFrame::setProperty(const std::string& name, float value) {
|
||||||
if (name == "x") {
|
if (name == "x") {
|
||||||
box.setPosition(sf::Vector2f(value, box.getPosition().y));
|
box.setPosition(sf::Vector2f(value, box.getPosition().y));
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "y") {
|
} else if (name == "y") {
|
||||||
box.setPosition(sf::Vector2f(box.getPosition().x, value));
|
box.setPosition(sf::Vector2f(box.getPosition().x, value));
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "w") {
|
} else if (name == "w") {
|
||||||
box.setSize(sf::Vector2f(value, box.getSize().y));
|
box.setSize(sf::Vector2f(value, box.getSize().y));
|
||||||
if (use_render_texture) {
|
|
||||||
// Need to recreate RenderTexture with new size
|
|
||||||
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
|
|
||||||
static_cast<unsigned int>(box.getSize().y));
|
|
||||||
}
|
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "h") {
|
} else if (name == "h") {
|
||||||
box.setSize(sf::Vector2f(box.getSize().x, value));
|
box.setSize(sf::Vector2f(box.getSize().x, value));
|
||||||
if (use_render_texture) {
|
|
||||||
// Need to recreate RenderTexture with new size
|
|
||||||
enableRenderTexture(static_cast<unsigned int>(box.getSize().x),
|
|
||||||
static_cast<unsigned int>(box.getSize().y));
|
|
||||||
}
|
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "outline") {
|
} else if (name == "outline") {
|
||||||
box.setOutlineThickness(value);
|
box.setOutlineThickness(value);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "fill_color.r") {
|
} else if (name == "fill_color.r") {
|
||||||
auto color = box.getFillColor();
|
auto color = box.getFillColor();
|
||||||
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setFillColor(color);
|
box.setFillColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "fill_color.g") {
|
} else if (name == "fill_color.g") {
|
||||||
auto color = box.getFillColor();
|
auto color = box.getFillColor();
|
||||||
color.g = std::clamp(static_cast<int>(value), 0, 255);
|
color.g = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setFillColor(color);
|
box.setFillColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "fill_color.b") {
|
} else if (name == "fill_color.b") {
|
||||||
auto color = box.getFillColor();
|
auto color = box.getFillColor();
|
||||||
color.b = std::clamp(static_cast<int>(value), 0, 255);
|
color.b = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setFillColor(color);
|
box.setFillColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "fill_color.a") {
|
} else if (name == "fill_color.a") {
|
||||||
auto color = box.getFillColor();
|
auto color = box.getFillColor();
|
||||||
color.a = std::clamp(static_cast<int>(value), 0, 255);
|
color.a = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setFillColor(color);
|
box.setFillColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "outline_color.r") {
|
} else if (name == "outline_color.r") {
|
||||||
auto color = box.getOutlineColor();
|
auto color = box.getOutlineColor();
|
||||||
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setOutlineColor(color);
|
box.setOutlineColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "outline_color.g") {
|
} else if (name == "outline_color.g") {
|
||||||
auto color = box.getOutlineColor();
|
auto color = box.getOutlineColor();
|
||||||
color.g = std::clamp(static_cast<int>(value), 0, 255);
|
color.g = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setOutlineColor(color);
|
box.setOutlineColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "outline_color.b") {
|
} else if (name == "outline_color.b") {
|
||||||
auto color = box.getOutlineColor();
|
auto color = box.getOutlineColor();
|
||||||
color.b = std::clamp(static_cast<int>(value), 0, 255);
|
color.b = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setOutlineColor(color);
|
box.setOutlineColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "outline_color.a") {
|
} else if (name == "outline_color.a") {
|
||||||
auto color = box.getOutlineColor();
|
auto color = box.getOutlineColor();
|
||||||
color.a = std::clamp(static_cast<int>(value), 0, 255);
|
color.a = std::clamp(static_cast<int>(value), 0, 255);
|
||||||
box.setOutlineColor(color);
|
box.setOutlineColor(color);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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) {
|
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
|
||||||
if (name == "fill_color") {
|
if (name == "fill_color") {
|
||||||
box.setFillColor(value);
|
box.setFillColor(value);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "outline_color") {
|
} else if (name == "outline_color") {
|
||||||
box.setOutlineColor(value);
|
box.setOutlineColor(value);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
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) {
|
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||||
if (name == "position") {
|
if (name == "position") {
|
||||||
box.setPosition(value);
|
box.setPosition(value);
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
} else if (name == "size") {
|
} else if (name == "size") {
|
||||||
box.setSize(value);
|
box.setSize(value);
|
||||||
if (use_render_texture) {
|
|
||||||
// Need to recreate RenderTexture with new size
|
|
||||||
enableRenderTexture(static_cast<unsigned int>(value.x),
|
|
||||||
static_cast<unsigned int>(value.y));
|
|
||||||
}
|
|
||||||
markDirty();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -29,16 +29,10 @@ public:
|
||||||
float outline;
|
float outline;
|
||||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
|
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
|
||||||
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
|
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 render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||||
void move(sf::Vector2f);
|
void move(sf::Vector2f);
|
||||||
PyObjectsEnum derived_type() override final;
|
PyObjectsEnum derived_type() override final;
|
||||||
virtual UIDrawable* click_at(sf::Vector2f point) 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);
|
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 int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
|
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
|
||||||
static int set_pos(PyUIFrameObject* self, PyObject* value, 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 PyGetSetDef getsetters[];
|
||||||
static PyObject* repr(PyUIFrameObject* self);
|
static PyObject* repr(PyUIFrameObject* self);
|
||||||
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
|
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;
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Forward declaration of methods array
|
|
||||||
extern PyMethodDef UIFrame_methods[];
|
|
||||||
|
|
||||||
namespace mcrfpydef {
|
namespace mcrfpydef {
|
||||||
static PyTypeObject PyUIFrameType = {
|
static PyTypeObject PyUIFrameType = {
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
|
@ -85,7 +74,7 @@ namespace mcrfpydef {
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("docstring"),
|
.tp_doc = PyDoc_STR("docstring"),
|
||||||
.tp_methods = UIFrame_methods,
|
//.tp_methods = PyUIFrame_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
.tp_getset = UIFrame::getsetters,
|
.tp_getset = UIFrame::getsetters,
|
||||||
//.tp_base = NULL,
|
//.tp_base = NULL,
|
||||||
|
|
|
||||||
432
src/UIGrid.cpp
|
|
@ -1,38 +1,14 @@
|
||||||
#include "UIGrid.h"
|
#include "UIGrid.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PyPositionHelper.h"
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
// UIDrawable methods now in UIBase.h
|
|
||||||
|
|
||||||
UIGrid::UIGrid()
|
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<std::list<std::shared_ptr<UIEntity>>>();
|
|
||||||
|
|
||||||
// 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(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
|
UIGrid::UIGrid(int gx, int gy, std::shared_ptr<PyTexture> _ptex, sf::Vector2f _xy, sf::Vector2f _wh)
|
||||||
: grid_x(gx), grid_y(gy),
|
: grid_x(gx), grid_y(gy),
|
||||||
zoom(1.0f),
|
zoom(1.0f),
|
||||||
ptex(_ptex), points(gx * gy),
|
ptex(_ptex), points(gx * gy)
|
||||||
background_color(8, 8, 8, 255) // Default dark gray background
|
|
||||||
{
|
{
|
||||||
// Use texture dimensions if available, otherwise use defaults
|
// Use texture dimensions if available, otherwise use defaults
|
||||||
int cell_width = _ptex ? _ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
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)
|
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.setPosition(box.getPosition() + offset); // output sprite can move; update position when drawing
|
||||||
// output size can change; update size when drawing
|
// output size can change; update size when drawing
|
||||||
output.setTextureRect(
|
output.setTextureRect(
|
||||||
sf::IntRect(0, 0,
|
sf::IntRect(0, 0,
|
||||||
box.getSize().x, box.getSize().y));
|
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
|
// Get cell dimensions - use texture if available, otherwise defaults
|
||||||
int cell_width = ptex ? ptex->sprite_width : DEFAULT_CELL_WIDTH;
|
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
|
// middle layer - entities
|
||||||
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
// disabling entity rendering until I can render their UISprite inside the rendertexture (not directly to window)
|
||||||
for (auto e : *entities) {
|
for (auto e : *entities) {
|
||||||
// Skip out-of-bounds entities for performance
|
// TODO skip out-of-bounds entities (grid square not visible at all, check for partially on visible grid squares / floating point grid position)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
//auto drawent = e->cGrid->indexsprite.drawable();
|
//auto drawent = e->cGrid->indexsprite.drawable();
|
||||||
auto& drawent = e->sprite;
|
auto& drawent = e->sprite;
|
||||||
//drawent.setScale(zoom, zoom);
|
//drawent.setScale(zoom, zoom);
|
||||||
|
|
@ -237,29 +202,6 @@ PyObjectsEnum UIGrid::derived_type()
|
||||||
return PyObjectsEnum::UIGRID;
|
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<unsigned int>(w), static_cast<unsigned int>(h));
|
|
||||||
output.setTexture(renderTexture.getTexture());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<PyTexture> UIGrid::getTexture()
|
std::shared_ptr<PyTexture> UIGrid::getTexture()
|
||||||
{
|
{
|
||||||
return ptex;
|
return ptex;
|
||||||
|
|
@ -267,110 +209,24 @@ std::shared_ptr<PyTexture> UIGrid::getTexture()
|
||||||
|
|
||||||
UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
UIDrawable* UIGrid::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
// Check grid bounds first
|
if (click_callable)
|
||||||
if (!box.getGlobalBounds().contains(point)) {
|
{
|
||||||
return nullptr;
|
if(box.getGlobalBounds().contains(point)) return this;
|
||||||
}
|
}
|
||||||
|
return NULL;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
int UIGrid::init(PyUIGridObject* self, PyObject* args, PyObject* kwds) {
|
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;
|
PyObject* textureObj = Py_None;
|
||||||
|
//float box_x, box_y, box_w, box_h;
|
||||||
PyObject* pos = NULL;
|
PyObject* pos = NULL;
|
||||||
PyObject* size = 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_ParseTuple(args, "iiOffff", &grid_x, &grid_y, &textureObj, &box_x, &box_y, &box_w, &box_h)) {
|
||||||
if (PyArg_ParseTupleAndKeywords(args, kwds, "|iiOOOO", const_cast<char**>(keywords),
|
if (!PyArg_ParseTuple(args, "ii|OOO", &grid_x, &grid_y, &textureObj, &pos, &size)) {
|
||||||
&grid_x, &grid_y, &textureObj, &pos, &size, &grid_size_obj)) {
|
return -1; // If parsing fails, return an error
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default position and size if not provided
|
// Default position and size if not provided
|
||||||
|
|
@ -619,20 +475,13 @@ PyObject* UIGrid::get_texture(PyUIGridObject* self, void* closure) {
|
||||||
return (PyObject*)obj;
|
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
|
int x, y;
|
||||||
auto result = PyPositionHelper::parse_position_int(args, kwds);
|
if (!PyArg_ParseTuple(o, "ii", &x, &y)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "UIGrid.at requires two integer arguments: (x, y)");
|
||||||
if (!result.has_position) {
|
|
||||||
PyPositionHelper::set_position_int_error();
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
int x = result.x;
|
|
||||||
int y = result.y;
|
|
||||||
|
|
||||||
// Range validation
|
|
||||||
if (x < 0 || x >= self->data->grid_x) {
|
if (x < 0 || x >= self->data->grid_x) {
|
||||||
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)");
|
PyErr_SetString(PyExc_ValueError, "x value out of range (0, Grid.grid_x)");
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
@ -651,43 +500,11 @@ PyObject* UIGrid::py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds)
|
||||||
return (PyObject*)obj;
|
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[] = {
|
PyMethodDef UIGrid::methods[] = {
|
||||||
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS | METH_KEYWORDS},
|
{"at", (PyCFunction)UIGrid::py_at, METH_VARARGS},
|
||||||
{NULL, NULL, 0, NULL}
|
{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[] = {
|
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},
|
{"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
|
{"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},
|
{"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 */
|
{NULL} /* Sentinel */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -1026,6 +840,184 @@ PyObject* UIEntityCollection::inplace_concat(PyUIEntityCollectionObject* self, P
|
||||||
return (PyObject*)self;
|
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 = {
|
PySequenceMethods UIEntityCollection::sqmethods = {
|
||||||
.sq_length = (lenfunc)UIEntityCollection::len,
|
.sq_length = (lenfunc)UIEntityCollection::len,
|
||||||
|
|
@ -1481,22 +1473,6 @@ bool UIGrid::setProperty(const std::string& name, float value) {
|
||||||
z_index = static_cast<int>(value);
|
z_index = static_cast<int>(value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (name == "background_color.r") {
|
|
||||||
background_color.r = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (name == "background_color.g") {
|
|
||||||
background_color.g = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (name == "background_color.b") {
|
|
||||||
background_color.b = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (name == "background_color.a") {
|
|
||||||
background_color.a = static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, value)));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1552,22 +1528,6 @@ bool UIGrid::getProperty(const std::string& name, float& value) const {
|
||||||
value = static_cast<float>(z_index);
|
value = static_cast<float>(z_index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (name == "background_color.r") {
|
|
||||||
value = static_cast<float>(background_color.r);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (name == "background_color.g") {
|
|
||||||
value = static_cast<float>(background_color.g);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (name == "background_color.b") {
|
|
||||||
value = static_cast<float>(background_color.b);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (name == "background_color.a") {
|
|
||||||
value = static_cast<float>(background_color.a);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
src/UIGrid.h
|
|
@ -34,11 +34,6 @@ public:
|
||||||
PyObjectsEnum derived_type() override final;
|
PyObjectsEnum derived_type() override final;
|
||||||
//void setSprite(int);
|
//void setSprite(int);
|
||||||
virtual UIDrawable* click_at(sf::Vector2f point) 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;
|
|
||||||
|
|
||||||
int grid_x, grid_y;
|
int grid_x, grid_y;
|
||||||
//int grid_size; // grid sizes are implied by IndexTexture now
|
//int grid_size; // grid sizes are implied by IndexTexture now
|
||||||
|
|
@ -51,9 +46,6 @@ public:
|
||||||
std::vector<UIGridPoint> points;
|
std::vector<UIGridPoint> points;
|
||||||
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
|
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
|
||||||
|
|
||||||
// Background rendering
|
|
||||||
sf::Color background_color;
|
|
||||||
|
|
||||||
// Property system for animations
|
// Property system for animations
|
||||||
bool setProperty(const std::string& name, float value) override;
|
bool setProperty(const std::string& name, float value) override;
|
||||||
bool setProperty(const std::string& name, const sf::Vector2f& 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 PyObject* get_float_member(PyUIGridObject* self, void* closure);
|
||||||
static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure);
|
static int set_float_member(PyUIGridObject* self, PyObject* value, void* closure);
|
||||||
static PyObject* get_texture(PyUIGridObject* self, void* closure);
|
static PyObject* get_texture(PyUIGridObject* self, void* closure);
|
||||||
static PyObject* get_background_color(PyUIGridObject* self, void* closure);
|
static PyObject* py_at(PyUIGridObject* self, PyObject* o);
|
||||||
static int set_background_color(PyUIGridObject* self, PyObject* value, void* closure);
|
|
||||||
static PyObject* py_at(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
|
||||||
static PyMethodDef methods[];
|
static PyMethodDef methods[];
|
||||||
static PyGetSetDef getsetters[];
|
static PyGetSetDef getsetters[];
|
||||||
static PyObject* get_children(PyUIGridObject* self, void* closure);
|
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 {
|
namespace mcrfpydef {
|
||||||
static PyTypeObject PyUIGridType = {
|
static PyTypeObject PyUIGridType = {
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
|
@ -150,7 +137,7 @@ namespace mcrfpydef {
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("docstring"),
|
.tp_doc = PyDoc_STR("docstring"),
|
||||||
.tp_methods = UIGrid_all_methods,
|
.tp_methods = UIGrid::methods,
|
||||||
//.tp_members = UIGrid::members,
|
//.tp_members = UIGrid::members,
|
||||||
.tp_getset = UIGrid::getsetters,
|
.tp_getset = UIGrid::getsetters,
|
||||||
//.tp_base = NULL,
|
//.tp_base = NULL,
|
||||||
|
|
|
||||||
104
src/UISprite.cpp
|
|
@ -1,8 +1,6 @@
|
||||||
#include "UISprite.h"
|
#include "UISprite.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
#include "PyVector.h"
|
#include "PyVector.h"
|
||||||
#include "PyPositionHelper.h"
|
|
||||||
// UIDrawable methods now in UIBase.h
|
|
||||||
|
|
||||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||||
{
|
{
|
||||||
|
|
@ -13,13 +11,7 @@ UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
UISprite::UISprite()
|
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(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vector2f _pos, float _scale)
|
UISprite::UISprite(std::shared_ptr<PyTexture> _ptex, int _sprite_index, sf::Vector2f _pos, float _scale)
|
||||||
: ptex(_ptex), sprite_index(_sprite_index)
|
: 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)
|
void UISprite::render(sf::Vector2f offset, sf::RenderTarget& target)
|
||||||
{
|
{
|
||||||
// Check visibility
|
|
||||||
if (!visible) return;
|
|
||||||
|
|
||||||
// Apply opacity
|
|
||||||
auto color = sprite.getColor();
|
|
||||||
color.a = static_cast<sf::Uint8>(255 * opacity);
|
|
||||||
sprite.setColor(color);
|
|
||||||
|
|
||||||
sprite.move(offset);
|
sprite.move(offset);
|
||||||
target.draw(sprite);
|
target.draw(sprite);
|
||||||
sprite.move(-offset);
|
sprite.move(-offset);
|
||||||
|
|
||||||
// Restore original alpha
|
|
||||||
color.a = 255;
|
|
||||||
sprite.setColor(color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void UISprite::setPosition(sf::Vector2f pos)
|
void UISprite::setPosition(sf::Vector2f pos)
|
||||||
|
|
@ -104,28 +84,6 @@ PyObjectsEnum UISprite::derived_type()
|
||||||
return PyObjectsEnum::UISPRITE;
|
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)
|
PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure)
|
||||||
{
|
{
|
||||||
auto member_ptr = reinterpret_cast<long>(closure);
|
auto member_ptr = reinterpret_cast<long>(closure);
|
||||||
|
|
@ -268,15 +226,6 @@ int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the PyObjectType alias for the macros
|
|
||||||
typedef PyUISpriteObject PyObjectType;
|
|
||||||
|
|
||||||
// Method definitions
|
|
||||||
PyMethodDef UISprite_methods[] = {
|
|
||||||
UIDRAWABLE_METHODS,
|
|
||||||
{NULL} // Sentinel
|
|
||||||
};
|
|
||||||
|
|
||||||
PyGetSetDef UISprite::getsetters[] = {
|
PyGetSetDef UISprite::getsetters[] = {
|
||||||
{"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"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},
|
{"pos", (getter)UISprite::get_pos, (setter)UISprite::set_pos, "Position as a Vector", NULL},
|
||||||
UIDRAWABLE_GETSETTERS,
|
|
||||||
{NULL}
|
{NULL}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -310,47 +257,33 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
|
||||||
|
|
||||||
int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
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;
|
float x = 0.0f, y = 0.0f, scale = 1.0f;
|
||||||
int sprite_index = 0;
|
int sprite_index = 0;
|
||||||
PyObject* texture = NULL;
|
PyObject* texture = NULL;
|
||||||
PyObject* click_handler = NULL;
|
|
||||||
PyObject* pos_obj = NULL;
|
|
||||||
|
|
||||||
// Try to parse all arguments with keywords
|
// First try to parse as (x, y, texture, ...)
|
||||||
if (PyArg_ParseTupleAndKeywords(args, kwds, "|ffOifOO",
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif",
|
||||||
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale, &click_handler, &pos_obj))
|
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale))
|
||||||
{
|
|
||||||
// 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
|
|
||||||
{
|
{
|
||||||
PyErr_Clear(); // Clear the error
|
PyErr_Clear(); // Clear the error
|
||||||
|
|
||||||
// Try alternative: first arg is pos tuple/Vector
|
// Try to parse as ((x,y), texture, ...) or (Vector, texture, ...)
|
||||||
const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", "click", nullptr };
|
PyObject* pos_obj = nullptr;
|
||||||
PyObject* pos = NULL;
|
const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr };
|
||||||
|
|
||||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOifO", const_cast<char**>(alt_keywords),
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast<char**>(alt_keywords),
|
||||||
&pos, &texture, &sprite_index, &scale, &click_handler))
|
&pos_obj, &texture, &sprite_index, &scale))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert position argument to x, y
|
// Convert position argument to x, y
|
||||||
if (pos && pos != Py_None) {
|
if (pos_obj) {
|
||||||
PyVectorObject* vec = PyVector::from_arg(pos);
|
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||||
if (!vec) {
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
x = vec->data.x;
|
x = vec->data.x;
|
||||||
|
|
@ -379,15 +312,6 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
||||||
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
|
self->data = std::make_shared<UISprite>(texture_ptr, sprite_index, sf::Vector2f(x, y), scale);
|
||||||
self->data->setPosition(sf::Vector2f(x, y));
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,6 @@ public:
|
||||||
|
|
||||||
PyObjectsEnum derived_type() override final;
|
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
|
// Property system for animations
|
||||||
bool setProperty(const std::string& name, float value) override;
|
bool setProperty(const std::string& name, float value) override;
|
||||||
bool setProperty(const std::string& name, int 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 {
|
namespace mcrfpydef {
|
||||||
static PyTypeObject PyUISpriteType = {
|
static PyTypeObject PyUISpriteType = {
|
||||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
|
@ -91,7 +83,7 @@ namespace mcrfpydef {
|
||||||
//.tp_iternext
|
//.tp_iternext
|
||||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||||
.tp_doc = PyDoc_STR("docstring"),
|
.tp_doc = PyDoc_STR("docstring"),
|
||||||
.tp_methods = UISprite_methods,
|
//.tp_methods = PyUIFrame_methods,
|
||||||
//.tp_members = PyUIFrame_members,
|
//.tp_members = PyUIFrame_members,
|
||||||
.tp_getset = UISprite::getsetters,
|
.tp_getset = UISprite::getsetters,
|
||||||
//.tp_base = NULL,
|
//.tp_base = NULL,
|
||||||
|
|
|
||||||
13
src/main.cpp
|
|
@ -41,9 +41,6 @@ int run_game_engine(const McRogueFaceConfig& config)
|
||||||
{
|
{
|
||||||
GameEngine g(config);
|
GameEngine g(config);
|
||||||
g.run();
|
g.run();
|
||||||
if (Py_IsInitialized()) {
|
|
||||||
McRFPy_API::api_shutdown();
|
|
||||||
}
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -105,7 +102,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
||||||
// Continue to interactive mode below
|
// Continue to interactive mode below
|
||||||
} else {
|
} else {
|
||||||
int result = PyRun_SimpleString(config.python_command.c_str());
|
int result = PyRun_SimpleString(config.python_command.c_str());
|
||||||
McRFPy_API::api_shutdown();
|
Py_Finalize();
|
||||||
delete engine;
|
delete engine;
|
||||||
return result;
|
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";
|
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());
|
int result = PyRun_SimpleString(run_module_code.c_str());
|
||||||
McRFPy_API::api_shutdown();
|
Py_Finalize();
|
||||||
delete engine;
|
delete engine;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
@ -182,7 +179,7 @@ int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv
|
||||||
// Run the game engine after script execution
|
// Run the game engine after script execution
|
||||||
engine->run();
|
engine->run();
|
||||||
|
|
||||||
McRFPy_API::api_shutdown();
|
Py_Finalize();
|
||||||
delete engine;
|
delete engine;
|
||||||
return result;
|
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)
|
// Interactive Python interpreter (only if explicitly requested with -i)
|
||||||
Py_InspectFlag = 1;
|
Py_InspectFlag = 1;
|
||||||
PyRun_InteractiveLoop(stdin, "<stdin>");
|
PyRun_InteractiveLoop(stdin, "<stdin>");
|
||||||
McRFPy_API::api_shutdown();
|
Py_Finalize();
|
||||||
delete engine;
|
delete engine;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
else if (!config.exec_scripts.empty()) {
|
else if (!config.exec_scripts.empty()) {
|
||||||
// With --exec, run the game engine after scripts execute
|
// With --exec, run the game engine after scripts execute
|
||||||
engine->run();
|
engine->run();
|
||||||
McRFPy_API::api_shutdown();
|
Py_Finalize();
|
||||||
delete engine;
|
delete engine;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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...")
|
|
||||||
|
|
@ -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...")
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -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<UIDrawable*> getClickableChildren() override {
|
|
||||||
std::vector<UIDrawable*> 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<UIDrawable*> getClickableChildren() override {
|
|
||||||
std::vector<UIDrawable*> 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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||