Compare commits
5 Commits
167636ce8c
...
99f301e3a0
Author | SHA1 | Date |
---|---|---|
|
99f301e3a0 | |
|
2f2b488fb5 | |
|
5a003a9aa5 | |
|
e5affaf317 | |
|
d03182d347 |
|
@ -0,0 +1,99 @@
|
|||
#!/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)
|
|
@ -0,0 +1,61 @@
|
|||
#!/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)
|
|
@ -0,0 +1,105 @@
|
|||
#!/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)
|
|
@ -0,0 +1,111 @@
|
|||
#!/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)
|
|
@ -0,0 +1,101 @@
|
|||
#!/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)
|
|
@ -0,0 +1,77 @@
|
|||
#!/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)
|
|
@ -0,0 +1,60 @@
|
|||
#!/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)
|
|
@ -0,0 +1,87 @@
|
|||
#!/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)
|
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
|
@ -0,0 +1,73 @@
|
|||
#!/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)
|
|
@ -10,3 +10,20 @@ build
|
|||
lib
|
||||
obj
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.md
|
||||
*.zip
|
||||
__lib/
|
||||
_oldscripts/
|
||||
assets/
|
||||
cellular_automata_fire/
|
||||
*.txt
|
||||
deps/
|
||||
fetch_issues_txt.py
|
||||
forest_fire_CA.py
|
||||
mcrogueface.github.io
|
||||
scripts/
|
||||
test_*
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
# Convenience Makefile wrapper for McRogueFace
|
||||
# This delegates to CMake build in the build directory
|
||||
|
||||
.PHONY: all build clean run test dist help
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
# Build the project
|
||||
build:
|
||||
@./build.sh
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@./clean.sh
|
||||
|
||||
# Run the game
|
||||
run: build
|
||||
@cd build && ./mcrogueface
|
||||
|
||||
# Run in Python mode
|
||||
python: build
|
||||
@cd build && ./mcrogueface -i
|
||||
|
||||
# Test basic functionality
|
||||
test: build
|
||||
@echo "Testing McRogueFace..."
|
||||
@cd build && ./mcrogueface -V
|
||||
@cd build && ./mcrogueface -c "print('Test passed')"
|
||||
@cd build && ./mcrogueface --headless -c "import mcrfpy; print('mcrfpy imported successfully')"
|
||||
|
||||
# Create distribution archive
|
||||
dist: build
|
||||
@echo "Creating distribution archive..."
|
||||
@cd build && zip -r ../McRogueFace-$$(date +%Y%m%d).zip . -x "*.o" "CMakeFiles/*" "Makefile" "*.cmake"
|
||||
@echo "Distribution archive created: McRogueFace-$$(date +%Y%m%d).zip"
|
||||
|
||||
# Show help
|
||||
help:
|
||||
@echo "McRogueFace Build System"
|
||||
@echo "======================="
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " make - Build the project (default)"
|
||||
@echo " make build - Build the project"
|
||||
@echo " make clean - Remove all build artifacts"
|
||||
@echo " make run - Build and run the game"
|
||||
@echo " make python - Build and run in Python interactive mode"
|
||||
@echo " make test - Run basic tests"
|
||||
@echo " make dist - Create distribution archive"
|
||||
@echo " make help - Show this help message"
|
||||
@echo ""
|
||||
@echo "Build output goes to: ./build/"
|
||||
@echo "Distribution archives are created in project root"
|
104
README.md
|
@ -1,30 +1,88 @@
|
|||
# McRogueFace - 2D Game Engine
|
||||
|
||||
An experimental prototype game engine built for my own use in 7DRL 2023.
|
||||
|
||||
# McRogueFace
|
||||
*Blame my wife for the name*
|
||||
|
||||
## Tenets:
|
||||
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
|
||||
|
||||
* C++ first, Python close behind.
|
||||
* Entity-Component system based on David Churchill's Memorial University COMP4300 course lectures available on Youtube.
|
||||
* Graphics, particles and shaders provided by SFML.
|
||||
* Pathfinding, noise generation, and other Roguelike goodness provided by TCOD.
|
||||
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
|
||||
|
||||
## Why?
|
||||
## Tenets
|
||||
|
||||
I did the r/RoguelikeDev TCOD tutorial in Python. I loved it, but I did not want to be limited to ASCII. I want to be able to draw pixels on top of my tiles (like lines or circles) and eventually incorporate even more polish.
|
||||
- **Python & C++ Hand-in-Hand**: Create your game without ever recompiling. Your Python commands create C++ objects, and animations can occur without calling Python at all.
|
||||
- **Simple Yet Flexible UI System**: Sprites, Grids, Frames, and Captions with full animation support
|
||||
- **Entity-Component Architecture**: Implement your game objects with Python integration
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod (demos still under construction)
|
||||
- **Automation API**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
|
||||
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
|
||||
|
||||
## To-do
|
||||
## Quick Start
|
||||
|
||||
* ✅ Initial Commit
|
||||
* ✅ Integrate scene, action, entity, component system from COMP4300 engine
|
||||
* ✅ Windows / Visual Studio project
|
||||
* ✅ Draw Sprites
|
||||
* ✅ Play Sounds
|
||||
* ✅ Draw UI, spawn entity from Python code
|
||||
* ❌ Python AI for entities (NPCs on set paths, enemies towards player)
|
||||
* ✅ Walking / Collision
|
||||
* ❌ "Boards" (stairs / doors / walk off edge of screen)
|
||||
* ❌ Cutscenes - interrupt normal controls, text scroll, character portraits
|
||||
* ❌ Mouse integration - tooltips, zoom, click to select targets, cursors
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone <wherever you found this repo>
|
||||
cd McRogueFace
|
||||
make
|
||||
|
||||
# Run the example game
|
||||
cd build
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
## Example: Creating a Simple Scene
|
||||
|
||||
```python
|
||||
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")
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For comprehensive documentation, tutorials, and API reference, visit:
|
||||
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
|
||||
|
||||
## Requirements
|
||||
|
||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||
- CMake 3.14+
|
||||
- Python 3.12+
|
||||
- SFML 2.5+
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── src/ # C++ engine source
|
||||
├── scripts/ # Python game scripts
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output directory
|
||||
└── tests/ # Automated test suite
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
PRs will be considered! Please include explicit mention that your contribution is your own work and released under the MIT license in the pull request.
|
||||
|
||||
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Developed for 7-Day Roguelike 2023, 2024, 2025 - here's to many more
|
||||
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
|
||||
- Inspired by David Churchill's COMP4300 game engine lectures
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
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")
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
#!/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!")
|
|
@ -0,0 +1,336 @@
|
|||
#!/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")
|
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
# Build script for McRogueFace - compiles everything into ./build directory
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}McRogueFace Build Script${NC}"
|
||||
echo "========================="
|
||||
|
||||
# Create build directory if it doesn't exist
|
||||
if [ ! -d "build" ]; then
|
||||
echo -e "${YELLOW}Creating build directory...${NC}"
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
# Change to build directory
|
||||
cd build
|
||||
|
||||
# Run CMake to generate build files
|
||||
echo -e "${YELLOW}Running CMake...${NC}"
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Check if CMake succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}CMake configuration failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Run make with parallel jobs
|
||||
echo -e "${YELLOW}Building with make...${NC}"
|
||||
make -j$(nproc)
|
||||
|
||||
# Check if make succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}Build completed successfully!${NC}"
|
||||
echo ""
|
||||
echo "The build directory contains:"
|
||||
ls -la
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}To run McRogueFace:${NC}"
|
||||
echo " cd build"
|
||||
echo " ./mcrogueface"
|
||||
echo ""
|
||||
echo -e "${GREEN}To create a distribution archive:${NC}"
|
||||
echo " cd build"
|
||||
echo " zip -r ../McRogueFace-$(date +%Y%m%d).zip ."
|
|
@ -0,0 +1,33 @@
|
|||
#!/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}"
|
|
@ -0,0 +1,112 @@
|
|||
[
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/GameEngine.cpp.o -c /home/john/Development/McRogueFace/src/GameEngine.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/GameEngine.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/IndexTexture.cpp.o -c /home/john/Development/McRogueFace/src/IndexTexture.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/IndexTexture.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/McRFPy_API.cpp.o -c /home/john/Development/McRogueFace/src/McRFPy_API.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/McRFPy_API.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyCallable.cpp.o -c /home/john/Development/McRogueFace/src/PyCallable.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyCallable.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyColor.cpp.o -c /home/john/Development/McRogueFace/src/PyColor.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyColor.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyFont.cpp.o -c /home/john/Development/McRogueFace/src/PyFont.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyFont.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyScene.cpp.o -c /home/john/Development/McRogueFace/src/PyScene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyScene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyTexture.cpp.o -c /home/john/Development/McRogueFace/src/PyTexture.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyTexture.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/PyVector.cpp.o -c /home/john/Development/McRogueFace/src/PyVector.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/PyVector.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Resources.cpp.o -c /home/john/Development/McRogueFace/src/Resources.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Resources.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Scene.cpp.o -c /home/john/Development/McRogueFace/src/Scene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Scene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/Timer.cpp.o -c /home/john/Development/McRogueFace/src/Timer.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/Timer.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICaption.cpp.o -c /home/john/Development/McRogueFace/src/UICaption.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UICaption.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UICollection.cpp.o -c /home/john/Development/McRogueFace/src/UICollection.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UICollection.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIDrawable.cpp.o -c /home/john/Development/McRogueFace/src/UIDrawable.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIDrawable.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIEntity.cpp.o -c /home/john/Development/McRogueFace/src/UIEntity.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIEntity.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIFrame.cpp.o -c /home/john/Development/McRogueFace/src/UIFrame.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIFrame.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGrid.cpp.o -c /home/john/Development/McRogueFace/src/UIGrid.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIGrid.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UIGridPoint.cpp.o -c /home/john/Development/McRogueFace/src/UIGridPoint.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UIGridPoint.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UISprite.cpp.o -c /home/john/Development/McRogueFace/src/UISprite.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UISprite.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/UITestScene.cpp.o -c /home/john/Development/McRogueFace/src/UITestScene.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/UITestScene.cpp"
|
||||
},
|
||||
{
|
||||
"directory": "/home/john/Development/McRogueFace/build",
|
||||
"command": "/usr/bin/c++ -I/home/john/Development/McRogueFace/deps -I/home/john/Development/McRogueFace/deps/libtcod -I/home/john/Development/McRogueFace/deps/cpython -I/home/john/Development/McRogueFace/deps/Python -I/home/john/Development/McRogueFace/deps/platform/linux -g -std=gnu++2a -o CMakeFiles/mcrogueface.dir/src/main.cpp.o -c /home/john/Development/McRogueFace/src/main.cpp",
|
||||
"file": "/home/john/Development/McRogueFace/src/main.cpp"
|
||||
}
|
||||
]
|
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 30 KiB |
|
@ -0,0 +1,63 @@
|
|||
#!/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")
|
|
@ -0,0 +1,53 @@
|
|||
#!/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")
|
|
@ -0,0 +1,69 @@
|
|||
#!/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")
|
|
@ -0,0 +1,189 @@
|
|||
// 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
|
|
@ -0,0 +1,102 @@
|
|||
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()
|
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 30 KiB |
|
@ -11,10 +11,10 @@ public:
|
|||
const static int WHEEL_NUM = 4;
|
||||
const static int WHEEL_NEG = 2;
|
||||
const static int WHEEL_DEL = 1;
|
||||
static int keycode(sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||
static int keycode(sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
||||
static int keycode(const sf::Keyboard::Key& k) { return KEY + (int)k; }
|
||||
static int keycode(const sf::Mouse::Button& b) { return MOUSEBUTTON + (int)b; }
|
||||
//static int keycode(sf::Mouse::Wheel& w, float d) { return MOUSEWHEEL + (((int)w)<<12) + int(d*16) + 512; }
|
||||
static int keycode(sf::Mouse::Wheel& w, float d) {
|
||||
static int keycode(const sf::Mouse::Wheel& w, float d) {
|
||||
int neg = 0;
|
||||
if (d < 0) { neg = 1; }
|
||||
return MOUSEWHEEL + (w * WHEEL_NUM) + (neg * WHEEL_NEG) + 1;
|
||||
|
@ -32,7 +32,7 @@ public:
|
|||
return (a & WHEEL_DEL) * factor;
|
||||
}
|
||||
|
||||
static std::string key_str(sf::Keyboard::Key& keycode)
|
||||
static std::string key_str(const sf::Keyboard::Key& keycode)
|
||||
{
|
||||
switch(keycode)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,527 @@
|
|||
#include "Animation.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIEntity.h"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
// Animation implementation
|
||||
Animation::Animation(const std::string& targetProperty,
|
||||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc,
|
||||
bool delta)
|
||||
: targetProperty(targetProperty)
|
||||
, targetValue(targetValue)
|
||||
, duration(duration)
|
||||
, easingFunc(easingFunc)
|
||||
, delta(delta)
|
||||
{
|
||||
}
|
||||
|
||||
void Animation::start(UIDrawable* target) {
|
||||
currentTarget = target;
|
||||
elapsed = 0.0f;
|
||||
|
||||
// Capture startValue from target based on targetProperty
|
||||
if (!currentTarget) return;
|
||||
|
||||
// Try to get the current value based on the expected type
|
||||
std::visit([this](const auto& targetVal) {
|
||||
using T = std::decay_t<decltype(targetVal)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
float value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
int value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
||||
// For sprite animation, get current sprite index
|
||||
int value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
sf::Color value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
sf::Vector2f value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
std::string value;
|
||||
if (currentTarget->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
}, targetValue);
|
||||
}
|
||||
|
||||
void Animation::startEntity(UIEntity* target) {
|
||||
currentEntityTarget = target;
|
||||
currentTarget = nullptr; // Clear drawable target
|
||||
elapsed = 0.0f;
|
||||
|
||||
// Capture the starting value from the entity
|
||||
std::visit([this, target](const auto& val) {
|
||||
using T = std::decay_t<decltype(val)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
float value = 0.0f;
|
||||
if (target->getProperty(targetProperty, value)) {
|
||||
startValue = value;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
// For entities, we might need to handle sprite_index differently
|
||||
if (targetProperty == "sprite_index" || targetProperty == "sprite_number") {
|
||||
startValue = target->sprite.getSpriteIndex();
|
||||
}
|
||||
}
|
||||
// Entities don't support other types yet
|
||||
}, targetValue);
|
||||
}
|
||||
|
||||
bool Animation::update(float deltaTime) {
|
||||
if ((!currentTarget && !currentEntityTarget) || isComplete()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
elapsed += deltaTime;
|
||||
elapsed = std::min(elapsed, duration);
|
||||
|
||||
// Calculate easing value (0.0 to 1.0)
|
||||
float t = duration > 0 ? elapsed / duration : 1.0f;
|
||||
float easedT = easingFunc(t);
|
||||
|
||||
// Get interpolated value
|
||||
AnimationValue currentValue = interpolate(easedT);
|
||||
|
||||
// Apply currentValue to target (either drawable or entity)
|
||||
std::visit([this](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
|
||||
if (currentTarget) {
|
||||
// Handle UIDrawable targets
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
currentTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
}
|
||||
else if (currentEntityTarget) {
|
||||
// Handle UIEntity targets
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
currentEntityTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
currentEntityTarget->setProperty(targetProperty, value);
|
||||
}
|
||||
// Entities don't support other types yet
|
||||
}
|
||||
}, currentValue);
|
||||
|
||||
return !isComplete();
|
||||
}
|
||||
|
||||
AnimationValue Animation::getCurrentValue() const {
|
||||
float t = duration > 0 ? elapsed / duration : 1.0f;
|
||||
float easedT = easingFunc(t);
|
||||
return interpolate(easedT);
|
||||
}
|
||||
|
||||
AnimationValue Animation::interpolate(float t) const {
|
||||
// Visit the variant to perform type-specific interpolation
|
||||
return std::visit([this, t](const auto& target) -> AnimationValue {
|
||||
using T = std::decay_t<decltype(target)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
// Interpolate float
|
||||
const float* start = std::get_if<float>(&startValue);
|
||||
if (!start) return target; // Type mismatch
|
||||
|
||||
if (delta) {
|
||||
return *start + target * t;
|
||||
} else {
|
||||
return *start + (target - *start) * t;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
// Interpolate integer
|
||||
const int* start = std::get_if<int>(&startValue);
|
||||
if (!start) return target;
|
||||
|
||||
float result;
|
||||
if (delta) {
|
||||
result = *start + target * t;
|
||||
} else {
|
||||
result = *start + (target - *start) * t;
|
||||
}
|
||||
return static_cast<int>(std::round(result));
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
||||
// For sprite animation, interpolate through the list
|
||||
if (target.empty()) return target;
|
||||
|
||||
// Map t to an index in the vector
|
||||
size_t index = static_cast<size_t>(t * (target.size() - 1));
|
||||
index = std::min(index, target.size() - 1);
|
||||
return static_cast<int>(target[index]);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
// Interpolate color
|
||||
const sf::Color* start = std::get_if<sf::Color>(&startValue);
|
||||
if (!start) return target;
|
||||
|
||||
sf::Color result;
|
||||
if (delta) {
|
||||
result.r = std::clamp(start->r + target.r * t, 0.0f, 255.0f);
|
||||
result.g = std::clamp(start->g + target.g * t, 0.0f, 255.0f);
|
||||
result.b = std::clamp(start->b + target.b * t, 0.0f, 255.0f);
|
||||
result.a = std::clamp(start->a + target.a * t, 0.0f, 255.0f);
|
||||
} else {
|
||||
result.r = start->r + (target.r - start->r) * t;
|
||||
result.g = start->g + (target.g - start->g) * t;
|
||||
result.b = start->b + (target.b - start->b) * t;
|
||||
result.a = start->a + (target.a - start->a) * t;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
// Interpolate vector
|
||||
const sf::Vector2f* start = std::get_if<sf::Vector2f>(&startValue);
|
||||
if (!start) return target;
|
||||
|
||||
if (delta) {
|
||||
return sf::Vector2f(start->x + target.x * t,
|
||||
start->y + target.y * t);
|
||||
} else {
|
||||
return sf::Vector2f(start->x + (target.x - start->x) * t,
|
||||
start->y + (target.y - start->y) * t);
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
// For text, show characters based on t
|
||||
const std::string* start = std::get_if<std::string>(&startValue);
|
||||
if (!start) return target;
|
||||
|
||||
// If delta mode, append characters from target
|
||||
if (delta) {
|
||||
size_t chars = static_cast<size_t>(target.length() * t);
|
||||
return *start + target.substr(0, chars);
|
||||
} else {
|
||||
// Transition from start text to target text
|
||||
if (t < 0.5f) {
|
||||
// First half: remove characters from start
|
||||
size_t chars = static_cast<size_t>(start->length() * (1.0f - t * 2.0f));
|
||||
return start->substr(0, chars);
|
||||
} else {
|
||||
// Second half: add characters to target
|
||||
size_t chars = static_cast<size_t>(target.length() * ((t - 0.5f) * 2.0f));
|
||||
return target.substr(0, chars);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target; // Fallback
|
||||
}, targetValue);
|
||||
}
|
||||
|
||||
// Easing functions implementation
|
||||
namespace EasingFunctions {
|
||||
|
||||
float linear(float t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
float easeIn(float t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
float easeOut(float t) {
|
||||
return t * (2.0f - t);
|
||||
}
|
||||
|
||||
float easeInOut(float t) {
|
||||
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
|
||||
}
|
||||
|
||||
// Quadratic
|
||||
float easeInQuad(float t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
float easeOutQuad(float t) {
|
||||
return t * (2.0f - t);
|
||||
}
|
||||
|
||||
float easeInOutQuad(float t) {
|
||||
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
|
||||
}
|
||||
|
||||
// Cubic
|
||||
float easeInCubic(float t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
float easeOutCubic(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return t1 * t1 * t1 + 1.0f;
|
||||
}
|
||||
|
||||
float easeInOutCubic(float t) {
|
||||
return t < 0.5f ? 4.0f * t * t * t : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
|
||||
}
|
||||
|
||||
// Quartic
|
||||
float easeInQuart(float t) {
|
||||
return t * t * t * t;
|
||||
}
|
||||
|
||||
float easeOutQuart(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return 1.0f - t1 * t1 * t1 * t1;
|
||||
}
|
||||
|
||||
float easeInOutQuart(float t) {
|
||||
return t < 0.5f ? 8.0f * t * t * t * t : 1.0f - 8.0f * (t - 1.0f) * (t - 1.0f) * (t - 1.0f) * (t - 1.0f);
|
||||
}
|
||||
|
||||
// Sine
|
||||
float easeInSine(float t) {
|
||||
return 1.0f - std::cos(t * M_PI / 2.0f);
|
||||
}
|
||||
|
||||
float easeOutSine(float t) {
|
||||
return std::sin(t * M_PI / 2.0f);
|
||||
}
|
||||
|
||||
float easeInOutSine(float t) {
|
||||
return 0.5f * (1.0f - std::cos(M_PI * t));
|
||||
}
|
||||
|
||||
// Exponential
|
||||
float easeInExpo(float t) {
|
||||
return t == 0.0f ? 0.0f : std::pow(2.0f, 10.0f * (t - 1.0f));
|
||||
}
|
||||
|
||||
float easeOutExpo(float t) {
|
||||
return t == 1.0f ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t);
|
||||
}
|
||||
|
||||
float easeInOutExpo(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * std::pow(2.0f, 20.0f * t - 10.0f);
|
||||
} else {
|
||||
return 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Circular
|
||||
float easeInCirc(float t) {
|
||||
return 1.0f - std::sqrt(1.0f - t * t);
|
||||
}
|
||||
|
||||
float easeOutCirc(float t) {
|
||||
float t1 = t - 1.0f;
|
||||
return std::sqrt(1.0f - t1 * t1);
|
||||
}
|
||||
|
||||
float easeInOutCirc(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * (1.0f - std::sqrt(1.0f - 4.0f * t * t));
|
||||
} else {
|
||||
return 0.5f * (std::sqrt(1.0f - (2.0f * t - 2.0f) * (2.0f * t - 2.0f)) + 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Elastic
|
||||
float easeInElastic(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
float p = 0.3f;
|
||||
float a = 1.0f;
|
||||
float s = p / 4.0f;
|
||||
float t1 = t - 1.0f;
|
||||
return -(a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
|
||||
}
|
||||
|
||||
float easeOutElastic(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
float p = 0.3f;
|
||||
float a = 1.0f;
|
||||
float s = p / 4.0f;
|
||||
return a * std::pow(2.0f, -10.0f * t) * std::sin((t - s) * (2.0f * M_PI) / p) + 1.0f;
|
||||
}
|
||||
|
||||
float easeInOutElastic(float t) {
|
||||
if (t == 0.0f) return 0.0f;
|
||||
if (t == 1.0f) return 1.0f;
|
||||
float p = 0.45f;
|
||||
float a = 1.0f;
|
||||
float s = p / 4.0f;
|
||||
|
||||
if (t < 0.5f) {
|
||||
float t1 = 2.0f * t - 1.0f;
|
||||
return -0.5f * (a * std::pow(2.0f, 10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p));
|
||||
} else {
|
||||
float t1 = 2.0f * t - 1.0f;
|
||||
return a * std::pow(2.0f, -10.0f * t1) * std::sin((t1 - s) * (2.0f * M_PI) / p) * 0.5f + 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Back (overshooting)
|
||||
float easeInBack(float t) {
|
||||
const float s = 1.70158f;
|
||||
return t * t * ((s + 1.0f) * t - s);
|
||||
}
|
||||
|
||||
float easeOutBack(float t) {
|
||||
const float s = 1.70158f;
|
||||
float t1 = t - 1.0f;
|
||||
return t1 * t1 * ((s + 1.0f) * t1 + s) + 1.0f;
|
||||
}
|
||||
|
||||
float easeInOutBack(float t) {
|
||||
const float s = 1.70158f * 1.525f;
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * (4.0f * t * t * ((s + 1.0f) * 2.0f * t - s));
|
||||
} else {
|
||||
float t1 = 2.0f * t - 2.0f;
|
||||
return 0.5f * (t1 * t1 * ((s + 1.0f) * t1 + s) + 2.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// Bounce
|
||||
float easeOutBounce(float t) {
|
||||
if (t < 1.0f / 2.75f) {
|
||||
return 7.5625f * t * t;
|
||||
} else if (t < 2.0f / 2.75f) {
|
||||
float t1 = t - 1.5f / 2.75f;
|
||||
return 7.5625f * t1 * t1 + 0.75f;
|
||||
} else if (t < 2.5f / 2.75f) {
|
||||
float t1 = t - 2.25f / 2.75f;
|
||||
return 7.5625f * t1 * t1 + 0.9375f;
|
||||
} else {
|
||||
float t1 = t - 2.625f / 2.75f;
|
||||
return 7.5625f * t1 * t1 + 0.984375f;
|
||||
}
|
||||
}
|
||||
|
||||
float easeInBounce(float t) {
|
||||
return 1.0f - easeOutBounce(1.0f - t);
|
||||
}
|
||||
|
||||
float easeInOutBounce(float t) {
|
||||
if (t < 0.5f) {
|
||||
return 0.5f * easeInBounce(2.0f * t);
|
||||
} else {
|
||||
return 0.5f * easeOutBounce(2.0f * t - 1.0f) + 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
// Get easing function by name
|
||||
EasingFunction getByName(const std::string& name) {
|
||||
static std::unordered_map<std::string, EasingFunction> easingMap = {
|
||||
{"linear", linear},
|
||||
{"easeIn", easeIn},
|
||||
{"easeOut", easeOut},
|
||||
{"easeInOut", easeInOut},
|
||||
{"easeInQuad", easeInQuad},
|
||||
{"easeOutQuad", easeOutQuad},
|
||||
{"easeInOutQuad", easeInOutQuad},
|
||||
{"easeInCubic", easeInCubic},
|
||||
{"easeOutCubic", easeOutCubic},
|
||||
{"easeInOutCubic", easeInOutCubic},
|
||||
{"easeInQuart", easeInQuart},
|
||||
{"easeOutQuart", easeOutQuart},
|
||||
{"easeInOutQuart", easeInOutQuart},
|
||||
{"easeInSine", easeInSine},
|
||||
{"easeOutSine", easeOutSine},
|
||||
{"easeInOutSine", easeInOutSine},
|
||||
{"easeInExpo", easeInExpo},
|
||||
{"easeOutExpo", easeOutExpo},
|
||||
{"easeInOutExpo", easeInOutExpo},
|
||||
{"easeInCirc", easeInCirc},
|
||||
{"easeOutCirc", easeOutCirc},
|
||||
{"easeInOutCirc", easeInOutCirc},
|
||||
{"easeInElastic", easeInElastic},
|
||||
{"easeOutElastic", easeOutElastic},
|
||||
{"easeInOutElastic", easeInOutElastic},
|
||||
{"easeInBack", easeInBack},
|
||||
{"easeOutBack", easeOutBack},
|
||||
{"easeInOutBack", easeInOutBack},
|
||||
{"easeInBounce", easeInBounce},
|
||||
{"easeOutBounce", easeOutBounce},
|
||||
{"easeInOutBounce", easeInOutBounce}
|
||||
};
|
||||
|
||||
auto it = easingMap.find(name);
|
||||
if (it != easingMap.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return linear; // Default to linear
|
||||
}
|
||||
|
||||
} // namespace EasingFunctions
|
||||
|
||||
// AnimationManager implementation
|
||||
AnimationManager& AnimationManager::getInstance() {
|
||||
static AnimationManager instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
void AnimationManager::addAnimation(std::shared_ptr<Animation> animation) {
|
||||
activeAnimations.push_back(animation);
|
||||
}
|
||||
|
||||
void AnimationManager::update(float deltaTime) {
|
||||
for (auto& anim : activeAnimations) {
|
||||
anim->update(deltaTime);
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
void AnimationManager::cleanup() {
|
||||
activeAnimations.erase(
|
||||
std::remove_if(activeAnimations.begin(), activeAnimations.end(),
|
||||
[](const std::shared_ptr<Animation>& anim) {
|
||||
return anim->isComplete();
|
||||
}),
|
||||
activeAnimations.end()
|
||||
);
|
||||
}
|
||||
|
||||
void AnimationManager::clear() {
|
||||
activeAnimations.clear();
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
#include <SFML/Graphics.hpp>
|
||||
|
||||
// Forward declarations
|
||||
class UIDrawable;
|
||||
class UIEntity;
|
||||
|
||||
// Forward declare namespace
|
||||
namespace EasingFunctions {
|
||||
float linear(float t);
|
||||
}
|
||||
|
||||
// Easing function type
|
||||
typedef std::function<float(float)> EasingFunction;
|
||||
|
||||
// Animation target value can be various types
|
||||
typedef std::variant<
|
||||
float, // Single float value
|
||||
int, // Single integer value
|
||||
std::vector<int>, // List of integers (for sprite animation)
|
||||
sf::Color, // Color animation
|
||||
sf::Vector2f, // Vector animation
|
||||
std::string // String animation (for text)
|
||||
> AnimationValue;
|
||||
|
||||
class Animation {
|
||||
public:
|
||||
// Constructor
|
||||
Animation(const std::string& targetProperty,
|
||||
const AnimationValue& targetValue,
|
||||
float duration,
|
||||
EasingFunction easingFunc = EasingFunctions::linear,
|
||||
bool delta = false);
|
||||
|
||||
// Apply this animation to a drawable
|
||||
void start(UIDrawable* target);
|
||||
|
||||
// Apply this animation to an entity (special case since Entity doesn't inherit from UIDrawable)
|
||||
void startEntity(UIEntity* target);
|
||||
|
||||
// Update animation (called each frame)
|
||||
// Returns true if animation is still running, false if complete
|
||||
bool update(float deltaTime);
|
||||
|
||||
// Get current interpolated value
|
||||
AnimationValue getCurrentValue() const;
|
||||
|
||||
// Animation properties
|
||||
std::string getTargetProperty() const { return targetProperty; }
|
||||
float getDuration() const { return duration; }
|
||||
float getElapsed() const { return elapsed; }
|
||||
bool isComplete() const { return elapsed >= duration; }
|
||||
bool isDelta() const { return delta; }
|
||||
|
||||
private:
|
||||
std::string targetProperty; // Property name to animate (e.g., "x", "color.r", "sprite_number")
|
||||
AnimationValue startValue; // Starting value (captured when animation starts)
|
||||
AnimationValue targetValue; // Target value to animate to
|
||||
float duration; // Animation duration in seconds
|
||||
float elapsed = 0.0f; // Elapsed time
|
||||
EasingFunction easingFunc; // Easing function to use
|
||||
bool delta; // If true, targetValue is relative to start
|
||||
|
||||
UIDrawable* currentTarget = nullptr; // Current target being animated
|
||||
UIEntity* currentEntityTarget = nullptr; // Current entity target (alternative to drawable)
|
||||
|
||||
// Helper to interpolate between values
|
||||
AnimationValue interpolate(float t) const;
|
||||
};
|
||||
|
||||
// Easing functions library
|
||||
namespace EasingFunctions {
|
||||
// Basic easing functions
|
||||
float linear(float t);
|
||||
float easeIn(float t);
|
||||
float easeOut(float t);
|
||||
float easeInOut(float t);
|
||||
|
||||
// Advanced easing functions
|
||||
float easeInQuad(float t);
|
||||
float easeOutQuad(float t);
|
||||
float easeInOutQuad(float t);
|
||||
|
||||
float easeInCubic(float t);
|
||||
float easeOutCubic(float t);
|
||||
float easeInOutCubic(float t);
|
||||
|
||||
float easeInQuart(float t);
|
||||
float easeOutQuart(float t);
|
||||
float easeInOutQuart(float t);
|
||||
|
||||
float easeInSine(float t);
|
||||
float easeOutSine(float t);
|
||||
float easeInOutSine(float t);
|
||||
|
||||
float easeInExpo(float t);
|
||||
float easeOutExpo(float t);
|
||||
float easeInOutExpo(float t);
|
||||
|
||||
float easeInCirc(float t);
|
||||
float easeOutCirc(float t);
|
||||
float easeInOutCirc(float t);
|
||||
|
||||
float easeInElastic(float t);
|
||||
float easeOutElastic(float t);
|
||||
float easeInOutElastic(float t);
|
||||
|
||||
float easeInBack(float t);
|
||||
float easeOutBack(float t);
|
||||
float easeInOutBack(float t);
|
||||
|
||||
float easeInBounce(float t);
|
||||
float easeOutBounce(float t);
|
||||
float easeInOutBounce(float t);
|
||||
|
||||
// Get easing function by name
|
||||
EasingFunction getByName(const std::string& name);
|
||||
}
|
||||
|
||||
// Animation manager to handle active animations
|
||||
class AnimationManager {
|
||||
public:
|
||||
static AnimationManager& getInstance();
|
||||
|
||||
// Add an animation to be managed
|
||||
void addAnimation(std::shared_ptr<Animation> animation);
|
||||
|
||||
// Update all animations
|
||||
void update(float deltaTime);
|
||||
|
||||
// Remove completed animations
|
||||
void cleanup();
|
||||
|
||||
// Clear all animations
|
||||
void clear();
|
||||
|
||||
private:
|
||||
AnimationManager() = default;
|
||||
std::vector<std::shared_ptr<Animation>> activeAnimations;
|
||||
};
|
|
@ -0,0 +1,172 @@
|
|||
#include "CommandLineParser.h"
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
|
||||
CommandLineParser::CommandLineParser(int argc, char* argv[])
|
||||
: argc(argc), argv(argv) {}
|
||||
|
||||
CommandLineParser::ParseResult CommandLineParser::parse(McRogueFaceConfig& config) {
|
||||
ParseResult result;
|
||||
current_arg = 1; // Reset for each parse
|
||||
|
||||
// Detect if running as Python interpreter
|
||||
std::filesystem::path exec_name = std::filesystem::path(argv[0]).filename();
|
||||
if (exec_name.string().find("python") == 0) {
|
||||
config.headless = true;
|
||||
config.python_mode = true;
|
||||
}
|
||||
|
||||
while (current_arg < argc) {
|
||||
std::string arg = argv[current_arg];
|
||||
|
||||
// Handle Python-style single-letter flags
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
print_help();
|
||||
result.should_exit = true;
|
||||
result.exit_code = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
if (arg == "-V" || arg == "--version") {
|
||||
print_version();
|
||||
result.should_exit = true;
|
||||
result.exit_code = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Python execution modes
|
||||
if (arg == "-c") {
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
if (current_arg >= argc) {
|
||||
std::cerr << "Argument expected for the -c option" << std::endl;
|
||||
result.should_exit = true;
|
||||
result.exit_code = 1;
|
||||
return result;
|
||||
}
|
||||
config.python_command = argv[current_arg];
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "-m") {
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
if (current_arg >= argc) {
|
||||
std::cerr << "Argument expected for the -m option" << std::endl;
|
||||
result.should_exit = true;
|
||||
result.exit_code = 1;
|
||||
return result;
|
||||
}
|
||||
config.python_module = argv[current_arg];
|
||||
current_arg++;
|
||||
// Collect remaining args as module args
|
||||
while (current_arg < argc) {
|
||||
config.script_args.push_back(argv[current_arg]);
|
||||
current_arg++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "-i") {
|
||||
config.interactive_mode = true;
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// McRogueFace specific flags
|
||||
if (arg == "--headless") {
|
||||
config.headless = true;
|
||||
config.audio_enabled = false;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--audio-off") {
|
||||
config.audio_enabled = false;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--audio-on") {
|
||||
config.audio_enabled = true;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--screenshot") {
|
||||
config.take_screenshot = true;
|
||||
current_arg++;
|
||||
if (current_arg < argc && argv[current_arg][0] != '-') {
|
||||
config.screenshot_path = argv[current_arg];
|
||||
current_arg++;
|
||||
} else {
|
||||
config.screenshot_path = "screenshot.png";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == "--exec") {
|
||||
current_arg++;
|
||||
if (current_arg >= argc) {
|
||||
std::cerr << "Argument expected for the --exec option" << std::endl;
|
||||
result.should_exit = true;
|
||||
result.exit_code = 1;
|
||||
return result;
|
||||
}
|
||||
config.exec_scripts.push_back(argv[current_arg]);
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no flags matched, treat as positional argument (script name)
|
||||
if (arg[0] != '-') {
|
||||
config.script_path = arg;
|
||||
config.python_mode = true;
|
||||
current_arg++;
|
||||
// Remaining args are script args
|
||||
while (current_arg < argc) {
|
||||
config.script_args.push_back(argv[current_arg]);
|
||||
current_arg++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Unknown flag
|
||||
std::cerr << "Unknown option: " << arg << std::endl;
|
||||
result.should_exit = true;
|
||||
result.exit_code = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void CommandLineParser::print_help() {
|
||||
std::cout << "usage: mcrogueface [option] ... [-c cmd | -m mod | file | -] [arg] ...\n"
|
||||
<< "Options:\n"
|
||||
<< " -c cmd : program passed in as string (terminates option list)\n"
|
||||
<< " -h : print this help message and exit (also --help)\n"
|
||||
<< " -i : inspect interactively after running script\n"
|
||||
<< " -m mod : run library module as a script (terminates option list)\n"
|
||||
<< " -V : print the Python version number and exit (also --version)\n"
|
||||
<< "\n"
|
||||
<< "McRogueFace specific options:\n"
|
||||
<< " --exec file : execute script before main program (can be used multiple times)\n"
|
||||
<< " --headless : run without creating a window (implies --audio-off)\n"
|
||||
<< " --audio-off : disable audio\n"
|
||||
<< " --audio-on : enable audio (even in headless mode)\n"
|
||||
<< " --screenshot [path] : take a screenshot in headless mode\n"
|
||||
<< "\n"
|
||||
<< "Arguments:\n"
|
||||
<< " file : program read from script file\n"
|
||||
<< " - : program read from stdin\n"
|
||||
<< " arg ...: arguments passed to program in sys.argv[1:]\n";
|
||||
}
|
||||
|
||||
void CommandLineParser::print_version() {
|
||||
std::cout << "Python 3.12.0 (McRogueFace embedded)\n";
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
#ifndef COMMAND_LINE_PARSER_H
|
||||
#define COMMAND_LINE_PARSER_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "McRogueFaceConfig.h"
|
||||
|
||||
class CommandLineParser {
|
||||
public:
|
||||
struct ParseResult {
|
||||
bool should_exit = false;
|
||||
int exit_code = 0;
|
||||
};
|
||||
|
||||
CommandLineParser(int argc, char* argv[]);
|
||||
ParseResult parse(McRogueFaceConfig& config);
|
||||
|
||||
private:
|
||||
int argc;
|
||||
char** argv;
|
||||
int current_arg = 1; // Skip program name
|
||||
|
||||
bool has_flag(const std::string& short_flag, const std::string& long_flag = "");
|
||||
std::string get_next_arg(const std::string& flag_name);
|
||||
void parse_positional_args(McRogueFaceConfig& config);
|
||||
void print_help();
|
||||
void print_version();
|
||||
};
|
||||
|
||||
#endif // COMMAND_LINE_PARSER_H
|
|
@ -4,27 +4,80 @@
|
|||
#include "PyScene.h"
|
||||
#include "UITestScene.h"
|
||||
#include "Resources.h"
|
||||
#include "Animation.h"
|
||||
|
||||
GameEngine::GameEngine()
|
||||
GameEngine::GameEngine() : GameEngine(McRogueFaceConfig{})
|
||||
{
|
||||
}
|
||||
|
||||
GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||
: config(cfg), headless(cfg.headless)
|
||||
{
|
||||
Resources::font.loadFromFile("./assets/JetbrainsMono.ttf");
|
||||
Resources::game = this;
|
||||
window_title = "Crypt of Sokoban - 7DRL 2025, McRogueface Engine";
|
||||
window.create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
|
||||
visible = window.getDefaultView();
|
||||
window.setFramerateLimit(60);
|
||||
|
||||
// Initialize rendering based on headless mode
|
||||
if (headless) {
|
||||
headless_renderer = std::make_unique<HeadlessRenderer>();
|
||||
if (!headless_renderer->init(1024, 768)) {
|
||||
throw std::runtime_error("Failed to initialize headless renderer");
|
||||
}
|
||||
render_target = &headless_renderer->getRenderTarget();
|
||||
} else {
|
||||
window = std::make_unique<sf::RenderWindow>();
|
||||
window->create(sf::VideoMode(1024, 768), window_title, sf::Style::Titlebar | sf::Style::Close);
|
||||
window->setFramerateLimit(60);
|
||||
render_target = window.get();
|
||||
}
|
||||
|
||||
visible = render_target->getDefaultView();
|
||||
scene = "uitest";
|
||||
scenes["uitest"] = new UITestScene(this);
|
||||
|
||||
McRFPy_API::game = this;
|
||||
McRFPy_API::api_init();
|
||||
McRFPy_API::executePyString("import mcrfpy");
|
||||
McRFPy_API::executeScript("scripts/game.py");
|
||||
|
||||
// Only load game.py if no custom script/command/module/exec is specified
|
||||
bool should_load_game = config.script_path.empty() &&
|
||||
config.python_command.empty() &&
|
||||
config.python_module.empty() &&
|
||||
config.exec_scripts.empty() &&
|
||||
!config.interactive_mode &&
|
||||
!config.python_mode;
|
||||
|
||||
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 in order
|
||||
if (!config.exec_scripts.empty()) {
|
||||
if (!Py_IsInitialized()) {
|
||||
McRFPy_API::api_init();
|
||||
}
|
||||
McRFPy_API::executePyString("import mcrfpy");
|
||||
|
||||
for (const auto& exec_script : config.exec_scripts) {
|
||||
std::cout << "Executing script: " << exec_script << std::endl;
|
||||
McRFPy_API::executeScript(exec_script.string());
|
||||
}
|
||||
std::cout << "All --exec scripts completed" << std::endl;
|
||||
}
|
||||
|
||||
clock.restart();
|
||||
runtime.restart();
|
||||
}
|
||||
|
||||
GameEngine::~GameEngine()
|
||||
{
|
||||
for (auto& [name, scene] : scenes) {
|
||||
delete scene;
|
||||
}
|
||||
}
|
||||
|
||||
Scene* GameEngine::currentScene() { return scenes[scene]; }
|
||||
void GameEngine::changeScene(std::string s)
|
||||
{
|
||||
|
@ -37,36 +90,77 @@ void GameEngine::changeScene(std::string s)
|
|||
void GameEngine::quit() { running = false; }
|
||||
void GameEngine::setPause(bool p) { paused = p; }
|
||||
sf::Font & GameEngine::getFont() { /*return font; */ return Resources::font; }
|
||||
sf::RenderWindow & GameEngine::getWindow() { return window; }
|
||||
sf::RenderWindow & GameEngine::getWindow() {
|
||||
if (!window) {
|
||||
throw std::runtime_error("Window not available in headless mode");
|
||||
}
|
||||
return *window;
|
||||
}
|
||||
|
||||
sf::RenderTarget & GameEngine::getRenderTarget() {
|
||||
return *render_target;
|
||||
}
|
||||
|
||||
void GameEngine::createScene(std::string s) { scenes[s] = new PyScene(this); }
|
||||
|
||||
void GameEngine::setWindowScale(float multiplier)
|
||||
{
|
||||
window.setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
|
||||
if (!headless && window) {
|
||||
window->setSize(sf::Vector2u(1024 * multiplier, 768 * multiplier)); // 7DRL 2024: window scaling
|
||||
}
|
||||
//window.create(sf::VideoMode(1024 * multiplier, 768 * multiplier), window_title, sf::Style::Titlebar | sf::Style::Close);
|
||||
}
|
||||
|
||||
void GameEngine::run()
|
||||
{
|
||||
std::cout << "GameEngine::run() starting main loop..." << std::endl;
|
||||
float fps = 0.0;
|
||||
frameTime = 0.016f; // Initialize to ~60 FPS
|
||||
clock.restart();
|
||||
while (running)
|
||||
{
|
||||
currentScene()->update();
|
||||
testTimers();
|
||||
sUserInput();
|
||||
|
||||
// Update animations (only if frameTime is valid)
|
||||
if (frameTime > 0.0f && frameTime < 1.0f) {
|
||||
AnimationManager::getInstance().update(frameTime);
|
||||
}
|
||||
|
||||
if (!headless) {
|
||||
sUserInput();
|
||||
}
|
||||
if (!paused)
|
||||
{
|
||||
}
|
||||
currentScene()->render();
|
||||
|
||||
// Display the frame
|
||||
if (headless) {
|
||||
headless_renderer->display();
|
||||
// Take screenshot if requested
|
||||
if (config.take_screenshot) {
|
||||
headless_renderer->saveScreenshot(config.screenshot_path.empty() ? "screenshot.png" : config.screenshot_path);
|
||||
config.take_screenshot = false; // Only take one screenshot
|
||||
}
|
||||
} else {
|
||||
window->display();
|
||||
}
|
||||
|
||||
currentFrame++;
|
||||
frameTime = clock.restart().asSeconds();
|
||||
fps = 1 / frameTime;
|
||||
int whole_fps = (int)fps;
|
||||
int tenth_fps = int(fps * 100) % 10;
|
||||
//window.setTitle(window_title + " " + std::to_string(fps) + " FPS");
|
||||
window.setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||
|
||||
if (!headless && window) {
|
||||
window->setTitle(window_title + " " + std::to_string(whole_fps) + "." + std::to_string(tenth_fps) + " FPS");
|
||||
}
|
||||
|
||||
// In windowed mode, check if window was closed
|
||||
if (!headless && window && !window->isOpen()) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,86 +202,54 @@ void GameEngine::testTimers()
|
|||
}
|
||||
}
|
||||
|
||||
void GameEngine::processEvent(const sf::Event& event)
|
||||
{
|
||||
std::string actionType;
|
||||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; return; }
|
||||
// TODO: add resize event to Scene to react; call it after constructor too, maybe
|
||||
else if (event.type == sf::Event::Resized) {
|
||||
return; // 7DRL short circuit. Resizing manually disabled
|
||||
}
|
||||
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseWheelScrolled) actionType = "start";
|
||||
else if (event.type == sf::Event::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||
|
||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||
actionCode = ActionCode::keycode(event.key.code);
|
||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||
{
|
||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||
{
|
||||
int delta = 1;
|
||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||
}
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
else if (currentScene()->key_callable &&
|
||||
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
||||
{
|
||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||
}
|
||||
}
|
||||
|
||||
void GameEngine::sUserInput()
|
||||
{
|
||||
sf::Event event;
|
||||
while (window.pollEvent(event))
|
||||
while (window && window->pollEvent(event))
|
||||
{
|
||||
std::string actionType;
|
||||
int actionCode = 0;
|
||||
|
||||
if (event.type == sf::Event::Closed) { running = false; continue; }
|
||||
// TODO: add resize event to Scene to react; call it after constructor too, maybe
|
||||
else if (event.type == sf::Event::Resized) {
|
||||
continue; // 7DRL short circuit. Resizing manually disabled
|
||||
/*
|
||||
sf::FloatRect area(0.f, 0.f, event.size.width, event.size.height);
|
||||
//sf::FloatRect area(0.f, 0.f, 1024.f, 768.f); // 7DRL 2024: attempt to set scale appropriately
|
||||
//sf::FloatRect area(0.f, 0.f, event.size.width, event.size.width * 0.75);
|
||||
visible = sf::View(area);
|
||||
window.setView(visible);
|
||||
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
|
||||
std::cout << "Visible area set to (0, 0, " << event.size.width << ", " << event.size.height <<")"<<std::endl;
|
||||
actionType = "resize";
|
||||
//window.setSize(sf::Vector2u(event.size.width, event.size.width * 0.75)); // 7DRL 2024: window scaling
|
||||
*/
|
||||
}
|
||||
|
||||
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::KeyReleased || event.type == sf::Event::MouseButtonReleased) actionType = "end";
|
||||
|
||||
if (event.type == sf::Event::MouseButtonPressed || event.type == sf::Event::MouseButtonReleased)
|
||||
actionCode = ActionCode::keycode(event.mouseButton.button);
|
||||
else if (event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased)
|
||||
actionCode = ActionCode::keycode(event.key.code);
|
||||
else if (event.type == sf::Event::MouseWheelScrolled)
|
||||
{
|
||||
// //sf::Mouse::Wheel w = event.MouseWheelScrollEvent.wheel;
|
||||
if (event.mouseWheelScroll.wheel == sf::Mouse::VerticalWheel)
|
||||
{
|
||||
int delta = 1;
|
||||
if (event.mouseWheelScroll.delta < 0) delta = -1;
|
||||
actionCode = ActionCode::keycode(event.mouseWheelScroll.wheel, delta );
|
||||
/*
|
||||
std::cout << "[GameEngine] Generated MouseWheel code w(" << (int)event.mouseWheelScroll.wheel << ") d(" << event.mouseWheelScroll.delta << ") D(" << delta << ") = " << actionCode << std::endl;
|
||||
std::cout << " test decode: isMouseWheel=" << ActionCode::isMouseWheel(actionCode) << ", wheel=" << ActionCode::wheel(actionCode) << ", delta=" << ActionCode::delta(actionCode) << std::endl;
|
||||
std::cout << " math test: actionCode && WHEEL_NEG -> " << (actionCode && ActionCode::WHEEL_NEG) << "; actionCode && WHEEL_DEL -> " << (actionCode && ActionCode::WHEEL_DEL) << ";" << std::endl;
|
||||
*/
|
||||
}
|
||||
// float d = event.MouseWheelScrollEvent.delta;
|
||||
// actionCode = ActionCode::keycode(0, d);
|
||||
}
|
||||
else
|
||||
continue;
|
||||
|
||||
//std::cout << "Event produced action code " << actionCode << ": " << actionType << std::endl;
|
||||
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
else if (currentScene()->key_callable)
|
||||
{
|
||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||
/*
|
||||
PyObject* args = Py_BuildValue("(ss)", ActionCode::key_str(event.key.code).c_str(), actionType.c_str());
|
||||
PyObject* retval = PyObject_Call(currentScene()->key_callable, args, NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cout << "key_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 << "key_callable returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
*/
|
||||
}
|
||||
else
|
||||
{
|
||||
//std::cout << "[GameEngine] Action not registered for input: " << actionCode << ": " << actionType << std::endl;
|
||||
}
|
||||
processEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,10 +6,16 @@
|
|||
#include "IndexTexture.h"
|
||||
#include "Timer.h"
|
||||
#include "PyCallable.h"
|
||||
#include "McRogueFaceConfig.h"
|
||||
#include "HeadlessRenderer.h"
|
||||
#include <memory>
|
||||
|
||||
class GameEngine
|
||||
{
|
||||
sf::RenderWindow window;
|
||||
std::unique_ptr<sf::RenderWindow> window;
|
||||
std::unique_ptr<HeadlessRenderer> headless_renderer;
|
||||
sf::RenderTarget* render_target;
|
||||
|
||||
sf::Font font;
|
||||
std::map<std::string, Scene*> scenes;
|
||||
bool running = true;
|
||||
|
@ -20,6 +26,9 @@ class GameEngine
|
|||
float frameTime;
|
||||
std::string window_title;
|
||||
|
||||
bool headless = false;
|
||||
McRogueFaceConfig config;
|
||||
|
||||
sf::Clock runtime;
|
||||
//std::map<std::string, Timer> timers;
|
||||
std::map<std::string, std::shared_ptr<PyTimerCallable>> timers;
|
||||
|
@ -28,6 +37,8 @@ class GameEngine
|
|||
public:
|
||||
std::string scene;
|
||||
GameEngine();
|
||||
GameEngine(const McRogueFaceConfig& cfg);
|
||||
~GameEngine();
|
||||
Scene* currentScene();
|
||||
void changeScene(std::string);
|
||||
void createScene(std::string);
|
||||
|
@ -35,6 +46,8 @@ public:
|
|||
void setPause(bool);
|
||||
sf::Font & getFont();
|
||||
sf::RenderWindow & getWindow();
|
||||
sf::RenderTarget & getRenderTarget();
|
||||
sf::RenderTarget* getRenderTargetPtr() { return render_target; }
|
||||
void run();
|
||||
void sUserInput();
|
||||
int getFrame() { return currentFrame; }
|
||||
|
@ -42,6 +55,8 @@ public:
|
|||
sf::View getView() { return visible; }
|
||||
void manageTimer(std::string, PyObject*, int);
|
||||
void setWindowScale(float);
|
||||
bool isHeadless() const { return headless; }
|
||||
void processEvent(const sf::Event& event);
|
||||
|
||||
// global textures for scripts to access
|
||||
std::vector<IndexTexture> textures;
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
#include "HeadlessRenderer.h"
|
||||
#include <iostream>
|
||||
|
||||
bool HeadlessRenderer::init(int width, int height) {
|
||||
if (!render_texture.create(width, height)) {
|
||||
std::cerr << "Failed to create headless render texture" << std::endl;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
sf::RenderTarget& HeadlessRenderer::getRenderTarget() {
|
||||
return render_texture;
|
||||
}
|
||||
|
||||
void HeadlessRenderer::saveScreenshot(const std::string& path) {
|
||||
sf::Image screenshot = render_texture.getTexture().copyToImage();
|
||||
if (!screenshot.saveToFile(path)) {
|
||||
std::cerr << "Failed to save screenshot to: " << path << std::endl;
|
||||
} else {
|
||||
std::cout << "Screenshot saved to: " << path << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void HeadlessRenderer::display() {
|
||||
render_texture.display();
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef HEADLESS_RENDERER_H
|
||||
#define HEADLESS_RENDERER_H
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class HeadlessRenderer {
|
||||
private:
|
||||
sf::RenderTexture render_texture;
|
||||
|
||||
public:
|
||||
bool init(int width = 1024, int height = 768);
|
||||
sf::RenderTarget& getRenderTarget();
|
||||
void saveScreenshot(const std::string& path);
|
||||
void display(); // Finalize the current frame
|
||||
bool isOpen() const { return true; } // Always "open" in headless mode
|
||||
};
|
||||
|
||||
#endif // HEADLESS_RENDERER_H
|
|
@ -1,10 +1,14 @@
|
|||
#include "McRFPy_API.h"
|
||||
#include "McRFPy_Automation.h"
|
||||
#include "platform.h"
|
||||
#include "PyAnimation.h"
|
||||
#include "GameEngine.h"
|
||||
#include "UI.h"
|
||||
#include "Resources.h"
|
||||
#include "PyScene.h"
|
||||
#include <filesystem>
|
||||
#include <cstring>
|
||||
|
||||
std::map<std::string, PyObject*> McRFPy_API::callbacks;
|
||||
std::vector<sf::SoundBuffer> McRFPy_API::soundbuffers;
|
||||
sf::Music McRFPy_API::music;
|
||||
sf::Sound McRFPy_API::sfx;
|
||||
|
@ -15,11 +19,6 @@ PyObject* McRFPy_API::mcrf_module;
|
|||
|
||||
|
||||
static PyMethodDef mcrfpyMethods[] = {
|
||||
{"registerPyAction", McRFPy_API::_registerPyAction, METH_VARARGS,
|
||||
"Register a callable Python object to correspond to an action string. (actionstr, callable)"},
|
||||
|
||||
{"registerInputAction", McRFPy_API::_registerInputAction, METH_VARARGS,
|
||||
"Register a SFML input code to correspond to an action string. (input_code, actionstr)"},
|
||||
|
||||
{"createSoundBuffer", McRFPy_API::_createSoundBuffer, METH_VARARGS, "(filename)"},
|
||||
{"loadMusic", McRFPy_API::_loadMusic, METH_VARARGS, "(filename)"},
|
||||
|
@ -79,17 +78,20 @@ PyObject* PyInit_mcrfpy()
|
|||
/*collections & iterators*/
|
||||
&PyUICollectionType, &PyUICollectionIterType,
|
||||
&PyUIEntityCollectionType, &PyUIEntityCollectionIterType,
|
||||
|
||||
/*animation*/
|
||||
&PyAnimationType,
|
||||
nullptr};
|
||||
int i = 0;
|
||||
auto t = pytypes[i];
|
||||
while (t != nullptr)
|
||||
{
|
||||
std::cout << "Registering type: " << t->tp_name << std::endl;
|
||||
//std::cout << "Registering type: " << t->tp_name << std::endl;
|
||||
if (PyType_Ready(t) < 0) {
|
||||
std::cout << "ERROR: PyType_Ready failed for " << t->tp_name << std::endl;
|
||||
return NULL;
|
||||
}
|
||||
std::cout << " tp_alloc after PyType_Ready: " << (void*)t->tp_alloc << std::endl;
|
||||
//std::cout << " tp_alloc after PyType_Ready: " << (void*)t->tp_alloc << std::endl;
|
||||
PyModule_AddType(m, t);
|
||||
i++;
|
||||
t = pytypes[i];
|
||||
|
@ -102,6 +104,17 @@ PyObject* PyInit_mcrfpy()
|
|||
//PyModule_AddObject(m, "default_texture", McRFPy_API::default_texture->pyObject());
|
||||
PyModule_AddObject(m, "default_font", Py_None);
|
||||
PyModule_AddObject(m, "default_texture", Py_None);
|
||||
|
||||
// Add automation submodule
|
||||
PyObject* automation_module = McRFPy_Automation::init_automation_module();
|
||||
if (automation_module != NULL) {
|
||||
PyModule_AddObject(m, "automation", automation_module);
|
||||
|
||||
// Also add to sys.modules for proper import behavior
|
||||
PyObject* sys_modules = PyImport_GetModuleDict();
|
||||
PyDict_SetItemString(sys_modules, "mcrfpy.automation", automation_module);
|
||||
}
|
||||
|
||||
//McRFPy_API::mcrf_module = m;
|
||||
return m;
|
||||
}
|
||||
|
@ -160,6 +173,75 @@ PyStatus init_python(const char *program_name)
|
|||
return status;
|
||||
}
|
||||
|
||||
PyStatus McRFPy_API::init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv)
|
||||
{
|
||||
// If Python is already initialized, just return success
|
||||
if (Py_IsInitialized()) {
|
||||
return PyStatus_Ok();
|
||||
}
|
||||
|
||||
PyStatus status;
|
||||
PyConfig pyconfig;
|
||||
PyConfig_InitIsolatedConfig(&pyconfig);
|
||||
|
||||
// CRITICAL: Pass actual command line arguments to Python
|
||||
status = PyConfig_SetBytesArgv(&pyconfig, argc, argv);
|
||||
if (PyStatus_Exception(status)) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Check if we're in a virtual environment
|
||||
auto exe_path = std::filesystem::path(argv[0]);
|
||||
auto exe_dir = exe_path.parent_path();
|
||||
auto venv_root = exe_dir.parent_path();
|
||||
|
||||
if (std::filesystem::exists(venv_root / "pyvenv.cfg")) {
|
||||
// We're running from within a venv!
|
||||
// Add venv's site-packages to module search paths
|
||||
auto site_packages = venv_root / "lib" / "python3.12" / "site-packages";
|
||||
PyWideStringList_Append(&pyconfig.module_search_paths,
|
||||
site_packages.wstring().c_str());
|
||||
pyconfig.module_search_paths_set = 1;
|
||||
}
|
||||
|
||||
// Set Python home to our bundled Python
|
||||
auto python_home = executable_path() + L"/lib/Python";
|
||||
PyConfig_SetString(&pyconfig, &pyconfig.home, python_home.c_str());
|
||||
|
||||
// Set up module search paths
|
||||
#if __PLATFORM_SET_PYTHON_SEARCH_PATHS == 1
|
||||
if (!pyconfig.module_search_paths_set) {
|
||||
pyconfig.module_search_paths_set = 1;
|
||||
}
|
||||
|
||||
// search paths for python libs/modules/scripts
|
||||
const wchar_t* str_arr[] = {
|
||||
L"/scripts",
|
||||
L"/lib/Python/lib.linux-x86_64-3.12",
|
||||
L"/lib/Python",
|
||||
L"/lib/Python/Lib",
|
||||
L"/venv/lib/python3.12/site-packages"
|
||||
};
|
||||
|
||||
for(auto s : str_arr) {
|
||||
status = PyWideStringList_Append(&pyconfig.module_search_paths, (executable_path() + s).c_str());
|
||||
if (PyStatus_Exception(status)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// Register mcrfpy module before initialization
|
||||
if (!Py_IsInitialized()) {
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
}
|
||||
|
||||
status = Py_InitializeFromConfig(&pyconfig);
|
||||
PyConfig_Clear(&pyconfig);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/*
|
||||
void McRFPy_API::setSpriteTexture(int ti)
|
||||
{
|
||||
|
@ -177,9 +259,11 @@ void McRFPy_API::setSpriteTexture(int ti)
|
|||
void McRFPy_API::api_init() {
|
||||
|
||||
// build API exposure before python initialization
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
// use full path version of argv[0] from OS to init python
|
||||
init_python(narrow_string(executable_filename()).c_str());
|
||||
if (!Py_IsInitialized()) {
|
||||
PyImport_AppendInittab("mcrfpy", &PyInit_mcrfpy);
|
||||
// use full path version of argv[0] from OS to init python
|
||||
init_python(narrow_string(executable_filename()).c_str());
|
||||
}
|
||||
|
||||
//texture.loadFromFile("./assets/kenney_tinydungeon.png");
|
||||
//texture_size = 16, texture_width = 12, texture_height= 11;
|
||||
|
@ -200,12 +284,56 @@ void McRFPy_API::api_init() {
|
|||
//setSpriteTexture(0);
|
||||
}
|
||||
|
||||
void McRFPy_API::api_init(const McRogueFaceConfig& config, int argc, char** argv) {
|
||||
// Initialize Python with proper argv - this is CRITICAL
|
||||
PyStatus status = init_python_with_config(config, argc, argv);
|
||||
if (PyStatus_Exception(status)) {
|
||||
Py_ExitStatusException(status);
|
||||
}
|
||||
|
||||
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
|
||||
|
||||
// For -m module execution, let Python handle it
|
||||
if (!config.python_module.empty() && config.python_module != "venv") {
|
||||
// Py_RunMain() will handle -m execution
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute based on mode - this is handled in main.cpp now
|
||||
// The actual execution logic is in run_python_interpreter()
|
||||
|
||||
// Set up default resources only if in game mode
|
||||
if (!config.python_mode) {
|
||||
//PyModule_AddObject(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
|
||||
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
|
||||
//PyModule_AddObject(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
|
||||
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
|
||||
}
|
||||
}
|
||||
|
||||
void McRFPy_API::executeScript(std::string filename)
|
||||
{
|
||||
FILE* PScriptFile = fopen(filename.c_str(), "r");
|
||||
std::filesystem::path script_path(filename);
|
||||
|
||||
// If the path is relative and the file doesn't exist, try resolving it relative to the executable
|
||||
if (script_path.is_relative() && !std::filesystem::exists(script_path)) {
|
||||
// Get the directory where the executable is located using platform-specific function
|
||||
std::wstring exe_dir_w = executable_path();
|
||||
std::filesystem::path exe_dir(exe_dir_w);
|
||||
|
||||
// Try the script path relative to the executable directory
|
||||
std::filesystem::path resolved_path = exe_dir / script_path;
|
||||
if (std::filesystem::exists(resolved_path)) {
|
||||
script_path = resolved_path;
|
||||
}
|
||||
}
|
||||
|
||||
FILE* PScriptFile = fopen(script_path.string().c_str(), "r");
|
||||
if(PScriptFile) {
|
||||
PyRun_SimpleFile(PScriptFile, filename.c_str());
|
||||
PyRun_SimpleFile(PScriptFile, script_path.string().c_str());
|
||||
fclose(PScriptFile);
|
||||
} else {
|
||||
std::cout << "Failed to open script: " << script_path.string() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -230,63 +358,7 @@ void McRFPy_API::REPL_device(FILE * fp, const char *filename)
|
|||
}
|
||||
|
||||
// python connection
|
||||
PyObject* McRFPy_API::_registerPyAction(PyObject *self, PyObject *args)
|
||||
{
|
||||
PyObject* callable;
|
||||
const char * actionstr;
|
||||
if (!PyArg_ParseTuple(args, "sO", &actionstr, &callable)) return NULL;
|
||||
//TODO: if the string already exists in the callbacks map,
|
||||
// decrease our reference count so it can potentially be garbage collected
|
||||
callbacks[std::string(actionstr)] = callable;
|
||||
Py_INCREF(callable);
|
||||
|
||||
// return None correctly
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_registerInputAction(PyObject *self, PyObject *args)
|
||||
{
|
||||
int action_code;
|
||||
const char * actionstr;
|
||||
if (!PyArg_ParseTuple(args, "iz", &action_code, &actionstr)) return NULL;
|
||||
|
||||
bool success;
|
||||
|
||||
if (actionstr == NULL) { // Action provided is None, i.e. unregister
|
||||
std::cout << "Unregistering\n";
|
||||
success = game->currentScene()->unregisterActionInjected(action_code, std::string(actionstr) + "_py");
|
||||
} else {
|
||||
std::cout << "Registering " << actionstr << "_py to " << action_code << "\n";
|
||||
success = game->currentScene()->registerActionInjected(action_code, std::string(actionstr) + "_py");
|
||||
}
|
||||
|
||||
success ? Py_INCREF(Py_True) : Py_INCREF(Py_False);
|
||||
return success ? Py_True : Py_False;
|
||||
|
||||
}
|
||||
|
||||
void McRFPy_API::doAction(std::string actionstr) {
|
||||
// hard coded actions that require no registration
|
||||
//std::cout << "Calling Python Action: " << actionstr;
|
||||
if (!actionstr.compare("startrepl")) return McRFPy_API::REPL();
|
||||
if (callbacks.find(actionstr) == callbacks.end())
|
||||
{
|
||||
//std::cout << " (not found)" << std::endl;
|
||||
return;
|
||||
}
|
||||
//std::cout << " (" << PyUnicode_AsUTF8(PyObject_Repr(callbacks[actionstr])) << ")" << std::endl;
|
||||
PyObject* retval = PyObject_Call(callbacks[actionstr], PyTuple_New(0), NULL);
|
||||
if (!retval)
|
||||
{
|
||||
std::cout << "doAction 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 << "doAction returned a non-None value. It's not an error, it's just not being saved or used." << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
PyObject* McRFPy_API::_refreshFov(PyObject* self, PyObject* args) {
|
||||
|
@ -359,73 +431,10 @@ PyObject* McRFPy_API::_getSoundVolume(PyObject* self, PyObject* args) {
|
|||
return Py_BuildValue("f", McRFPy_API::sfx.getVolume());
|
||||
}
|
||||
|
||||
// Removed deprecated player_input, computerTurn, playerTurn functions
|
||||
// These were part of the old turn-based system that is no longer used
|
||||
|
||||
/*
|
||||
void McRFPy_API::player_input(int dx, int dy) {
|
||||
//std::cout << "# entities tagged 'player': " << McRFPy_API::entities.getEntities("player").size() << std::endl;
|
||||
auto player_entity = McRFPy_API::entities.getEntities("player")[0];
|
||||
auto grid = player_entity->cGrid->grid;
|
||||
//std::cout << "Grid pointed to: " << (long)player_entity->cGrid->grid << std::endl;
|
||||
if (McRFPy_API::input_mode.compare("playerturn") != 0) {
|
||||
// no input accepted while computer moving
|
||||
//std::cout << "Can't move while it's not player's turn." << std::endl;
|
||||
return;
|
||||
}
|
||||
// TODO: selection cursor via keyboard
|
||||
// else if (!input_mode.compare("selectpoint") {}
|
||||
// else if (!input_mode.compare("selectentity") {}
|
||||
|
||||
// grid bounds check
|
||||
if (player_entity->cGrid->x + dx < 0 ||
|
||||
player_entity->cGrid->y + dy < 0 ||
|
||||
player_entity->cGrid->x + dx > grid->grid_x - 1 ||
|
||||
player_entity->cGrid->y + dy > grid->grid_y - 1) {
|
||||
//std::cout << "(" << player_entity->cGrid->x << ", " << player_entity->cGrid->y <<
|
||||
// ") + (" << dx << ", " << dy << ") is OOB." << std::endl;
|
||||
return;
|
||||
}
|
||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(player_entity->cBehavior->object)) << std::endl;
|
||||
PyObject* move_fn = PyObject_GetAttrString(player_entity->cBehavior->object, "move");
|
||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(move_fn)) << std::endl;
|
||||
if (move_fn) {
|
||||
//std::cout << "Calling `move`" << std::endl;
|
||||
PyObject* move_args = Py_BuildValue("(ii)", dx, dy);
|
||||
PyObject_CallObject((PyObject*) move_fn, move_args);
|
||||
} else {
|
||||
//std::cout << "player_input called on entity with no `move` method" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void McRFPy_API::computerTurn() {
|
||||
McRFPy_API::input_mode = "computerturnrunning";
|
||||
for (auto e : McRFPy_API::grids[McRFPy_API::active_grid]->entities) {
|
||||
if (e->cBehavior) {
|
||||
PyObject_Call(PyObject_GetAttrString(e->cBehavior->object, "ai_act"), PyTuple_New(0), NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void McRFPy_API::playerTurn() {
|
||||
McRFPy_API::input_mode = "playerturn";
|
||||
for (auto e : McRFPy_API::entities.getEntities("player")) {
|
||||
if (e->cBehavior) {
|
||||
PyObject_Call(PyObject_GetAttrString(e->cBehavior->object, "player_act"), PyTuple_New(0), NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void McRFPy_API::camFollow() {
|
||||
if (!McRFPy_API::do_camfollow) return;
|
||||
auto& ag = McRFPy_API::grids[McRFPy_API::active_grid];
|
||||
for (auto e : McRFPy_API::entities.getEntities("player")) {
|
||||
//std::cout << "grid center: " << ag->center_x << ", " << ag->center_y << std::endl <<
|
||||
// "player grid pos: " << e->cGrid->x << ", " << e->cGrid->y << std::endl <<
|
||||
// "player sprite pos: " << e->cGrid->indexsprite.x << ", " << e->cGrid->indexsprite.y << std::endl;
|
||||
ag->center_x = e->cGrid->indexsprite.x * ag->grid_size + ag->grid_size * 0.5;
|
||||
ag->center_y = e->cGrid->indexsprite.y * ag->grid_size + ag->grid_size * 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
PyObject* McRFPy_API::_camFollow(PyObject* self, PyObject* args) {
|
||||
PyObject* set_camfollow = NULL;
|
||||
//std::cout << "camFollow Parse Args" << std::endl;
|
||||
|
@ -489,6 +498,13 @@ PyObject* McRFPy_API::_createScene(PyObject* self, PyObject* args) {
|
|||
PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) {
|
||||
PyObject* callable;
|
||||
if (!PyArg_ParseTuple(args, "O", &callable)) return NULL;
|
||||
|
||||
// Validate that the argument is callable
|
||||
if (!PyCallable_Check(callable)) {
|
||||
PyErr_SetString(PyExc_TypeError, "keypressScene() argument must be callable");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
if (game->currentScene()->key_callable != NULL and game->currentScene()->key_callable != Py_None)
|
||||
{
|
||||
|
@ -499,6 +515,7 @@ PyObject* McRFPy_API::_keypressScene(PyObject* self, PyObject* args) {
|
|||
Py_INCREF(Py_None);
|
||||
*/
|
||||
game->currentScene()->key_callable = std::make_unique<PyKeyCallable>(callable);
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
|
@ -538,3 +555,15 @@ PyObject* McRFPy_API::_setScale(PyObject* self, PyObject* args) {
|
|||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
void McRFPy_API::markSceneNeedsSort() {
|
||||
// Mark the current scene as needing a z_index sort
|
||||
auto scene = game->currentScene();
|
||||
if (scene && scene->ui_elements) {
|
||||
// Cast to PyScene to access ui_elements_need_sort
|
||||
PyScene* pyscene = dynamic_cast<PyScene*>(scene);
|
||||
if (pyscene) {
|
||||
pyscene->ui_elements_need_sort = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
#include "PyFont.h"
|
||||
#include "PyTexture.h"
|
||||
#include "McRogueFaceConfig.h"
|
||||
|
||||
class GameEngine; // forward declared (circular members)
|
||||
|
||||
|
@ -27,6 +28,8 @@ public:
|
|||
//static void setSpriteTexture(int);
|
||||
inline static GameEngine* game;
|
||||
static void api_init();
|
||||
static void api_init(const McRogueFaceConfig& config, int argc, char** argv);
|
||||
static PyStatus init_python_with_config(const McRogueFaceConfig& config, int argc, char** argv);
|
||||
static void api_shutdown();
|
||||
// Python API functionality - use mcrfpy.* in scripts
|
||||
//static PyObject* _drawSprite(PyObject*, PyObject*);
|
||||
|
@ -37,9 +40,6 @@ public:
|
|||
static sf::Music music;
|
||||
static sf::Sound sfx;
|
||||
|
||||
static std::map<std::string, PyObject*> callbacks;
|
||||
static PyObject* _registerPyAction(PyObject*, PyObject*);
|
||||
static PyObject* _registerInputAction(PyObject*, PyObject*);
|
||||
|
||||
static PyObject* _createSoundBuffer(PyObject*, PyObject*);
|
||||
static PyObject* _loadMusic(PyObject*, PyObject*);
|
||||
|
@ -66,12 +66,11 @@ public:
|
|||
|
||||
// accept keyboard input from scene
|
||||
static sf::Vector2i cursor_position;
|
||||
static void player_input(int, int);
|
||||
static void computerTurn();
|
||||
static void playerTurn();
|
||||
|
||||
static void doAction(std::string);
|
||||
|
||||
static void executeScript(std::string);
|
||||
static void executePyString(std::string);
|
||||
|
||||
// Helper to mark scenes as needing z_index resort
|
||||
static void markSceneNeedsSort();
|
||||
};
|
||||
|
|
|
@ -0,0 +1,817 @@
|
|||
#include "McRFPy_Automation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "GameEngine.h"
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <unordered_map>
|
||||
|
||||
// Helper function to get game engine
|
||||
GameEngine* McRFPy_Automation::getGameEngine() {
|
||||
return McRFPy_API::game;
|
||||
}
|
||||
|
||||
// Sleep helper
|
||||
void McRFPy_Automation::sleep_ms(int milliseconds) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
|
||||
}
|
||||
|
||||
// Convert string to SFML key code
|
||||
sf::Keyboard::Key McRFPy_Automation::stringToKey(const std::string& keyName) {
|
||||
static const std::unordered_map<std::string, sf::Keyboard::Key> keyMap = {
|
||||
// Letters
|
||||
{"a", sf::Keyboard::A}, {"b", sf::Keyboard::B}, {"c", sf::Keyboard::C},
|
||||
{"d", sf::Keyboard::D}, {"e", sf::Keyboard::E}, {"f", sf::Keyboard::F},
|
||||
{"g", sf::Keyboard::G}, {"h", sf::Keyboard::H}, {"i", sf::Keyboard::I},
|
||||
{"j", sf::Keyboard::J}, {"k", sf::Keyboard::K}, {"l", sf::Keyboard::L},
|
||||
{"m", sf::Keyboard::M}, {"n", sf::Keyboard::N}, {"o", sf::Keyboard::O},
|
||||
{"p", sf::Keyboard::P}, {"q", sf::Keyboard::Q}, {"r", sf::Keyboard::R},
|
||||
{"s", sf::Keyboard::S}, {"t", sf::Keyboard::T}, {"u", sf::Keyboard::U},
|
||||
{"v", sf::Keyboard::V}, {"w", sf::Keyboard::W}, {"x", sf::Keyboard::X},
|
||||
{"y", sf::Keyboard::Y}, {"z", sf::Keyboard::Z},
|
||||
|
||||
// Numbers
|
||||
{"0", sf::Keyboard::Num0}, {"1", sf::Keyboard::Num1}, {"2", sf::Keyboard::Num2},
|
||||
{"3", sf::Keyboard::Num3}, {"4", sf::Keyboard::Num4}, {"5", sf::Keyboard::Num5},
|
||||
{"6", sf::Keyboard::Num6}, {"7", sf::Keyboard::Num7}, {"8", sf::Keyboard::Num8},
|
||||
{"9", sf::Keyboard::Num9},
|
||||
|
||||
// Function keys
|
||||
{"f1", sf::Keyboard::F1}, {"f2", sf::Keyboard::F2}, {"f3", sf::Keyboard::F3},
|
||||
{"f4", sf::Keyboard::F4}, {"f5", sf::Keyboard::F5}, {"f6", sf::Keyboard::F6},
|
||||
{"f7", sf::Keyboard::F7}, {"f8", sf::Keyboard::F8}, {"f9", sf::Keyboard::F9},
|
||||
{"f10", sf::Keyboard::F10}, {"f11", sf::Keyboard::F11}, {"f12", sf::Keyboard::F12},
|
||||
{"f13", sf::Keyboard::F13}, {"f14", sf::Keyboard::F14}, {"f15", sf::Keyboard::F15},
|
||||
|
||||
// Special keys
|
||||
{"escape", sf::Keyboard::Escape}, {"esc", sf::Keyboard::Escape},
|
||||
{"enter", sf::Keyboard::Enter}, {"return", sf::Keyboard::Enter},
|
||||
{"space", sf::Keyboard::Space}, {" ", sf::Keyboard::Space},
|
||||
{"tab", sf::Keyboard::Tab}, {"\t", sf::Keyboard::Tab},
|
||||
{"backspace", sf::Keyboard::BackSpace},
|
||||
{"delete", sf::Keyboard::Delete}, {"del", sf::Keyboard::Delete},
|
||||
{"insert", sf::Keyboard::Insert},
|
||||
{"home", sf::Keyboard::Home},
|
||||
{"end", sf::Keyboard::End},
|
||||
{"pageup", sf::Keyboard::PageUp}, {"pgup", sf::Keyboard::PageUp},
|
||||
{"pagedown", sf::Keyboard::PageDown}, {"pgdn", sf::Keyboard::PageDown},
|
||||
|
||||
// Arrow keys
|
||||
{"left", sf::Keyboard::Left},
|
||||
{"right", sf::Keyboard::Right},
|
||||
{"up", sf::Keyboard::Up},
|
||||
{"down", sf::Keyboard::Down},
|
||||
|
||||
// Modifiers
|
||||
{"ctrl", sf::Keyboard::LControl}, {"ctrlleft", sf::Keyboard::LControl},
|
||||
{"ctrlright", sf::Keyboard::RControl},
|
||||
{"alt", sf::Keyboard::LAlt}, {"altleft", sf::Keyboard::LAlt},
|
||||
{"altright", sf::Keyboard::RAlt},
|
||||
{"shift", sf::Keyboard::LShift}, {"shiftleft", sf::Keyboard::LShift},
|
||||
{"shiftright", sf::Keyboard::RShift},
|
||||
{"win", sf::Keyboard::LSystem}, {"winleft", sf::Keyboard::LSystem},
|
||||
{"winright", sf::Keyboard::RSystem}, {"command", sf::Keyboard::LSystem},
|
||||
|
||||
// Punctuation
|
||||
{",", sf::Keyboard::Comma}, {".", sf::Keyboard::Period},
|
||||
{"/", sf::Keyboard::Slash}, {"\\", sf::Keyboard::BackSlash},
|
||||
{";", sf::Keyboard::SemiColon}, {"'", sf::Keyboard::Quote},
|
||||
{"[", sf::Keyboard::LBracket}, {"]", sf::Keyboard::RBracket},
|
||||
{"-", sf::Keyboard::Dash}, {"=", sf::Keyboard::Equal},
|
||||
|
||||
// Numpad
|
||||
{"num0", sf::Keyboard::Numpad0}, {"num1", sf::Keyboard::Numpad1},
|
||||
{"num2", sf::Keyboard::Numpad2}, {"num3", sf::Keyboard::Numpad3},
|
||||
{"num4", sf::Keyboard::Numpad4}, {"num5", sf::Keyboard::Numpad5},
|
||||
{"num6", sf::Keyboard::Numpad6}, {"num7", sf::Keyboard::Numpad7},
|
||||
{"num8", sf::Keyboard::Numpad8}, {"num9", sf::Keyboard::Numpad9},
|
||||
{"add", sf::Keyboard::Add}, {"subtract", sf::Keyboard::Subtract},
|
||||
{"multiply", sf::Keyboard::Multiply}, {"divide", sf::Keyboard::Divide},
|
||||
|
||||
// Other
|
||||
{"pause", sf::Keyboard::Pause},
|
||||
{"capslock", sf::Keyboard::LControl}, // Note: SFML doesn't have CapsLock
|
||||
{"numlock", sf::Keyboard::LControl}, // Note: SFML doesn't have NumLock
|
||||
{"scrolllock", sf::Keyboard::LControl}, // Note: SFML doesn't have ScrollLock
|
||||
};
|
||||
|
||||
auto it = keyMap.find(keyName);
|
||||
if (it != keyMap.end()) {
|
||||
return it->second;
|
||||
}
|
||||
return sf::Keyboard::Unknown;
|
||||
}
|
||||
|
||||
// Inject mouse event into the game engine
|
||||
void McRFPy_Automation::injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
sf::Event event;
|
||||
event.type = type;
|
||||
|
||||
switch (type) {
|
||||
case sf::Event::MouseMoved:
|
||||
event.mouseMove.x = x;
|
||||
event.mouseMove.y = y;
|
||||
break;
|
||||
case sf::Event::MouseButtonPressed:
|
||||
case sf::Event::MouseButtonReleased:
|
||||
event.mouseButton.button = button;
|
||||
event.mouseButton.x = x;
|
||||
event.mouseButton.y = y;
|
||||
break;
|
||||
case sf::Event::MouseWheelScrolled:
|
||||
event.mouseWheelScroll.wheel = sf::Mouse::VerticalWheel;
|
||||
event.mouseWheelScroll.delta = static_cast<float>(x); // x is used for scroll amount
|
||||
event.mouseWheelScroll.x = x;
|
||||
event.mouseWheelScroll.y = y;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
// Inject keyboard event into the game engine
|
||||
void McRFPy_Automation::injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
sf::Event event;
|
||||
event.type = type;
|
||||
|
||||
if (type == sf::Event::KeyPressed || type == sf::Event::KeyReleased) {
|
||||
event.key.code = key;
|
||||
event.key.alt = sf::Keyboard::isKeyPressed(sf::Keyboard::LAlt) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RAlt);
|
||||
event.key.control = sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RControl);
|
||||
event.key.shift = sf::Keyboard::isKeyPressed(sf::Keyboard::LShift) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RShift);
|
||||
event.key.system = sf::Keyboard::isKeyPressed(sf::Keyboard::LSystem) ||
|
||||
sf::Keyboard::isKeyPressed(sf::Keyboard::RSystem);
|
||||
}
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
// Inject text event for typing
|
||||
void McRFPy_Automation::injectTextEvent(sf::Uint32 unicode) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) return;
|
||||
|
||||
sf::Event event;
|
||||
event.type = sf::Event::TextEntered;
|
||||
event.text.unicode = unicode;
|
||||
|
||||
engine->processEvent(event);
|
||||
}
|
||||
|
||||
// Screenshot implementation
|
||||
PyObject* McRFPy_Automation::_screenshot(PyObject* self, PyObject* args) {
|
||||
const char* filename;
|
||||
if (!PyArg_ParseTuple(args, "s", &filename)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Game engine not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the render target
|
||||
sf::RenderTarget* target = engine->getRenderTargetPtr();
|
||||
if (!target) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No render target available");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// For RenderWindow, we can get a screenshot directly
|
||||
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);
|
||||
|
||||
if (texture.copyToImage().saveToFile(filename)) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
// For RenderTexture (headless mode)
|
||||
else if (auto* renderTexture = dynamic_cast<sf::RenderTexture*>(target)) {
|
||||
if (renderTexture->getTexture().copyToImage().saveToFile(filename)) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unknown render target type");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current mouse position
|
||||
PyObject* McRFPy_Automation::_position(PyObject* self, PyObject* args) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", 0, 0);
|
||||
}
|
||||
|
||||
// In headless mode, we'd need to track the simulated mouse position
|
||||
// For now, return the actual mouse position relative to window if available
|
||||
if (auto* window = dynamic_cast<sf::RenderWindow*>(engine->getRenderTargetPtr())) {
|
||||
sf::Vector2i pos = sf::Mouse::getPosition(*window);
|
||||
return Py_BuildValue("(ii)", pos.x, pos.y);
|
||||
}
|
||||
|
||||
// In headless mode, return simulated position (TODO: track this)
|
||||
return Py_BuildValue("(ii)", 0, 0);
|
||||
}
|
||||
|
||||
// Get screen size
|
||||
PyObject* McRFPy_Automation::_size(PyObject* self, PyObject* args) {
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
return Py_BuildValue("(ii)", 1024, 768); // Default size
|
||||
}
|
||||
|
||||
sf::Vector2u size = engine->getRenderTarget().getSize();
|
||||
return Py_BuildValue("(ii)", size.x, size.y);
|
||||
}
|
||||
|
||||
// Check if coordinates are on screen
|
||||
PyObject* McRFPy_Automation::_onScreen(PyObject* self, PyObject* args) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
auto engine = getGameEngine();
|
||||
if (!engine || !engine->getRenderTargetPtr()) {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
|
||||
sf::Vector2u size = engine->getRenderTarget().getSize();
|
||||
if (x >= 0 && x < (int)size.x && y >= 0 && y < (int)size.y) {
|
||||
Py_RETURN_TRUE;
|
||||
} else {
|
||||
Py_RETURN_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
// Move mouse to position
|
||||
PyObject* McRFPy_Automation::_moveTo(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "duration", NULL};
|
||||
int x, y;
|
||||
float duration = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
|
||||
&x, &y, &duration)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// TODO: Implement smooth movement with duration
|
||||
injectMouseEvent(sf::Event::MouseMoved, x, y);
|
||||
|
||||
if (duration > 0) {
|
||||
sleep_ms(static_cast<int>(duration * 1000));
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Move mouse relative
|
||||
PyObject* McRFPy_Automation::_moveRel(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"xOffset", "yOffset", "duration", NULL};
|
||||
int xOffset, yOffset;
|
||||
float duration = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|f", const_cast<char**>(kwlist),
|
||||
&xOffset, &yOffset, &duration)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
int currentX, currentY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Move to new position
|
||||
injectMouseEvent(sf::Event::MouseMoved, currentX + xOffset, currentY + yOffset);
|
||||
|
||||
if (duration > 0) {
|
||||
sleep_ms(static_cast<int>(duration * 1000));
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Click implementation
|
||||
PyObject* McRFPy_Automation::_click(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "clicks", "interval", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
int clicks = 1;
|
||||
float interval = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iiifs", const_cast<char**>(kwlist),
|
||||
&x, &y, &clicks, &interval, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
// Determine button
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
if (strcmp(button, "right") == 0) {
|
||||
sfButton = sf::Mouse::Right;
|
||||
} else if (strcmp(button, "middle") == 0) {
|
||||
sfButton = sf::Mouse::Middle;
|
||||
}
|
||||
|
||||
// Move to position first
|
||||
injectMouseEvent(sf::Event::MouseMoved, x, y);
|
||||
|
||||
// Perform clicks
|
||||
for (int i = 0; i < clicks; i++) {
|
||||
if (i > 0 && interval > 0) {
|
||||
sleep_ms(static_cast<int>(interval * 1000));
|
||||
}
|
||||
|
||||
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
|
||||
sleep_ms(10); // Small delay between press and release
|
||||
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Right click
|
||||
PyObject* McRFPy_Automation::_rightClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Build new args with button="right"
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("right"));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Double click
|
||||
PyObject* McRFPy_Automation::_doubleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(2));
|
||||
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Type text
|
||||
PyObject* McRFPy_Automation::_typewrite(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"message", "interval", NULL};
|
||||
const char* message;
|
||||
float interval = 0.0f;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s|f", const_cast<char**>(kwlist),
|
||||
&message, &interval)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type each character
|
||||
for (size_t i = 0; message[i] != '\0'; i++) {
|
||||
if (i > 0 && interval > 0) {
|
||||
sleep_ms(static_cast<int>(interval * 1000));
|
||||
}
|
||||
|
||||
char c = message[i];
|
||||
|
||||
// Handle special characters
|
||||
if (c == '\n') {
|
||||
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Enter);
|
||||
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Enter);
|
||||
} else if (c == '\t') {
|
||||
injectKeyEvent(sf::Event::KeyPressed, sf::Keyboard::Tab);
|
||||
injectKeyEvent(sf::Event::KeyReleased, sf::Keyboard::Tab);
|
||||
} else {
|
||||
// For regular characters, send text event
|
||||
injectTextEvent(static_cast<sf::Uint32>(c));
|
||||
}
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Press and hold key
|
||||
PyObject* McRFPy_Automation::_keyDown(PyObject* self, PyObject* args) {
|
||||
const char* keyName;
|
||||
if (!PyArg_ParseTuple(args, "s", &keyName)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
if (key == sf::Keyboard::Unknown) {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
injectKeyEvent(sf::Event::KeyPressed, key);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Release key
|
||||
PyObject* McRFPy_Automation::_keyUp(PyObject* self, PyObject* args) {
|
||||
const char* keyName;
|
||||
if (!PyArg_ParseTuple(args, "s", &keyName)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
if (key == sf::Keyboard::Unknown) {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
injectKeyEvent(sf::Event::KeyReleased, key);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Hotkey combination
|
||||
PyObject* McRFPy_Automation::_hotkey(PyObject* self, PyObject* args) {
|
||||
// Get all keys as separate arguments
|
||||
Py_ssize_t numKeys = PyTuple_Size(args);
|
||||
if (numKeys == 0) {
|
||||
PyErr_SetString(PyExc_ValueError, "hotkey() requires at least one key");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Press all keys
|
||||
for (Py_ssize_t i = 0; i < numKeys; i++) {
|
||||
PyObject* keyObj = PyTuple_GetItem(args, i);
|
||||
const char* keyName = PyUnicode_AsUTF8(keyObj);
|
||||
if (!keyName) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
if (key == sf::Keyboard::Unknown) {
|
||||
PyErr_Format(PyExc_ValueError, "Unknown key: %s", keyName);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
injectKeyEvent(sf::Event::KeyPressed, key);
|
||||
sleep_ms(10); // Small delay between key presses
|
||||
}
|
||||
|
||||
// Release all keys in reverse order
|
||||
for (Py_ssize_t i = numKeys - 1; i >= 0; i--) {
|
||||
PyObject* keyObj = PyTuple_GetItem(args, i);
|
||||
const char* keyName = PyUnicode_AsUTF8(keyObj);
|
||||
|
||||
sf::Keyboard::Key key = stringToKey(keyName);
|
||||
injectKeyEvent(sf::Event::KeyReleased, key);
|
||||
sleep_ms(10);
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Scroll wheel
|
||||
PyObject* McRFPy_Automation::_scroll(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"clicks", "x", "y", NULL};
|
||||
int clicks;
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "i|ii", const_cast<char**>(kwlist),
|
||||
&clicks, &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
// Inject scroll event
|
||||
injectMouseEvent(sf::Event::MouseWheelScrolled, clicks, y);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Other click types using the main click function
|
||||
PyObject* McRFPy_Automation::_middleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "button", PyUnicode_FromString("middle"));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_Automation::_tripleClick(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", NULL};
|
||||
int x = -1, y = -1;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ii", const_cast<char**>(kwlist), &x, &y)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* newKwargs = PyDict_New();
|
||||
PyDict_SetItemString(newKwargs, "clicks", PyLong_FromLong(3));
|
||||
PyDict_SetItemString(newKwargs, "interval", PyFloat_FromDouble(0.1));
|
||||
if (x != -1) PyDict_SetItemString(newKwargs, "x", PyLong_FromLong(x));
|
||||
if (y != -1) PyDict_SetItemString(newKwargs, "y", PyLong_FromLong(y));
|
||||
|
||||
PyObject* result = _click(self, PyTuple_New(0), newKwargs);
|
||||
Py_DECREF(newKwargs);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Mouse button press/release
|
||||
PyObject* McRFPy_Automation::_mouseDown(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
|
||||
&x, &y, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
if (strcmp(button, "right") == 0) {
|
||||
sfButton = sf::Mouse::Right;
|
||||
} else if (strcmp(button, "middle") == 0) {
|
||||
sfButton = sf::Mouse::Middle;
|
||||
}
|
||||
|
||||
injectMouseEvent(sf::Event::MouseButtonPressed, x, y, sfButton);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_Automation::_mouseUp(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "button", NULL};
|
||||
int x = -1, y = -1;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|iis", const_cast<char**>(kwlist),
|
||||
&x, &y, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// If no position specified, use current position
|
||||
if (x == -1 || y == -1) {
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
if (!PyArg_ParseTuple(pos, "ii", &x, &y)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
}
|
||||
|
||||
sf::Mouse::Button sfButton = sf::Mouse::Left;
|
||||
if (strcmp(button, "right") == 0) {
|
||||
sfButton = sf::Mouse::Right;
|
||||
} else if (strcmp(button, "middle") == 0) {
|
||||
sfButton = sf::Mouse::Middle;
|
||||
}
|
||||
|
||||
injectMouseEvent(sf::Event::MouseButtonReleased, x, y, sfButton);
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
// Drag operations
|
||||
PyObject* McRFPy_Automation::_dragTo(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"x", "y", "duration", "button", NULL};
|
||||
int x, y;
|
||||
float duration = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
|
||||
&x, &y, &duration, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
int startX, startY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", &startX, &startY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Mouse down at current position
|
||||
PyObject* downArgs = Py_BuildValue("(ii)", startX, startY);
|
||||
PyObject* downKwargs = PyDict_New();
|
||||
PyDict_SetItemString(downKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* downResult = _mouseDown(self, downArgs, downKwargs);
|
||||
Py_DECREF(downArgs);
|
||||
Py_DECREF(downKwargs);
|
||||
if (!downResult) return NULL;
|
||||
Py_DECREF(downResult);
|
||||
|
||||
// Move to target position
|
||||
if (duration > 0) {
|
||||
// Smooth movement
|
||||
int steps = static_cast<int>(duration * 60); // 60 FPS
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
int currentX = startX + (x - startX) * i / steps;
|
||||
int currentY = startY + (y - startY) * i / steps;
|
||||
injectMouseEvent(sf::Event::MouseMoved, currentX, currentY);
|
||||
sleep_ms(1000 / 60); // 60 FPS
|
||||
}
|
||||
} else {
|
||||
injectMouseEvent(sf::Event::MouseMoved, x, y);
|
||||
}
|
||||
|
||||
// Mouse up at target position
|
||||
PyObject* upArgs = Py_BuildValue("(ii)", x, y);
|
||||
PyObject* upKwargs = PyDict_New();
|
||||
PyDict_SetItemString(upKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* upResult = _mouseUp(self, upArgs, upKwargs);
|
||||
Py_DECREF(upArgs);
|
||||
Py_DECREF(upKwargs);
|
||||
if (!upResult) return NULL;
|
||||
Py_DECREF(upResult);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* McRFPy_Automation::_dragRel(PyObject* self, PyObject* args, PyObject* kwargs) {
|
||||
static const char* kwlist[] = {"xOffset", "yOffset", "duration", "button", NULL};
|
||||
int xOffset, yOffset;
|
||||
float duration = 0.0f;
|
||||
const char* button = "left";
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "ii|fs", const_cast<char**>(kwlist),
|
||||
&xOffset, &yOffset, &duration, &button)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current position
|
||||
PyObject* pos = _position(self, NULL);
|
||||
if (!pos) return NULL;
|
||||
|
||||
int currentX, currentY;
|
||||
if (!PyArg_ParseTuple(pos, "ii", ¤tX, ¤tY)) {
|
||||
Py_DECREF(pos);
|
||||
return NULL;
|
||||
}
|
||||
Py_DECREF(pos);
|
||||
|
||||
// Call dragTo with absolute position
|
||||
PyObject* dragArgs = Py_BuildValue("(ii)", currentX + xOffset, currentY + yOffset);
|
||||
PyObject* dragKwargs = PyDict_New();
|
||||
PyDict_SetItemString(dragKwargs, "duration", PyFloat_FromDouble(duration));
|
||||
PyDict_SetItemString(dragKwargs, "button", PyUnicode_FromString(button));
|
||||
|
||||
PyObject* result = _dragTo(self, dragArgs, dragKwargs);
|
||||
Py_DECREF(dragArgs);
|
||||
Py_DECREF(dragKwargs);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Method definitions for the automation module
|
||||
static PyMethodDef automationMethods[] = {
|
||||
{"screenshot", McRFPy_Automation::_screenshot, METH_VARARGS,
|
||||
"screenshot(filename) - Save a screenshot to the specified file"},
|
||||
|
||||
{"position", McRFPy_Automation::_position, METH_NOARGS,
|
||||
"position() - Get current mouse position as (x, y) tuple"},
|
||||
{"size", McRFPy_Automation::_size, METH_NOARGS,
|
||||
"size() - Get screen size as (width, height) tuple"},
|
||||
{"onScreen", McRFPy_Automation::_onScreen, METH_VARARGS,
|
||||
"onScreen(x, y) - Check if coordinates are within screen bounds"},
|
||||
|
||||
{"moveTo", (PyCFunction)McRFPy_Automation::_moveTo, METH_VARARGS | METH_KEYWORDS,
|
||||
"moveTo(x, y, duration=0.0) - Move mouse to absolute position"},
|
||||
{"moveRel", (PyCFunction)McRFPy_Automation::_moveRel, METH_VARARGS | METH_KEYWORDS,
|
||||
"moveRel(xOffset, yOffset, duration=0.0) - Move mouse relative to current position"},
|
||||
{"dragTo", (PyCFunction)McRFPy_Automation::_dragTo, METH_VARARGS | METH_KEYWORDS,
|
||||
"dragTo(x, y, duration=0.0, button='left') - Drag mouse to position"},
|
||||
{"dragRel", (PyCFunction)McRFPy_Automation::_dragRel, METH_VARARGS | METH_KEYWORDS,
|
||||
"dragRel(xOffset, yOffset, duration=0.0, button='left') - Drag mouse relative to current position"},
|
||||
|
||||
{"click", (PyCFunction)McRFPy_Automation::_click, METH_VARARGS | METH_KEYWORDS,
|
||||
"click(x=None, y=None, clicks=1, interval=0.0, button='left') - Click at position"},
|
||||
{"rightClick", (PyCFunction)McRFPy_Automation::_rightClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"rightClick(x=None, y=None) - Right click at position"},
|
||||
{"middleClick", (PyCFunction)McRFPy_Automation::_middleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"middleClick(x=None, y=None) - Middle click at position"},
|
||||
{"doubleClick", (PyCFunction)McRFPy_Automation::_doubleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"doubleClick(x=None, y=None) - Double click at position"},
|
||||
{"tripleClick", (PyCFunction)McRFPy_Automation::_tripleClick, METH_VARARGS | METH_KEYWORDS,
|
||||
"tripleClick(x=None, y=None) - Triple click at position"},
|
||||
{"scroll", (PyCFunction)McRFPy_Automation::_scroll, METH_VARARGS | METH_KEYWORDS,
|
||||
"scroll(clicks, x=None, y=None) - Scroll wheel at position"},
|
||||
{"mouseDown", (PyCFunction)McRFPy_Automation::_mouseDown, METH_VARARGS | METH_KEYWORDS,
|
||||
"mouseDown(x=None, y=None, button='left') - Press mouse button"},
|
||||
{"mouseUp", (PyCFunction)McRFPy_Automation::_mouseUp, METH_VARARGS | METH_KEYWORDS,
|
||||
"mouseUp(x=None, y=None, button='left') - Release mouse button"},
|
||||
|
||||
{"typewrite", (PyCFunction)McRFPy_Automation::_typewrite, METH_VARARGS | METH_KEYWORDS,
|
||||
"typewrite(message, interval=0.0) - Type text with optional interval between keystrokes"},
|
||||
{"hotkey", McRFPy_Automation::_hotkey, METH_VARARGS,
|
||||
"hotkey(*keys) - Press a hotkey combination (e.g., hotkey('ctrl', 'c'))"},
|
||||
{"keyDown", McRFPy_Automation::_keyDown, METH_VARARGS,
|
||||
"keyDown(key) - Press and hold a key"},
|
||||
{"keyUp", McRFPy_Automation::_keyUp, METH_VARARGS,
|
||||
"keyUp(key) - Release a key"},
|
||||
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
// Module definition for mcrfpy.automation
|
||||
static PyModuleDef automationModule = {
|
||||
PyModuleDef_HEAD_INIT,
|
||||
"mcrfpy.automation",
|
||||
"Automation API for McRogueFace - PyAutoGUI-compatible interface",
|
||||
-1,
|
||||
automationMethods
|
||||
};
|
||||
|
||||
// Initialize automation submodule
|
||||
PyObject* McRFPy_Automation::init_automation_module() {
|
||||
PyObject* module = PyModule_Create(&automationModule);
|
||||
if (module == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return module;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
#pragma once
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
class GameEngine;
|
||||
|
||||
class McRFPy_Automation {
|
||||
public:
|
||||
// Initialize the automation submodule
|
||||
static PyObject* init_automation_module();
|
||||
|
||||
// Screenshot functionality
|
||||
static PyObject* _screenshot(PyObject* self, PyObject* args);
|
||||
|
||||
// Mouse position and screen info
|
||||
static PyObject* _position(PyObject* self, PyObject* args);
|
||||
static PyObject* _size(PyObject* self, PyObject* args);
|
||||
static PyObject* _onScreen(PyObject* self, PyObject* args);
|
||||
|
||||
// Mouse movement
|
||||
static PyObject* _moveTo(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _moveRel(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _dragTo(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _dragRel(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
|
||||
// Mouse clicks
|
||||
static PyObject* _click(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _rightClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _middleClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _doubleClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _tripleClick(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _scroll(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _mouseDown(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _mouseUp(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
|
||||
// Keyboard
|
||||
static PyObject* _typewrite(PyObject* self, PyObject* args, PyObject* kwargs);
|
||||
static PyObject* _hotkey(PyObject* self, PyObject* args);
|
||||
static PyObject* _keyDown(PyObject* self, PyObject* args);
|
||||
static PyObject* _keyUp(PyObject* self, PyObject* args);
|
||||
|
||||
// Helper functions
|
||||
static void injectMouseEvent(sf::Event::EventType type, int x, int y, sf::Mouse::Button button = sf::Mouse::Left);
|
||||
static void injectKeyEvent(sf::Event::EventType type, sf::Keyboard::Key key);
|
||||
static void injectTextEvent(sf::Uint32 unicode);
|
||||
static sf::Keyboard::Key stringToKey(const std::string& keyName);
|
||||
static void sleep_ms(int milliseconds);
|
||||
|
||||
private:
|
||||
static GameEngine* getGameEngine();
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
#ifndef MCROGUEFACE_CONFIG_H
|
||||
#define MCROGUEFACE_CONFIG_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <filesystem>
|
||||
|
||||
struct McRogueFaceConfig {
|
||||
// McRogueFace specific
|
||||
bool headless = false;
|
||||
bool audio_enabled = true;
|
||||
|
||||
// Python interpreter emulation
|
||||
bool python_mode = false;
|
||||
std::string python_command; // -c command
|
||||
std::string python_module; // -m module
|
||||
bool interactive_mode = false; // -i flag
|
||||
bool show_version = false; // -V flag
|
||||
bool show_help = false; // -h flag
|
||||
|
||||
// Script execution
|
||||
std::filesystem::path script_path;
|
||||
std::vector<std::string> script_args;
|
||||
|
||||
// Scripts to execute before main script (--exec flag)
|
||||
std::vector<std::filesystem::path> exec_scripts;
|
||||
|
||||
// Screenshot functionality for headless mode
|
||||
std::string screenshot_path;
|
||||
bool take_screenshot = false;
|
||||
};
|
||||
|
||||
#endif // MCROGUEFACE_CONFIG_H
|
|
@ -0,0 +1,234 @@
|
|||
#include "PyAnimation.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "UIDrawable.h"
|
||||
#include "UIFrame.h"
|
||||
#include "UICaption.h"
|
||||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "UIEntity.h"
|
||||
#include "UI.h" // For the PyTypeObject definitions
|
||||
#include <cstring>
|
||||
|
||||
PyObject* PyAnimation::create(PyTypeObject* type, PyObject* args, PyObject* kwds) {
|
||||
PyAnimationObject* self = (PyAnimationObject*)type->tp_alloc(type, 0);
|
||||
if (self != NULL) {
|
||||
// Will be initialized in init
|
||||
}
|
||||
return (PyObject*)self;
|
||||
}
|
||||
|
||||
int PyAnimation::init(PyAnimationObject* self, PyObject* args, PyObject* kwds) {
|
||||
static const char* keywords[] = {"property", "target", "duration", "easing", "delta", nullptr};
|
||||
|
||||
const char* property_name;
|
||||
PyObject* target_value;
|
||||
float duration;
|
||||
const char* easing_name = "linear";
|
||||
int delta = 0;
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "sOf|sp", const_cast<char**>(keywords),
|
||||
&property_name, &target_value, &duration, &easing_name, &delta)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert Python target value to AnimationValue
|
||||
AnimationValue animValue;
|
||||
|
||||
if (PyFloat_Check(target_value)) {
|
||||
animValue = static_cast<float>(PyFloat_AsDouble(target_value));
|
||||
}
|
||||
else if (PyLong_Check(target_value)) {
|
||||
animValue = static_cast<int>(PyLong_AsLong(target_value));
|
||||
}
|
||||
else if (PyList_Check(target_value)) {
|
||||
// List of integers for sprite animation
|
||||
std::vector<int> indices;
|
||||
Py_ssize_t size = PyList_Size(target_value);
|
||||
for (Py_ssize_t i = 0; i < size; i++) {
|
||||
PyObject* item = PyList_GetItem(target_value, i);
|
||||
if (PyLong_Check(item)) {
|
||||
indices.push_back(PyLong_AsLong(item));
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "Sprite animation list must contain only integers");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
animValue = indices;
|
||||
}
|
||||
else if (PyTuple_Check(target_value)) {
|
||||
Py_ssize_t size = PyTuple_Size(target_value);
|
||||
if (size == 2) {
|
||||
// Vector2f
|
||||
float x = PyFloat_AsDouble(PyTuple_GetItem(target_value, 0));
|
||||
float y = PyFloat_AsDouble(PyTuple_GetItem(target_value, 1));
|
||||
animValue = sf::Vector2f(x, y);
|
||||
}
|
||||
else if (size == 3 || size == 4) {
|
||||
// Color (RGB or RGBA)
|
||||
int r = PyLong_AsLong(PyTuple_GetItem(target_value, 0));
|
||||
int g = PyLong_AsLong(PyTuple_GetItem(target_value, 1));
|
||||
int b = PyLong_AsLong(PyTuple_GetItem(target_value, 2));
|
||||
int a = size == 4 ? PyLong_AsLong(PyTuple_GetItem(target_value, 3)) : 255;
|
||||
animValue = sf::Color(r, g, b, a);
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_ValueError, "Tuple must have 2 elements (vector) or 3-4 elements (color)");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
else if (PyUnicode_Check(target_value)) {
|
||||
// String for text animation
|
||||
const char* str = PyUnicode_AsUTF8(target_value);
|
||||
animValue = std::string(str);
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "Target value must be float, int, list, tuple, or string");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get easing function
|
||||
EasingFunction easingFunc = EasingFunctions::getByName(easing_name);
|
||||
|
||||
// Create the Animation
|
||||
self->data = std::make_shared<Animation>(property_name, animValue, duration, easingFunc, delta != 0);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void PyAnimation::dealloc(PyAnimationObject* self) {
|
||||
self->data.reset();
|
||||
Py_TYPE(self)->tp_free((PyObject*)self);
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::get_property(PyAnimationObject* self, void* closure) {
|
||||
return PyUnicode_FromString(self->data->getTargetProperty().c_str());
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::get_duration(PyAnimationObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getDuration());
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::get_elapsed(PyAnimationObject* self, void* closure) {
|
||||
return PyFloat_FromDouble(self->data->getElapsed());
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::get_is_complete(PyAnimationObject* self, void* closure) {
|
||||
return PyBool_FromLong(self->data->isComplete());
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::get_is_delta(PyAnimationObject* self, void* closure) {
|
||||
return PyBool_FromLong(self->data->isDelta());
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::start(PyAnimationObject* self, PyObject* args) {
|
||||
PyObject* target_obj;
|
||||
if (!PyArg_ParseTuple(args, "O", &target_obj)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the UIDrawable from the Python object
|
||||
UIDrawable* drawable = nullptr;
|
||||
|
||||
// Check type by comparing type names
|
||||
const char* type_name = Py_TYPE(target_obj)->tp_name;
|
||||
|
||||
if (strcmp(type_name, "mcrfpy.Frame") == 0) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)target_obj;
|
||||
drawable = frame->data.get();
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Caption") == 0) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)target_obj;
|
||||
drawable = caption->data.get();
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Sprite") == 0) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)target_obj;
|
||||
drawable = sprite->data.get();
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Grid") == 0) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)target_obj;
|
||||
drawable = grid->data.get();
|
||||
}
|
||||
else if (strcmp(type_name, "mcrfpy.Entity") == 0) {
|
||||
// Special handling for Entity since it doesn't inherit from UIDrawable
|
||||
PyUIEntityObject* entity = (PyUIEntityObject*)target_obj;
|
||||
// Start the animation directly on the entity
|
||||
self->data->startEntity(entity->data.get());
|
||||
|
||||
// Add to AnimationManager
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
else {
|
||||
PyErr_SetString(PyExc_TypeError, "Target must be a Frame, Caption, Sprite, Grid, or Entity");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Start the animation
|
||||
self->data->start(drawable);
|
||||
|
||||
// Add to AnimationManager
|
||||
AnimationManager::getInstance().addAnimation(self->data);
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::update(PyAnimationObject* self, PyObject* args) {
|
||||
float deltaTime;
|
||||
if (!PyArg_ParseTuple(args, "f", &deltaTime)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool still_running = self->data->update(deltaTime);
|
||||
return PyBool_FromLong(still_running);
|
||||
}
|
||||
|
||||
PyObject* PyAnimation::get_current_value(PyAnimationObject* self, PyObject* args) {
|
||||
AnimationValue value = self->data->getCurrentValue();
|
||||
|
||||
// Convert AnimationValue back to Python
|
||||
return std::visit([](const auto& val) -> PyObject* {
|
||||
using T = std::decay_t<decltype(val)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, float>) {
|
||||
return PyFloat_FromDouble(val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, int>) {
|
||||
return PyLong_FromLong(val);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::vector<int>>) {
|
||||
// This shouldn't happen as we interpolate to int
|
||||
return PyLong_FromLong(0);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Color>) {
|
||||
return Py_BuildValue("(iiii)", val.r, val.g, val.b, val.a);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, sf::Vector2f>) {
|
||||
return Py_BuildValue("(ff)", val.x, val.y);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, std::string>) {
|
||||
return PyUnicode_FromString(val.c_str());
|
||||
}
|
||||
|
||||
Py_RETURN_NONE;
|
||||
}, value);
|
||||
}
|
||||
|
||||
PyGetSetDef PyAnimation::getsetters[] = {
|
||||
{"property", (getter)get_property, NULL, "Target property name", NULL},
|
||||
{"duration", (getter)get_duration, NULL, "Animation duration in seconds", NULL},
|
||||
{"elapsed", (getter)get_elapsed, NULL, "Elapsed time in seconds", NULL},
|
||||
{"is_complete", (getter)get_is_complete, NULL, "Whether animation is complete", NULL},
|
||||
{"is_delta", (getter)get_is_delta, NULL, "Whether animation uses delta mode", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
PyMethodDef PyAnimation::methods[] = {
|
||||
{"start", (PyCFunction)start, METH_VARARGS,
|
||||
"Start the animation on a target UIDrawable"},
|
||||
{"update", (PyCFunction)update, METH_VARARGS,
|
||||
"Update the animation by deltaTime (returns True if still running)"},
|
||||
{"get_current_value", (PyCFunction)get_current_value, METH_NOARGS,
|
||||
"Get the current interpolated value"},
|
||||
{NULL}
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include "Common.h"
|
||||
#include "Python.h"
|
||||
#include "structmember.h"
|
||||
#include "Animation.h"
|
||||
#include <memory>
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
std::shared_ptr<Animation> data;
|
||||
} PyAnimationObject;
|
||||
|
||||
class PyAnimation {
|
||||
public:
|
||||
static PyObject* create(PyTypeObject* type, PyObject* args, PyObject* kwds);
|
||||
static int init(PyAnimationObject* self, PyObject* args, PyObject* kwds);
|
||||
static void dealloc(PyAnimationObject* self);
|
||||
|
||||
// Properties
|
||||
static PyObject* get_property(PyAnimationObject* self, void* closure);
|
||||
static PyObject* get_duration(PyAnimationObject* self, void* closure);
|
||||
static PyObject* get_elapsed(PyAnimationObject* self, void* closure);
|
||||
static PyObject* get_is_complete(PyAnimationObject* self, void* closure);
|
||||
static PyObject* get_is_delta(PyAnimationObject* self, void* closure);
|
||||
|
||||
// Methods
|
||||
static PyObject* start(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* update(PyAnimationObject* self, PyObject* args);
|
||||
static PyObject* get_current_value(PyAnimationObject* self, PyObject* args);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyMethodDef methods[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
static PyTypeObject PyAnimationType = {
|
||||
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||
.tp_name = "mcrfpy.Animation",
|
||||
.tp_basicsize = sizeof(PyAnimationObject),
|
||||
.tp_itemsize = 0,
|
||||
.tp_dealloc = (destructor)PyAnimation::dealloc,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Animation object for animating UI properties"),
|
||||
.tp_methods = PyAnimation::methods,
|
||||
.tp_getset = PyAnimation::getsetters,
|
||||
.tp_init = (initproc)PyAnimation::init,
|
||||
.tp_new = PyAnimation::create,
|
||||
};
|
||||
}
|
|
@ -133,13 +133,58 @@ PyObject* PyColor::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
|
||||
PyObject* PyColor::get_member(PyObject* obj, void* closure)
|
||||
{
|
||||
// TODO
|
||||
return Py_None;
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
long member = (long)closure;
|
||||
|
||||
switch (member) {
|
||||
case 0: // r
|
||||
return PyLong_FromLong(self->data.r);
|
||||
case 1: // g
|
||||
return PyLong_FromLong(self->data.g);
|
||||
case 2: // b
|
||||
return PyLong_FromLong(self->data.b);
|
||||
case 3: // a
|
||||
return PyLong_FromLong(self->data.a);
|
||||
default:
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int PyColor::set_member(PyObject* obj, PyObject* value, void* closure)
|
||||
{
|
||||
// TODO
|
||||
PyColorObject* self = (PyColorObject*)obj;
|
||||
long member = (long)closure;
|
||||
|
||||
if (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "Color values must be integers");
|
||||
return -1;
|
||||
}
|
||||
|
||||
long val = PyLong_AsLong(value);
|
||||
if (val < 0 || val > 255) {
|
||||
PyErr_SetString(PyExc_ValueError, "Color values must be between 0 and 255");
|
||||
return -1;
|
||||
}
|
||||
|
||||
switch (member) {
|
||||
case 0: // r
|
||||
self->data.r = static_cast<sf::Uint8>(val);
|
||||
break;
|
||||
case 1: // g
|
||||
self->data.g = static_cast<sf::Uint8>(val);
|
||||
break;
|
||||
case 2: // b
|
||||
self->data.b = static_cast<sf::Uint8>(val);
|
||||
break;
|
||||
case 3: // a
|
||||
self->data.a = static_cast<sf::Uint8>(val);
|
||||
break;
|
||||
default:
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid color member");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -61,3 +61,19 @@ PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
{
|
||||
return (PyObject*)type->tp_alloc(type, 0);
|
||||
}
|
||||
|
||||
PyObject* PyFont::get_family(PyFontObject* self, void* closure)
|
||||
{
|
||||
return PyUnicode_FromString(self->data->font.getInfo().family.c_str());
|
||||
}
|
||||
|
||||
PyObject* PyFont::get_source(PyFontObject* self, void* closure)
|
||||
{
|
||||
return PyUnicode_FromString(self->data->source.c_str());
|
||||
}
|
||||
|
||||
PyGetSetDef PyFont::getsetters[] = {
|
||||
{"family", (getter)PyFont::get_family, NULL, "Font family name", NULL},
|
||||
{"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
@ -21,6 +21,12 @@ public:
|
|||
static Py_hash_t hash(PyObject*);
|
||||
static int init(PyFontObject*, PyObject*, PyObject*);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
|
||||
|
||||
// Getters for properties
|
||||
static PyObject* get_family(PyFontObject* self, void* closure);
|
||||
static PyObject* get_source(PyFontObject* self, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
@ -33,6 +39,7 @@ namespace mcrfpydef {
|
|||
//.tp_hash = PyFont::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Font Object"),
|
||||
.tp_getset = PyFont::getsetters,
|
||||
//.tp_base = &PyBaseObject_Type,
|
||||
.tp_init = (initproc)PyFont::init,
|
||||
.tp_new = PyType_GenericNew, //PyFont::pynew,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#include "ActionCode.h"
|
||||
#include "Resources.h"
|
||||
#include "PyCallable.h"
|
||||
#include <algorithm>
|
||||
|
||||
PyScene::PyScene(GameEngine* g) : Scene(g)
|
||||
{
|
||||
|
@ -21,6 +22,11 @@ void PyScene::update()
|
|||
|
||||
void PyScene::do_mouse_input(std::string button, std::string type)
|
||||
{
|
||||
// In headless mode, mouse input is not available
|
||||
if (game->isHeadless()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto unscaledmousepos = sf::Mouse::getPosition(game->getWindow());
|
||||
auto mousepos = game->getWindow().mapPixelToCoords(unscaledmousepos);
|
||||
UIDrawable* target;
|
||||
|
@ -49,10 +55,7 @@ void PyScene::do_mouse_input(std::string button, std::string type)
|
|||
|
||||
void PyScene::doAction(std::string name, std::string type)
|
||||
{
|
||||
if (ACTIONPY) {
|
||||
McRFPy_API::doAction(name.substr(0, name.size() - 3));
|
||||
}
|
||||
else if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
|
||||
if (name.compare("left") == 0 || name.compare("rclick") == 0 || name.compare("wheel_up") == 0 || name.compare("wheel_down") == 0) {
|
||||
do_mouse_input(name, type);
|
||||
}
|
||||
else if ACTIONONCE("debug_menu") {
|
||||
|
@ -62,14 +65,23 @@ void PyScene::doAction(std::string name, std::string type)
|
|||
|
||||
void PyScene::render()
|
||||
{
|
||||
game->getWindow().clear();
|
||||
game->getRenderTarget().clear();
|
||||
|
||||
auto vec = *ui_elements;
|
||||
for (auto e: vec)
|
||||
// Only sort if z_index values have changed
|
||||
if (ui_elements_need_sort) {
|
||||
std::sort(ui_elements->begin(), ui_elements->end(),
|
||||
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
|
||||
return a->z_index < b->z_index;
|
||||
});
|
||||
ui_elements_need_sort = false;
|
||||
}
|
||||
|
||||
// Render in sorted order (no need to copy anymore)
|
||||
for (auto e: *ui_elements)
|
||||
{
|
||||
if (e)
|
||||
e->render();
|
||||
}
|
||||
|
||||
game->getWindow().display();
|
||||
// Display is handled by GameEngine
|
||||
}
|
||||
|
|
|
@ -14,4 +14,7 @@ public:
|
|||
void render() override final;
|
||||
|
||||
void do_mouse_input(std::string, std::string);
|
||||
|
||||
// Dirty flag for z_index sorting optimization
|
||||
bool ui_elements_need_sort = true;
|
||||
};
|
||||
|
|
|
@ -79,3 +79,43 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
{
|
||||
return (PyObject*)type->tp_alloc(type, 0);
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_sprite_width(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->sprite_width);
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_sprite_height(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->sprite_height);
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_sheet_width(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->sheet_width);
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_sheet_height(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->sheet_height);
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_sprite_count(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyLong_FromLong(self->data->getSpriteCount());
|
||||
}
|
||||
|
||||
PyObject* PyTexture::get_source(PyTextureObject* self, void* closure)
|
||||
{
|
||||
return PyUnicode_FromString(self->data->source.c_str());
|
||||
}
|
||||
|
||||
PyGetSetDef PyTexture::getsetters[] = {
|
||||
{"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL},
|
||||
{"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL},
|
||||
{"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL},
|
||||
{"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL},
|
||||
{"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL},
|
||||
{"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
|
|
|
@ -19,12 +19,23 @@ public:
|
|||
int sprite_width, sprite_height; // just use them read only, OK?
|
||||
PyTexture(std::string filename, int sprite_w, int sprite_h);
|
||||
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
|
||||
int getSpriteCount() const { return sheet_width * sheet_height; }
|
||||
|
||||
PyObject* pyObject();
|
||||
static PyObject* repr(PyObject*);
|
||||
static Py_hash_t hash(PyObject*);
|
||||
static int init(PyTextureObject*, PyObject*, PyObject*);
|
||||
static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL);
|
||||
|
||||
// Getters for properties
|
||||
static PyObject* get_sprite_width(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_sprite_height(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_sheet_width(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_sheet_height(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_sprite_count(PyTextureObject* self, void* closure);
|
||||
static PyObject* get_source(PyTextureObject* self, void* closure);
|
||||
|
||||
static PyGetSetDef getsetters[];
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
@ -37,6 +48,7 @@ namespace mcrfpydef {
|
|||
.tp_hash = PyTexture::hash,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("SFML Texture Object"),
|
||||
.tp_getset = PyTexture::getsetters,
|
||||
//.tp_base = &PyBaseObject_Type,
|
||||
.tp_init = (initproc)PyTexture::init,
|
||||
.tp_new = PyType_GenericNew, //PyTexture::pynew,
|
||||
|
|
|
@ -106,13 +106,37 @@ PyObject* PyVector::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds)
|
|||
|
||||
PyObject* PyVector::get_member(PyObject* obj, void* closure)
|
||||
{
|
||||
// TODO
|
||||
return Py_None;
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
if (reinterpret_cast<long>(closure) == 0) {
|
||||
// x
|
||||
return PyFloat_FromDouble(self->data.x);
|
||||
} else {
|
||||
// y
|
||||
return PyFloat_FromDouble(self->data.y);
|
||||
}
|
||||
}
|
||||
|
||||
int PyVector::set_member(PyObject* obj, PyObject* value, void* closure)
|
||||
{
|
||||
// TODO
|
||||
PyVectorObject* self = (PyVectorObject*)obj;
|
||||
float val;
|
||||
|
||||
if (PyFloat_Check(value)) {
|
||||
val = PyFloat_AsDouble(value);
|
||||
} else if (PyLong_Check(value)) {
|
||||
val = PyLong_AsDouble(value);
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "Vector members must be numeric");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (reinterpret_cast<long>(closure) == 0) {
|
||||
// x
|
||||
self->data.x = val;
|
||||
} else {
|
||||
// y
|
||||
self->data.y = val;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -120,11 +144,31 @@ PyVectorObject* PyVector::from_arg(PyObject* args)
|
|||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
if (PyObject_IsInstance(args, (PyObject*)type)) return (PyVectorObject*)args;
|
||||
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
int err = init(obj, args, NULL);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
|
||||
// Handle different input types
|
||||
if (PyTuple_Check(args)) {
|
||||
// It's already a tuple, pass it directly to init
|
||||
int err = init(obj, args, NULL);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
} else {
|
||||
// Wrap single argument in a tuple for init
|
||||
PyObject* tuple = PyTuple_Pack(1, args);
|
||||
if (!tuple) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
int err = init(obj, tuple, NULL);
|
||||
Py_DECREF(tuple);
|
||||
if (err) {
|
||||
Py_DECREF(obj);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
|
|
@ -30,16 +30,6 @@ std::string Scene::action(int code)
|
|||
return actions[code];
|
||||
}
|
||||
|
||||
bool Scene::registerActionInjected(int code, std::string name)
|
||||
{
|
||||
std::cout << "Inject registered action - default implementation\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Scene::unregisterActionInjected(int code, std::string name)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void Scene::key_register(PyObject* callable)
|
||||
{
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#define ACTION(X, Y) (name.compare(X) == 0 && type.compare(Y) == 0)
|
||||
#define ACTIONONCE(X) ((name.compare(X) == 0 && type.compare("start") == 0 && !actionState[name]))
|
||||
#define ACTIONAFTER(X) ((name.compare(X) == 0 && type.compare("end") == 0))
|
||||
#define ACTIONPY ((name.size() > 3 && name.compare(name.size() - 3, 3, "_py") == 0))
|
||||
|
||||
#include "Common.h"
|
||||
#include <list>
|
||||
|
@ -37,8 +36,6 @@ public:
|
|||
bool hasAction(int);
|
||||
std::string action(int);
|
||||
|
||||
virtual bool registerActionInjected(int, std::string);
|
||||
virtual bool unregisterActionInjected(int, std::string);
|
||||
|
||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> ui_elements;
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "PyColor.h"
|
||||
#include "PyVector.h"
|
||||
#include "PyFont.h"
|
||||
#include <algorithm>
|
||||
|
||||
UIDrawable* UICaption::click_at(sf::Vector2f point)
|
||||
{
|
||||
|
@ -196,8 +197,9 @@ PyGetSetDef UICaption::getsetters[] = {
|
|||
{"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1},
|
||||
//{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL},
|
||||
{"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL},
|
||||
{"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text 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},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
@ -234,7 +236,7 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
|
||||
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffzOOOf",
|
||||
// const_cast<char**>(keywords), &x, &y, &text, &font, &fill_color, &outline_color, &outline))
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|zOOOf",
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Oz|OOOf",
|
||||
const_cast<char**>(keywords), &pos, &text, &font, &fill_color, &outline_color, &outline))
|
||||
{
|
||||
return -1;
|
||||
|
@ -250,10 +252,10 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
// check types for font, fill_color, outline_color
|
||||
|
||||
//std::cout << PyUnicode_AsUTF8(PyObject_Repr(font)) << std::endl;
|
||||
if (font != NULL && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
|
||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance");
|
||||
if (font != NULL && font != Py_None && !PyObject_IsInstance(font, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Font")/*(PyObject*)&PyFontType)*/)){
|
||||
PyErr_SetString(PyExc_TypeError, "font must be a mcrfpy.Font instance or None");
|
||||
return -1;
|
||||
} else if (font != NULL)
|
||||
} else if (font != NULL && font != Py_None)
|
||||
{
|
||||
auto font_obj = (PyFontObject*)font;
|
||||
self->data->text.setFont(font_obj->data->font);
|
||||
|
@ -261,8 +263,16 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
Py_INCREF(font);
|
||||
} else
|
||||
{
|
||||
// default font
|
||||
//self->data->text.setFont(Resources::game->getFont());
|
||||
// Use default font when None or not provided
|
||||
if (McRFPy_API::default_font) {
|
||||
self->data->text.setFont(McRFPy_API::default_font->font);
|
||||
// Store reference to default font
|
||||
PyObject* default_font_obj = PyObject_GetAttrString(McRFPy_API::mcrf_module, "default_font");
|
||||
if (default_font_obj) {
|
||||
self->font = default_font_obj;
|
||||
// Don't need to DECREF since we're storing it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self->data->text.setString((std::string)text);
|
||||
|
@ -294,3 +304,172 @@ int UICaption::init(PyUICaptionObject* self, PyObject* args, PyObject* kwds)
|
|||
return 0;
|
||||
}
|
||||
|
||||
// Property system implementation for animations
|
||||
bool UICaption::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
text.setPosition(sf::Vector2f(value, text.getPosition().y));
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
text.setPosition(sf::Vector2f(text.getPosition().x, value));
|
||||
return true;
|
||||
}
|
||||
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
|
||||
text.setCharacterSize(static_cast<unsigned int>(value));
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline") {
|
||||
text.setOutlineThickness(value);
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.r") {
|
||||
auto color = text.getFillColor();
|
||||
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.g") {
|
||||
auto color = text.getFillColor();
|
||||
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.b") {
|
||||
auto color = text.getFillColor();
|
||||
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.a") {
|
||||
auto color = text.getFillColor();
|
||||
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setFillColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.r") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.r = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.g") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.g = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.b") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.b = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.a") {
|
||||
auto color = text.getOutlineColor();
|
||||
color.a = static_cast<sf::Uint8>(std::clamp(value, 0.0f, 255.0f));
|
||||
text.setOutlineColor(color);
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
z_index = static_cast<int>(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICaption::setProperty(const std::string& name, const sf::Color& value) {
|
||||
if (name == "fill_color") {
|
||||
text.setFillColor(value);
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color") {
|
||||
text.setOutlineColor(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICaption::setProperty(const std::string& name, const std::string& value) {
|
||||
if (name == "text") {
|
||||
text.setString(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICaption::getProperty(const std::string& name, float& value) const {
|
||||
if (name == "x") {
|
||||
value = text.getPosition().x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
value = text.getPosition().y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "font_size" || name == "size") { // Support both for backward compatibility
|
||||
value = static_cast<float>(text.getCharacterSize());
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline") {
|
||||
value = text.getOutlineThickness();
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.r") {
|
||||
value = text.getFillColor().r;
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.g") {
|
||||
value = text.getFillColor().g;
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.b") {
|
||||
value = text.getFillColor().b;
|
||||
return true;
|
||||
}
|
||||
else if (name == "fill_color.a") {
|
||||
value = text.getFillColor().a;
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.r") {
|
||||
value = text.getOutlineColor().r;
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.g") {
|
||||
value = text.getOutlineColor().g;
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.b") {
|
||||
value = text.getOutlineColor().b;
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color.a") {
|
||||
value = text.getOutlineColor().a;
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
value = static_cast<float>(z_index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICaption::getProperty(const std::string& name, sf::Color& value) const {
|
||||
if (name == "fill_color") {
|
||||
value = text.getFillColor();
|
||||
return true;
|
||||
}
|
||||
else if (name == "outline_color") {
|
||||
value = text.getOutlineColor();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UICaption::getProperty(const std::string& name, std::string& value) const {
|
||||
if (name == "text") {
|
||||
value = text.getString();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,15 @@ public:
|
|||
PyObjectsEnum derived_type() override final;
|
||||
virtual UIDrawable* click_at(sf::Vector2f point) override final;
|
||||
|
||||
// Property system for animations
|
||||
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 std::string& value) override;
|
||||
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||
bool getProperty(const std::string& name, std::string& value) const override;
|
||||
|
||||
static PyObject* get_float_member(PyUICaptionObject* self, void* closure);
|
||||
static int set_float_member(PyUICaptionObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_vec_member(PyUICaptionObject* self, void* closure);
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include <climits>
|
||||
#include <algorithm>
|
||||
|
||||
using namespace mcrfpydef;
|
||||
|
||||
|
@ -148,15 +150,394 @@ PyObject* UICollection::getitem(PyUICollectionObject* self, Py_ssize_t index) {
|
|||
|
||||
}
|
||||
|
||||
int UICollection::setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle negative indexing
|
||||
while (index < 0) index += self->data->size();
|
||||
|
||||
// Bounds check
|
||||
if (index >= self->data->size()) {
|
||||
PyErr_SetString(PyExc_IndexError, "UICollection assignment index out of range");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Handle deletion
|
||||
if (value == NULL) {
|
||||
self->data->erase(self->data->begin() + index);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "UICollection can only contain Frame, Caption, Sprite, and Grid objects");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> new_drawable = nullptr;
|
||||
int old_z_index = (*vec)[index]->z_index; // Preserve the z_index
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)value;
|
||||
new_drawable = frame->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)value;
|
||||
new_drawable = caption->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
|
||||
new_drawable = sprite->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)value;
|
||||
new_drawable = grid->data;
|
||||
}
|
||||
|
||||
if (!new_drawable) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Preserve the z_index of the replaced element
|
||||
new_drawable->z_index = old_z_index;
|
||||
|
||||
// Replace the element
|
||||
(*vec)[index] = new_drawable;
|
||||
|
||||
// Mark scene as needing resort after replacing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int UICollection::contains(PyUICollectionObject* self, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
// Not a valid type, so it can't be in the collection
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)value;
|
||||
search_drawable = frame->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)value;
|
||||
search_drawable = caption->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)value;
|
||||
search_drawable = sprite->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)value;
|
||||
search_drawable = grid->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Search for the object by comparing C++ pointers
|
||||
for (const auto& drawable : *vec) {
|
||||
if (drawable.get() == search_drawable.get()) {
|
||||
return 1; // Found
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // Not found
|
||||
}
|
||||
|
||||
PyObject* UICollection::concat(PyUICollectionObject* 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 UICollection");
|
||||
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
|
||||
for (Py_ssize_t i = 0; i < self_len; i++) {
|
||||
PyObject* item = convertDrawableToPython((*self->data)[i]);
|
||||
if (!item) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result_list, i, item); // Steals reference
|
||||
}
|
||||
|
||||
// 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* UICollection::inplace_concat(PyUICollectionObject* self, PyObject* other) {
|
||||
if (!PySequence_Check(other)) {
|
||||
PyErr_SetString(PyExc_TypeError, "can only concatenate sequence to UICollection");
|
||||
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, "Frame")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
Py_DECREF(item);
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"UICollection can only contain Frame, Caption, Sprite, and Grid 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 z_index assignment
|
||||
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;
|
||||
}
|
||||
|
||||
PyObject* UICollection::subscript(PyUICollectionObject* self, PyObject* key) {
|
||||
if (PyLong_Check(key)) {
|
||||
// Single index - delegate to sq_item
|
||||
Py_ssize_t index = PyLong_AsSsize_t(key);
|
||||
if (index == -1 && PyErr_Occurred()) {
|
||||
return NULL;
|
||||
}
|
||||
return getitem(self, index);
|
||||
} else if (PySlice_Check(key)) {
|
||||
// Handle slice
|
||||
Py_ssize_t start, stop, step, slicelength;
|
||||
|
||||
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* result_list = PyList_New(slicelength);
|
||||
if (!result_list) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
||||
PyObject* item = convertDrawableToPython((*self->data)[cur]);
|
||||
if (!item) {
|
||||
Py_DECREF(result_list);
|
||||
return NULL;
|
||||
}
|
||||
PyList_SET_ITEM(result_list, i, item); // Steals reference
|
||||
}
|
||||
|
||||
return result_list;
|
||||
} else {
|
||||
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
|
||||
Py_TYPE(key)->tp_name);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
int UICollection::ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value) {
|
||||
if (PyLong_Check(key)) {
|
||||
// Single index - delegate to sq_ass_item
|
||||
Py_ssize_t index = PyLong_AsSsize_t(key);
|
||||
if (index == -1 && PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
return setitem(self, index, value);
|
||||
} else if (PySlice_Check(key)) {
|
||||
// Handle slice assignment/deletion
|
||||
Py_ssize_t start, stop, step, slicelength;
|
||||
|
||||
if (PySlice_GetIndicesEx(key, self->data->size(), &start, &stop, &step, &slicelength) < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (value == NULL) {
|
||||
// Deletion
|
||||
if (step != 1) {
|
||||
// For non-contiguous slices, delete from highest to lowest to maintain indices
|
||||
std::vector<Py_ssize_t> indices;
|
||||
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
||||
indices.push_back(cur);
|
||||
}
|
||||
// Sort in descending order and delete
|
||||
std::sort(indices.begin(), indices.end(), std::greater<Py_ssize_t>());
|
||||
for (Py_ssize_t idx : indices) {
|
||||
self->data->erase(self->data->begin() + idx);
|
||||
}
|
||||
} else {
|
||||
// Contiguous slice - can delete in one go
|
||||
self->data->erase(self->data->begin() + start, self->data->begin() + stop);
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after slice deletion
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
return 0;
|
||||
} else {
|
||||
// Assignment
|
||||
if (!PySequence_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "can only assign sequence to slice");
|
||||
return -1;
|
||||
}
|
||||
|
||||
Py_ssize_t value_len = PySequence_Length(value);
|
||||
if (value_len == -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate all items first
|
||||
std::vector<std::shared_ptr<UIDrawable>> new_items;
|
||||
for (Py_ssize_t i = 0; i < value_len; i++) {
|
||||
PyObject* item = PySequence_GetItem(value, i);
|
||||
if (!item) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Type check and extract C++ object
|
||||
std::shared_ptr<UIDrawable> drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
drawable = ((PyUIFrameObject*)item)->data;
|
||||
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
drawable = ((PyUICaptionObject*)item)->data;
|
||||
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
drawable = ((PyUISpriteObject*)item)->data;
|
||||
} else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
drawable = ((PyUIGridObject*)item)->data;
|
||||
} else {
|
||||
Py_DECREF(item);
|
||||
PyErr_Format(PyExc_TypeError,
|
||||
"UICollection can only contain Frame, Caption, Sprite, and Grid objects; "
|
||||
"got %s at index %zd", Py_TYPE(item)->tp_name, i);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Py_DECREF(item);
|
||||
new_items.push_back(drawable);
|
||||
}
|
||||
|
||||
// Now perform the assignment
|
||||
if (step == 1) {
|
||||
// Contiguous slice
|
||||
if (slicelength != value_len) {
|
||||
// Need to resize
|
||||
auto it_start = self->data->begin() + start;
|
||||
auto it_stop = self->data->begin() + stop;
|
||||
self->data->erase(it_start, it_stop);
|
||||
self->data->insert(self->data->begin() + start, new_items.begin(), new_items.end());
|
||||
} else {
|
||||
// Same size, just replace
|
||||
for (Py_ssize_t i = 0; i < slicelength; i++) {
|
||||
// Preserve z_index
|
||||
new_items[i]->z_index = (*self->data)[start + i]->z_index;
|
||||
(*self->data)[start + i] = new_items[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Extended slice
|
||||
if (slicelength != value_len) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"attempt to assign sequence of size %zd to extended slice of size %zd",
|
||||
value_len, slicelength);
|
||||
return -1;
|
||||
}
|
||||
for (Py_ssize_t i = 0, cur = start; i < slicelength; i++, cur += step) {
|
||||
// Preserve z_index
|
||||
new_items[i]->z_index = (*self->data)[cur]->z_index;
|
||||
(*self->data)[cur] = new_items[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after slice assignment
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
PyErr_Format(PyExc_TypeError, "UICollection indices must be integers or slices, not %.200s",
|
||||
Py_TYPE(key)->tp_name);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
PyMappingMethods UICollection::mpmethods = {
|
||||
.mp_length = (lenfunc)UICollection::len,
|
||||
.mp_subscript = (binaryfunc)UICollection::subscript,
|
||||
.mp_ass_subscript = (objobjargproc)UICollection::ass_subscript
|
||||
};
|
||||
|
||||
PySequenceMethods UICollection::sqmethods = {
|
||||
.sq_length = (lenfunc)UICollection::len,
|
||||
.sq_concat = (binaryfunc)UICollection::concat,
|
||||
.sq_repeat = NULL,
|
||||
.sq_item = (ssizeargfunc)UICollection::getitem,
|
||||
//.sq_item_by_index = PyUICollection_getitem
|
||||
//.sq_slice - return a subset of the iterable
|
||||
//.sq_ass_item - called when `o[x] = y` is executed (x is any object type)
|
||||
//.sq_ass_slice - cool; no thanks, for now
|
||||
//.sq_contains - called when `x in o` is executed
|
||||
//.sq_ass_item_by_index - called when `o[x] = y` is executed (x is explictly an integer)
|
||||
.was_sq_slice = NULL,
|
||||
.sq_ass_item = (ssizeobjargproc)UICollection::setitem,
|
||||
.was_sq_ass_slice = NULL,
|
||||
.sq_contains = (objobjproc)UICollection::contains,
|
||||
.sq_inplace_concat = (binaryfunc)UICollection::inplace_concat,
|
||||
.sq_inplace_repeat = NULL
|
||||
};
|
||||
|
||||
/* Idiomatic way to fetch complete types from the API rather than referencing their PyTypeObject struct
|
||||
|
@ -173,6 +554,12 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
// if not UIDrawable subclass, reject it
|
||||
// self->data->push_back( c++ object inside o );
|
||||
|
||||
// Ensure module is initialized
|
||||
if (!McRFPy_API::mcrf_module) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// this would be a great use case for .tp_base
|
||||
if (!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
|
@ -184,27 +571,128 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
// Calculate z_index for the new element
|
||||
int new_z_index = 0;
|
||||
if (!self->data->empty()) {
|
||||
// Get the z_index of the last element and add 10
|
||||
int last_z = self->data->back()->z_index;
|
||||
if (last_z <= INT_MAX - 10) {
|
||||
new_z_index = last_z + 10;
|
||||
} else {
|
||||
new_z_index = INT_MAX;
|
||||
}
|
||||
}
|
||||
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")))
|
||||
{
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)o;
|
||||
frame->data->z_index = new_z_index;
|
||||
self->data->push_back(frame->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")))
|
||||
{
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)o;
|
||||
caption->data->z_index = new_z_index;
|
||||
self->data->push_back(caption->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")))
|
||||
{
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)o;
|
||||
sprite->data->z_index = new_z_index;
|
||||
self->data->push_back(sprite->data);
|
||||
}
|
||||
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
||||
{
|
||||
PyUIGridObject* grid = (PyUIGridObject*)o;
|
||||
grid->data->z_index = new_z_index;
|
||||
self->data->push_back(grid->data);
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after adding element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
||||
{
|
||||
// Accept any iterable of UIDrawable objects
|
||||
PyObject* iterator = PyObject_GetIter(iterable);
|
||||
if (iterator == NULL) {
|
||||
PyErr_SetString(PyExc_TypeError, "UICollection.extend requires an iterable");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Ensure module is initialized
|
||||
if (!McRFPy_API::mcrf_module) {
|
||||
Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get current highest z_index
|
||||
int current_z_index = 0;
|
||||
if (!self->data->empty()) {
|
||||
current_z_index = self->data->back()->z_index;
|
||||
}
|
||||
|
||||
PyObject* item;
|
||||
while ((item = PyIter_Next(iterator)) != NULL) {
|
||||
// Check if item is a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")))
|
||||
{
|
||||
Py_DECREF(item);
|
||||
Py_DECREF(iterator);
|
||||
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Increment z_index for each new element
|
||||
if (current_z_index <= INT_MAX - 10) {
|
||||
current_z_index += 10;
|
||||
} else {
|
||||
current_z_index = INT_MAX;
|
||||
}
|
||||
|
||||
// Add the item based on its type
|
||||
if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
PyUIFrameObject* frame = (PyUIFrameObject*)item;
|
||||
frame->data->z_index = current_z_index;
|
||||
self->data->push_back(frame->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
PyUICaptionObject* caption = (PyUICaptionObject*)item;
|
||||
caption->data->z_index = current_z_index;
|
||||
self->data->push_back(caption->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
PyUISpriteObject* sprite = (PyUISpriteObject*)item;
|
||||
sprite->data->z_index = current_z_index;
|
||||
self->data->push_back(sprite->data);
|
||||
}
|
||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyUIGridObject* grid = (PyUIGridObject*)item;
|
||||
grid->data->z_index = current_z_index;
|
||||
self->data->push_back(grid->data);
|
||||
}
|
||||
|
||||
Py_DECREF(item);
|
||||
}
|
||||
|
||||
Py_DECREF(iterator);
|
||||
|
||||
// Check if iteration ended due to an error
|
||||
if (PyErr_Occurred()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Mark scene as needing resort after adding elements
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
@ -217,27 +705,121 @@ PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o)
|
|||
return NULL;
|
||||
}
|
||||
long index = PyLong_AsLong(o);
|
||||
|
||||
// Handle negative indexing
|
||||
while (index < 0) index += self->data->size();
|
||||
|
||||
if (index >= self->data->size())
|
||||
{
|
||||
PyErr_SetString(PyExc_ValueError, "Index out of range");
|
||||
return NULL;
|
||||
}
|
||||
else if (index < 0)
|
||||
{
|
||||
PyErr_SetString(PyExc_NotImplementedError, "reverse indexing is not implemented.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// release the shared pointer at self->data[index];
|
||||
self->data->erase(self->data->begin() + index);
|
||||
|
||||
// Mark scene as needing resort after removing element
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
Py_INCREF(Py_None);
|
||||
return Py_None;
|
||||
}
|
||||
|
||||
PyObject* UICollection::index_method(PyUICollectionObject* self, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "UICollection.index requires a Frame, Caption, Sprite, or Grid object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
search_drawable = ((PyUIFrameObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
search_drawable = ((PyUICaptionObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
search_drawable = ((PyUISpriteObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
search_drawable = ((PyUIGridObject*)value)->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Failed to extract C++ object from Python object");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Search for the object
|
||||
for (size_t i = 0; i < vec->size(); i++) {
|
||||
if ((*vec)[i].get() == search_drawable.get()) {
|
||||
return PyLong_FromSsize_t(i);
|
||||
}
|
||||
}
|
||||
|
||||
PyErr_SetString(PyExc_ValueError, "value not in UICollection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) {
|
||||
auto vec = self->data.get();
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "the collection store returned a null pointer");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Type checking - must be a UIDrawable subclass
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) &&
|
||||
!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
// Not a valid type, so count is 0
|
||||
return PyLong_FromLong(0);
|
||||
}
|
||||
|
||||
// Get the C++ object from the Python object
|
||||
std::shared_ptr<UIDrawable> search_drawable = nullptr;
|
||||
|
||||
if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) {
|
||||
search_drawable = ((PyUIFrameObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) {
|
||||
search_drawable = ((PyUICaptionObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) {
|
||||
search_drawable = ((PyUISpriteObject*)value)->data;
|
||||
} else if (PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
search_drawable = ((PyUIGridObject*)value)->data;
|
||||
}
|
||||
|
||||
if (!search_drawable) {
|
||||
return PyLong_FromLong(0);
|
||||
}
|
||||
|
||||
// Count occurrences
|
||||
Py_ssize_t count = 0;
|
||||
for (const auto& drawable : *vec) {
|
||||
if (drawable.get() == search_drawable.get()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return PyLong_FromSsize_t(count);
|
||||
}
|
||||
|
||||
PyMethodDef UICollection::methods[] = {
|
||||
{"append", (PyCFunction)UICollection::append, METH_O},
|
||||
//{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO
|
||||
{"extend", (PyCFunction)UICollection::extend, METH_O},
|
||||
{"remove", (PyCFunction)UICollection::remove, METH_O},
|
||||
{"index", (PyCFunction)UICollection::index_method, METH_O},
|
||||
{"count", (PyCFunction)UICollection::count, METH_O},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
@ -246,7 +828,47 @@ PyObject* UICollection::repr(PyUICollectionObject* self)
|
|||
std::ostringstream ss;
|
||||
if (!self->data) ss << "<UICollection (invalid internal object)>";
|
||||
else {
|
||||
ss << "<UICollection (" << self->data->size() << " child objects)>";
|
||||
ss << "<UICollection (" << self->data->size() << " objects: ";
|
||||
|
||||
// Count each type
|
||||
int frame_count = 0, caption_count = 0, sprite_count = 0, grid_count = 0, other_count = 0;
|
||||
for (auto& item : *self->data) {
|
||||
switch(item->derived_type()) {
|
||||
case PyObjectsEnum::UIFRAME: frame_count++; break;
|
||||
case PyObjectsEnum::UICAPTION: caption_count++; break;
|
||||
case PyObjectsEnum::UISPRITE: sprite_count++; break;
|
||||
case PyObjectsEnum::UIGRID: grid_count++; break;
|
||||
default: other_count++; break;
|
||||
}
|
||||
}
|
||||
|
||||
// Build type summary
|
||||
bool first = true;
|
||||
if (frame_count > 0) {
|
||||
ss << frame_count << " Frame" << (frame_count > 1 ? "s" : "");
|
||||
first = false;
|
||||
}
|
||||
if (caption_count > 0) {
|
||||
if (!first) ss << ", ";
|
||||
ss << caption_count << " Caption" << (caption_count > 1 ? "s" : "");
|
||||
first = false;
|
||||
}
|
||||
if (sprite_count > 0) {
|
||||
if (!first) ss << ", ";
|
||||
ss << sprite_count << " Sprite" << (sprite_count > 1 ? "s" : "");
|
||||
first = false;
|
||||
}
|
||||
if (grid_count > 0) {
|
||||
if (!first) ss << ", ";
|
||||
ss << grid_count << " Grid" << (grid_count > 1 ? "s" : "");
|
||||
first = false;
|
||||
}
|
||||
if (other_count > 0) {
|
||||
if (!first) ss << ", ";
|
||||
ss << other_count << " UIDrawable" << (other_count > 1 ? "s" : "");
|
||||
}
|
||||
|
||||
ss << ")>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
|
|
|
@ -19,9 +19,19 @@ class UICollection
|
|||
public:
|
||||
static Py_ssize_t len(PyUICollectionObject* self);
|
||||
static PyObject* getitem(PyUICollectionObject* self, Py_ssize_t index);
|
||||
static int setitem(PyUICollectionObject* self, Py_ssize_t index, PyObject* value);
|
||||
static int contains(PyUICollectionObject* self, PyObject* value);
|
||||
static PyObject* concat(PyUICollectionObject* self, PyObject* other);
|
||||
static PyObject* inplace_concat(PyUICollectionObject* self, PyObject* other);
|
||||
static PySequenceMethods sqmethods;
|
||||
static PyMappingMethods mpmethods;
|
||||
static PyObject* subscript(PyUICollectionObject* self, PyObject* key);
|
||||
static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value);
|
||||
static PyObject* append(PyUICollectionObject* self, PyObject* o);
|
||||
static PyObject* extend(PyUICollectionObject* self, PyObject* iterable);
|
||||
static PyObject* remove(PyUICollectionObject* self, PyObject* o);
|
||||
static PyObject* index_method(PyUICollectionObject* self, PyObject* value);
|
||||
static PyObject* count(PyUICollectionObject* self, PyObject* value);
|
||||
static PyMethodDef methods[];
|
||||
static PyObject* repr(PyUICollectionObject* self);
|
||||
static int init(PyUICollectionObject* self, PyObject* args, PyObject* kwds);
|
||||
|
@ -71,6 +81,7 @@ namespace mcrfpydef {
|
|||
},
|
||||
.tp_repr = (reprfunc)UICollection::repr,
|
||||
.tp_as_sequence = &UICollection::sqmethods,
|
||||
.tp_as_mapping = &UICollection::mpmethods,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Iterable, indexable collection of UI objects"),
|
||||
.tp_iter = (getiterfunc)UICollection::iter,
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
#include "UISprite.h"
|
||||
#include "UIGrid.h"
|
||||
#include "GameEngine.h"
|
||||
#include "McRFPy_API.h"
|
||||
|
||||
UIDrawable::UIDrawable() { click_callable = NULL; }
|
||||
|
||||
|
@ -14,7 +15,7 @@ void UIDrawable::click_unregister()
|
|||
|
||||
void UIDrawable::render()
|
||||
{
|
||||
render(sf::Vector2f(), Resources::game->getWindow());
|
||||
render(sf::Vector2f(), Resources::game->getRenderTarget());
|
||||
}
|
||||
|
||||
PyObject* UIDrawable::get_click(PyObject* self, void* closure) {
|
||||
|
@ -80,3 +81,85 @@ void UIDrawable::click_register(PyObject* callable)
|
|||
{
|
||||
click_callable = std::make_unique<PyClickCallable>(callable);
|
||||
}
|
||||
|
||||
PyObject* UIDrawable::get_int(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 PyLong_FromLong(drawable->z_index);
|
||||
}
|
||||
|
||||
int UIDrawable::set_int(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 (!PyLong_Check(value)) {
|
||||
PyErr_SetString(PyExc_TypeError, "z_index must be an integer");
|
||||
return -1;
|
||||
}
|
||||
|
||||
long z = PyLong_AsLong(value);
|
||||
if (z == -1 && PyErr_Occurred()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Clamp to int range
|
||||
if (z < INT_MIN) z = INT_MIN;
|
||||
if (z > INT_MAX) z = INT_MAX;
|
||||
|
||||
int old_z_index = drawable->z_index;
|
||||
drawable->z_index = static_cast<int>(z);
|
||||
|
||||
// Notify of z_index change
|
||||
if (old_z_index != drawable->z_index) {
|
||||
drawable->notifyZIndexChanged();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void UIDrawable::notifyZIndexChanged() {
|
||||
// Mark the current scene as needing sort
|
||||
// This works for elements in the scene's ui_elements collection
|
||||
McRFPy_API::markSceneNeedsSort();
|
||||
|
||||
// TODO: In the future, we could add parent tracking to handle Frame children
|
||||
// For now, Frame children will need manual sorting or collection modification
|
||||
// to trigger a resort
|
||||
}
|
||||
|
|
|
@ -42,6 +42,27 @@ public:
|
|||
|
||||
static PyObject* get_click(PyObject* self, void* closure);
|
||||
static int set_click(PyObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_int(PyObject* self, void* closure);
|
||||
static int set_int(PyObject* self, PyObject* value, void* closure);
|
||||
|
||||
// Z-order for rendering (lower values rendered first, higher values on top)
|
||||
int z_index = 0;
|
||||
|
||||
// Notification for z_index changes
|
||||
void notifyZIndexChanged();
|
||||
|
||||
// Animation support
|
||||
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, const sf::Color& value) { return false; }
|
||||
virtual bool setProperty(const std::string& name, const sf::Vector2f& value) { return false; }
|
||||
virtual bool setProperty(const std::string& name, const std::string& value) { return false; }
|
||||
|
||||
virtual bool getProperty(const std::string& name, float& value) const { return false; }
|
||||
virtual bool getProperty(const std::string& name, int& 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, std::string& value) const { return false; }
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
|
167
src/UIEntity.cpp
|
@ -2,6 +2,8 @@
|
|||
#include "UIGrid.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyObjectUtils.h"
|
||||
#include "PyVector.h"
|
||||
|
||||
|
||||
UIEntity::UIEntity() {} // this will not work lol. TODO remove default constructor by finding the shared pointer inits that use it
|
||||
|
||||
|
@ -34,6 +36,33 @@ PyObject* UIEntity::at(PyUIEntityObject* self, PyObject* o) {
|
|||
|
||||
}
|
||||
|
||||
PyObject* UIEntity::index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored)) {
|
||||
// Check if entity has an associated grid
|
||||
if (!self->data || !self->data->grid) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Entity is not associated with a grid");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Get the grid's entity collection
|
||||
auto entities = self->data->grid->entities;
|
||||
if (!entities) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Grid has no entity collection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Find this entity in the collection
|
||||
int index = 0;
|
||||
for (auto it = entities->begin(); it != entities->end(); ++it, ++index) {
|
||||
if (it->get() == self->data.get()) {
|
||||
return PyLong_FromLong(index);
|
||||
}
|
||||
}
|
||||
|
||||
// Entity not found in its grid's collection
|
||||
PyErr_SetString(PyExc_ValueError, "Entity not found in its grid's entity collection");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
||||
//static const char* keywords[] = { "x", "y", "texture", "sprite_index", "grid", nullptr };
|
||||
//float x = 0.0f, y = 0.0f, scale = 1.0f;
|
||||
|
@ -46,7 +75,7 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
|
||||
//if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffOi|O",
|
||||
// const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &grid))
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi|O",
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OiO",
|
||||
const_cast<char**>(keywords), &pos, &texture, &sprite_index, &grid))
|
||||
{
|
||||
return -1;
|
||||
|
@ -61,33 +90,41 @@ int UIEntity::init(PyUIEntityObject* self, PyObject* args, PyObject* kwds) {
|
|||
|
||||
// check types for texture
|
||||
//
|
||||
// Set Texture
|
||||
// Set Texture - allow None or use default
|
||||
//
|
||||
if (texture != NULL && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
} /*else if (texture != NULL) // this section needs to go; texture isn't optional and isn't managed by the UI objects anymore
|
||||
{
|
||||
self->texture = texture;
|
||||
Py_INCREF(texture);
|
||||
} else
|
||||
{
|
||||
// default tex?
|
||||
}*/
|
||||
} else if (texture != NULL && texture != Py_None) {
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
texture_ptr = pytexture->data;
|
||||
} else {
|
||||
// Use default texture when None or not provided
|
||||
texture_ptr = McRFPy_API::default_texture;
|
||||
}
|
||||
|
||||
if (!texture_ptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (grid != NULL && !PyObject_IsInstance(grid, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "grid must be a mcrfpy.Grid instance");
|
||||
return -1;
|
||||
}
|
||||
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
if (grid == NULL)
|
||||
self->data = std::make_shared<UIEntity>();
|
||||
else
|
||||
self->data = std::make_shared<UIEntity>(*((PyUIGridObject*)grid)->data);
|
||||
|
||||
// Store reference to Python object
|
||||
self->data->self = (PyObject*)self;
|
||||
Py_INCREF(self);
|
||||
|
||||
// TODO - PyTextureObjects and IndexTextures are a little bit of a mess with shared/unshared pointers
|
||||
self->data->sprite = UISprite(pytexture->data, 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;
|
||||
if (grid != NULL) {
|
||||
PyUIGridObject* pygrid = (PyUIGridObject*)grid;
|
||||
|
@ -104,28 +141,40 @@ PyObject* UIEntity::get_spritenumber(PyUIEntityObject* self, void* closure) {
|
|||
return PyLong_FromDouble(self->data->sprite.getSpriteIndex());
|
||||
}
|
||||
|
||||
PyObject* sfVector2f_to_PyObject(sf::Vector2f vector) {
|
||||
return Py_BuildValue("(ff)", vector.x, vector.y);
|
||||
PyObject* sfVector2f_to_PyObject(sf::Vector2f vec) {
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
if (obj) {
|
||||
obj->data = vec;
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
PyObject* sfVector2i_to_PyObject(sf::Vector2i vector) {
|
||||
return Py_BuildValue("(ii)", vector.x, vector.y);
|
||||
PyObject* sfVector2i_to_PyObject(sf::Vector2i vec) {
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
if (obj) {
|
||||
obj->data = sf::Vector2f(static_cast<float>(vec.x), static_cast<float>(vec.y));
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
sf::Vector2f PyObject_to_sfVector2f(PyObject* obj) {
|
||||
float x, y;
|
||||
if (!PyArg_ParseTuple(obj, "ff", &x, &y)) {
|
||||
return sf::Vector2f(); // TODO / reconsider this default: Return default vector on parse error
|
||||
PyVectorObject* vec = PyVector::from_arg(obj);
|
||||
if (!vec) {
|
||||
// PyVector::from_arg already set the error
|
||||
return sf::Vector2f(0, 0);
|
||||
}
|
||||
return sf::Vector2f(x, y);
|
||||
return vec->data;
|
||||
}
|
||||
|
||||
sf::Vector2i PyObject_to_sfVector2i(PyObject* obj) {
|
||||
int x, y;
|
||||
if (!PyArg_ParseTuple(obj, "ii", &x, &y)) {
|
||||
return sf::Vector2i(); // TODO / reconsider this default: Return default vector on parse error
|
||||
PyVectorObject* vec = PyVector::from_arg(obj);
|
||||
if (!vec) {
|
||||
// PyVector::from_arg already set the error
|
||||
return sf::Vector2i(0, 0);
|
||||
}
|
||||
return sf::Vector2i(x, y);
|
||||
return sf::Vector2i(static_cast<int>(vec->data.x), static_cast<int>(vec->data.y));
|
||||
}
|
||||
|
||||
// TODO - deprecate / remove this helper
|
||||
|
@ -161,9 +210,17 @@ PyObject* UIEntity::get_position(PyUIEntityObject* self, void* closure) {
|
|||
|
||||
int UIEntity::set_position(PyUIEntityObject* self, PyObject* value, void* closure) {
|
||||
if (reinterpret_cast<long>(closure) == 0) {
|
||||
self->data->position = PyObject_to_sfVector2f(value);
|
||||
sf::Vector2f vec = PyObject_to_sfVector2f(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1; // Error already set by PyObject_to_sfVector2f
|
||||
}
|
||||
self->data->position = vec;
|
||||
} else {
|
||||
self->data->collision_pos = PyObject_to_sfVector2i(value);
|
||||
sf::Vector2i vec = PyObject_to_sfVector2i(value);
|
||||
if (PyErr_Occurred()) {
|
||||
return -1; // Error already set by PyObject_to_sfVector2i
|
||||
}
|
||||
self->data->collision_pos = vec;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
@ -189,6 +246,7 @@ int UIEntity::set_spritenumber(PyUIEntityObject* self, PyObject* value, void* cl
|
|||
|
||||
PyMethodDef UIEntity::methods[] = {
|
||||
{"at", (PyCFunction)UIEntity::at, METH_O},
|
||||
{"index", (PyCFunction)UIEntity::index, METH_NOARGS, "Return the index of this entity in its grid's entity collection"},
|
||||
{NULL, NULL, 0, NULL}
|
||||
};
|
||||
|
||||
|
@ -196,7 +254,8 @@ PyGetSetDef UIEntity::getsetters[] = {
|
|||
{"draw_pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (graphically)", (void*)0},
|
||||
{"pos", (getter)UIEntity::get_position, (setter)UIEntity::set_position, "Entity position (integer grid coordinates)", (void*)1},
|
||||
{"gridstate", (getter)UIEntity::get_gridstate, NULL, "Grid point states for the entity", NULL},
|
||||
{"sprite_number", (getter)UIEntity::get_spritenumber, (setter)UIEntity::set_spritenumber, "Sprite number (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},
|
||||
{NULL} /* Sentinel */
|
||||
};
|
||||
|
||||
|
@ -205,9 +264,57 @@ PyObject* UIEntity::repr(PyUIEntityObject* self) {
|
|||
if (!self->data) ss << "<Entity (invalid internal object)>";
|
||||
else {
|
||||
auto ent = self->data;
|
||||
ss << "<Entity (x=" << self->data->position.x << ", y=" << self->data->position.y << ", sprite_number=" << self->data->sprite.getSpriteIndex() <<
|
||||
ss << "<Entity (x=" << self->data->position.x << ", y=" << self->data->position.y << ", sprite_index=" << self->data->sprite.getSpriteIndex() <<
|
||||
")>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
}
|
||||
|
||||
// Property system implementation for animations
|
||||
bool UIEntity::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
position.x = value;
|
||||
collision_pos.x = static_cast<int>(value);
|
||||
// Update sprite position based on grid position
|
||||
// Note: This is a simplified version - actual grid-to-pixel conversion depends on grid properties
|
||||
sprite.setPosition(sf::Vector2f(position.x, position.y));
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
position.y = value;
|
||||
collision_pos.y = static_cast<int>(value);
|
||||
// Update sprite position based on grid position
|
||||
sprite.setPosition(sf::Vector2f(position.x, position.y));
|
||||
return true;
|
||||
}
|
||||
else if (name == "sprite_scale") {
|
||||
sprite.setScale(sf::Vector2f(value, value));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIEntity::setProperty(const std::string& name, int value) {
|
||||
if (name == "sprite_index" || name == "sprite_number") {
|
||||
sprite.setSpriteIndex(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIEntity::getProperty(const std::string& name, float& value) const {
|
||||
if (name == "x") {
|
||||
value = position.x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
value = position.y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "sprite_scale") {
|
||||
value = sprite.getScale().x; // Assuming uniform scale
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ static PyObject* UIGridPointStateVector_to_PyList(const std::vector<UIGridPointS
|
|||
class UIEntity//: public UIDrawable
|
||||
{
|
||||
public:
|
||||
//PyObject* self;
|
||||
PyObject* self = nullptr; // Reference to the Python object (if created from Python)
|
||||
std::shared_ptr<UIGrid> grid;
|
||||
std::vector<UIGridPointState> gridstate;
|
||||
UISprite sprite;
|
||||
|
@ -46,7 +46,13 @@ public:
|
|||
UIEntity();
|
||||
UIEntity(UIGrid&);
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value);
|
||||
bool setProperty(const std::string& name, int value);
|
||||
bool getProperty(const std::string& name, float& value) const;
|
||||
|
||||
static PyObject* at(PyUIEntityObject* self, PyObject* o);
|
||||
static PyObject* index(PyUIEntityObject* self, PyObject* Py_UNUSED(ignored));
|
||||
static int init(PyUIEntityObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
static PyObject* get_position(PyUIEntityObject* self, void* closure);
|
||||
|
|
205
src/UIFrame.cpp
|
@ -1,6 +1,7 @@
|
|||
#include "UIFrame.h"
|
||||
#include "UICollection.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyVector.h"
|
||||
|
||||
UIDrawable* UIFrame::click_at(sf::Vector2f point)
|
||||
{
|
||||
|
@ -51,6 +52,15 @@ void UIFrame::render(sf::Vector2f offset, sf::RenderTarget& target)
|
|||
target.draw(box);
|
||||
box.move(-offset);
|
||||
|
||||
// Sort children by z_index if needed
|
||||
if (children_need_sort && !children->empty()) {
|
||||
std::sort(children->begin(), children->end(),
|
||||
[](const std::shared_ptr<UIDrawable>& a, const std::shared_ptr<UIDrawable>& b) {
|
||||
return a->z_index < b->z_index;
|
||||
});
|
||||
children_need_sort = false;
|
||||
}
|
||||
|
||||
for (auto drawable : *children) {
|
||||
drawable->render(offset + box.getPosition(), target);
|
||||
}
|
||||
|
@ -205,6 +215,28 @@ int UIFrame::set_color_member(PyUIFrameObject* self, PyObject* value, void* clos
|
|||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UIFrame::get_pos(PyUIFrameObject* self, void* closure)
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
if (obj) {
|
||||
auto pos = self->data->box.getPosition();
|
||||
obj->data = sf::Vector2f(pos.x, pos.y);
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
int UIFrame::set_pos(PyUIFrameObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
PyVectorObject* vec = PyVector::from_arg(value);
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector");
|
||||
return -1;
|
||||
}
|
||||
self->data->box.setPosition(vec->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyGetSetDef UIFrame::getsetters[] = {
|
||||
{"x", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "X coordinate of top-left corner", (void*)0},
|
||||
{"y", (getter)UIFrame::get_float_member, (setter)UIFrame::set_float_member, "Y coordinate of top-left corner", (void*)1},
|
||||
|
@ -215,6 +247,8 @@ PyGetSetDef UIFrame::getsetters[] = {
|
|||
{"outline_color", (getter)UIFrame::get_color_member, (setter)UIFrame::set_color_member, "Outline color of the rectangle", (void*)1},
|
||||
{"children", (getter)UIFrame::get_children, NULL, "UICollection of objects on top of this one", NULL},
|
||||
{"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UIFRAME},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UIFRAME},
|
||||
{"pos", (getter)UIFrame::get_pos, (setter)UIFrame::set_pos, "Position as a Vector", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
@ -246,9 +280,29 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
PyObject* fill_color = 0;
|
||||
PyObject* outline_color = 0;
|
||||
|
||||
// First try to parse as (x, y, w, h, ...)
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "ffff|OOf", const_cast<char**>(keywords), &x, &y, &w, &h, &fill_color, &outline_color, &outline))
|
||||
{
|
||||
return -1;
|
||||
PyErr_Clear(); // Clear the error
|
||||
|
||||
// Try to parse as ((x,y), w, h, ...) or (Vector, w, h, ...)
|
||||
PyObject* pos_obj = nullptr;
|
||||
const char* alt_keywords[] = { "pos", "w", "h", "fill_color", "outline_color", "outline", nullptr };
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "Off|OOf", const_cast<char**>(alt_keywords),
|
||||
&pos_obj, &w, &h, &fill_color, &outline_color, &outline))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert position argument to x, y
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
|
||||
return -1;
|
||||
}
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
}
|
||||
|
||||
self->data->box.setPosition(sf::Vector2f(x, y));
|
||||
|
@ -264,3 +318,152 @@ int UIFrame::init(PyUIFrameObject* self, PyObject* args, PyObject* kwds)
|
|||
if (err_val) return err_val;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Animation property system implementation
|
||||
bool UIFrame::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
box.setPosition(sf::Vector2f(value, box.getPosition().y));
|
||||
return true;
|
||||
} else if (name == "y") {
|
||||
box.setPosition(sf::Vector2f(box.getPosition().x, value));
|
||||
return true;
|
||||
} else if (name == "w") {
|
||||
box.setSize(sf::Vector2f(value, box.getSize().y));
|
||||
return true;
|
||||
} else if (name == "h") {
|
||||
box.setSize(sf::Vector2f(box.getSize().x, value));
|
||||
return true;
|
||||
} else if (name == "outline") {
|
||||
box.setOutlineThickness(value);
|
||||
return true;
|
||||
} else if (name == "fill_color.r") {
|
||||
auto color = box.getFillColor();
|
||||
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setFillColor(color);
|
||||
return true;
|
||||
} else if (name == "fill_color.g") {
|
||||
auto color = box.getFillColor();
|
||||
color.g = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setFillColor(color);
|
||||
return true;
|
||||
} else if (name == "fill_color.b") {
|
||||
auto color = box.getFillColor();
|
||||
color.b = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setFillColor(color);
|
||||
return true;
|
||||
} else if (name == "fill_color.a") {
|
||||
auto color = box.getFillColor();
|
||||
color.a = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setFillColor(color);
|
||||
return true;
|
||||
} else if (name == "outline_color.r") {
|
||||
auto color = box.getOutlineColor();
|
||||
color.r = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setOutlineColor(color);
|
||||
return true;
|
||||
} else if (name == "outline_color.g") {
|
||||
auto color = box.getOutlineColor();
|
||||
color.g = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setOutlineColor(color);
|
||||
return true;
|
||||
} else if (name == "outline_color.b") {
|
||||
auto color = box.getOutlineColor();
|
||||
color.b = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setOutlineColor(color);
|
||||
return true;
|
||||
} else if (name == "outline_color.a") {
|
||||
auto color = box.getOutlineColor();
|
||||
color.a = std::clamp(static_cast<int>(value), 0, 255);
|
||||
box.setOutlineColor(color);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIFrame::setProperty(const std::string& name, const sf::Color& value) {
|
||||
if (name == "fill_color") {
|
||||
box.setFillColor(value);
|
||||
return true;
|
||||
} else if (name == "outline_color") {
|
||||
box.setOutlineColor(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIFrame::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||
if (name == "position") {
|
||||
box.setPosition(value);
|
||||
return true;
|
||||
} else if (name == "size") {
|
||||
box.setSize(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIFrame::getProperty(const std::string& name, float& value) const {
|
||||
if (name == "x") {
|
||||
value = box.getPosition().x;
|
||||
return true;
|
||||
} else if (name == "y") {
|
||||
value = box.getPosition().y;
|
||||
return true;
|
||||
} else if (name == "w") {
|
||||
value = box.getSize().x;
|
||||
return true;
|
||||
} else if (name == "h") {
|
||||
value = box.getSize().y;
|
||||
return true;
|
||||
} else if (name == "outline") {
|
||||
value = box.getOutlineThickness();
|
||||
return true;
|
||||
} else if (name == "fill_color.r") {
|
||||
value = box.getFillColor().r;
|
||||
return true;
|
||||
} else if (name == "fill_color.g") {
|
||||
value = box.getFillColor().g;
|
||||
return true;
|
||||
} else if (name == "fill_color.b") {
|
||||
value = box.getFillColor().b;
|
||||
return true;
|
||||
} else if (name == "fill_color.a") {
|
||||
value = box.getFillColor().a;
|
||||
return true;
|
||||
} else if (name == "outline_color.r") {
|
||||
value = box.getOutlineColor().r;
|
||||
return true;
|
||||
} else if (name == "outline_color.g") {
|
||||
value = box.getOutlineColor().g;
|
||||
return true;
|
||||
} else if (name == "outline_color.b") {
|
||||
value = box.getOutlineColor().b;
|
||||
return true;
|
||||
} else if (name == "outline_color.a") {
|
||||
value = box.getOutlineColor().a;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIFrame::getProperty(const std::string& name, sf::Color& value) const {
|
||||
if (name == "fill_color") {
|
||||
value = box.getFillColor();
|
||||
return true;
|
||||
} else if (name == "outline_color") {
|
||||
value = box.getOutlineColor();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UIFrame::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||
if (name == "position") {
|
||||
value = box.getPosition();
|
||||
return true;
|
||||
} else if (name == "size") {
|
||||
value = box.getSize();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ public:
|
|||
sf::RectangleShape box;
|
||||
float outline;
|
||||
std::shared_ptr<std::vector<std::shared_ptr<UIDrawable>>> children;
|
||||
bool children_need_sort = true; // Dirty flag for z_index sorting optimization
|
||||
void render(sf::Vector2f, sf::RenderTarget&) override final;
|
||||
void move(sf::Vector2f);
|
||||
PyObjectsEnum derived_type() override final;
|
||||
|
@ -39,9 +40,20 @@ public:
|
|||
static int set_float_member(PyUIFrameObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_color_member(PyUIFrameObject* self, void* closure);
|
||||
static int set_color_member(PyUIFrameObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_pos(PyUIFrameObject* self, void* closure);
|
||||
static int set_pos(PyUIFrameObject* self, PyObject* value, void* closure);
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* repr(PyUIFrameObject* self);
|
||||
static int init(PyUIFrameObject* self, PyObject* args, PyObject* kwds);
|
||||
|
||||
// Animation property system
|
||||
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::Vector2f& value) override;
|
||||
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||
};
|
||||
|
||||
namespace mcrfpydef {
|
||||
|
|
835
src/UIGrid.cpp
22
src/UIGrid.h
|
@ -21,6 +21,9 @@ class UIGrid: public UIDrawable
|
|||
{
|
||||
private:
|
||||
std::shared_ptr<PyTexture> ptex;
|
||||
// Default cell dimensions when no texture is provided
|
||||
static constexpr int DEFAULT_CELL_WIDTH = 16;
|
||||
static constexpr int DEFAULT_CELL_HEIGHT = 16;
|
||||
public:
|
||||
UIGrid();
|
||||
//UIGrid(int, int, IndexTexture*, float, float, float, float);
|
||||
|
@ -43,8 +46,16 @@ public:
|
|||
std::vector<UIGridPoint> points;
|
||||
std::shared_ptr<std::list<std::shared_ptr<UIEntity>>> entities;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||
|
||||
static int init(PyUIGridObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* get_grid_size(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_grid_x(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_grid_y(PyUIGridObject* self, void* closure);
|
||||
static PyObject* get_position(PyUIGridObject* self, void* closure);
|
||||
static int set_position(PyUIGridObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_size(PyUIGridObject* self, void* closure);
|
||||
|
@ -71,14 +82,24 @@ typedef struct {
|
|||
class UIEntityCollection {
|
||||
public:
|
||||
static PySequenceMethods sqmethods;
|
||||
static PyMappingMethods mpmethods;
|
||||
static PyObject* append(PyUIEntityCollectionObject* self, PyObject* o);
|
||||
static PyObject* extend(PyUIEntityCollectionObject* self, PyObject* o);
|
||||
static PyObject* remove(PyUIEntityCollectionObject* self, PyObject* o);
|
||||
static PyObject* index_method(PyUIEntityCollectionObject* self, PyObject* value);
|
||||
static PyObject* count(PyUIEntityCollectionObject* self, PyObject* value);
|
||||
static PyMethodDef methods[];
|
||||
static PyObject* repr(PyUIEntityCollectionObject* self);
|
||||
static int init(PyUIEntityCollectionObject* self, PyObject* args, PyObject* kwds);
|
||||
static PyObject* iter(PyUIEntityCollectionObject* self);
|
||||
static Py_ssize_t len(PyUIEntityCollectionObject* self);
|
||||
static PyObject* getitem(PyUIEntityCollectionObject* self, Py_ssize_t index);
|
||||
static int setitem(PyUIEntityCollectionObject* self, Py_ssize_t index, PyObject* value);
|
||||
static int contains(PyUIEntityCollectionObject* self, PyObject* value);
|
||||
static PyObject* concat(PyUIEntityCollectionObject* self, PyObject* other);
|
||||
static PyObject* inplace_concat(PyUIEntityCollectionObject* self, PyObject* other);
|
||||
static PyObject* subscript(PyUIEntityCollectionObject* self, PyObject* key);
|
||||
static int ass_subscript(PyUIEntityCollectionObject* self, PyObject* key, PyObject* value);
|
||||
};
|
||||
|
||||
typedef struct {
|
||||
|
@ -168,6 +189,7 @@ namespace mcrfpydef {
|
|||
},
|
||||
.tp_repr = (reprfunc)UIEntityCollection::repr,
|
||||
.tp_as_sequence = &UIEntityCollection::sqmethods,
|
||||
.tp_as_mapping = &UIEntityCollection::mpmethods,
|
||||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = PyDoc_STR("Iterable, indexable collection of Entities"),
|
||||
.tp_iter = (getiterfunc)UIEntityCollection::iter,
|
||||
|
|
|
@ -75,7 +75,7 @@ namespace mcrfpydef {
|
|||
.tp_doc = "UIGridPoint object",
|
||||
.tp_getset = UIGridPoint::getsetters,
|
||||
//.tp_init = (initproc)PyUIGridPoint_init, // TODO Define the init function
|
||||
.tp_new = PyType_GenericNew,
|
||||
.tp_new = NULL, // Prevent instantiation from Python - Issue #12
|
||||
};
|
||||
|
||||
static PyTypeObject PyUIGridPointStateType = {
|
||||
|
@ -87,6 +87,6 @@ namespace mcrfpydef {
|
|||
.tp_flags = Py_TPFLAGS_DEFAULT,
|
||||
.tp_doc = "UIGridPointState object", // TODO: Add PyUIGridPointState tp_init
|
||||
.tp_getset = UIGridPointState::getsetters,
|
||||
.tp_new = PyType_GenericNew,
|
||||
.tp_new = NULL, // Prevent instantiation from Python - Issue #12
|
||||
};
|
||||
}
|
||||
|
|
212
src/UISprite.cpp
|
@ -1,5 +1,6 @@
|
|||
#include "UISprite.h"
|
||||
#include "GameEngine.h"
|
||||
#include "PyVector.h"
|
||||
|
||||
UIDrawable* UISprite::click_at(sf::Vector2f point)
|
||||
{
|
||||
|
@ -58,7 +59,7 @@ void UISprite::setSpriteIndex(int _sprite_index)
|
|||
sprite = ptex->sprite(sprite_index, sprite.getPosition(), sprite.getScale());
|
||||
}
|
||||
|
||||
sf::Vector2f UISprite::getScale()
|
||||
sf::Vector2f UISprite::getScale() const
|
||||
{
|
||||
return sprite.getScale();
|
||||
}
|
||||
|
@ -92,6 +93,10 @@ PyObject* UISprite::get_float_member(PyUISpriteObject* self, void* closure)
|
|||
return PyFloat_FromDouble(self->data->getPosition().y);
|
||||
else if (member_ptr == 2)
|
||||
return PyFloat_FromDouble(self->data->getScale().x); // scale X and Y are identical, presently
|
||||
else if (member_ptr == 3)
|
||||
return PyFloat_FromDouble(self->data->getScale().x); // scale_x
|
||||
else if (member_ptr == 4)
|
||||
return PyFloat_FromDouble(self->data->getScale().y); // scale_y
|
||||
else
|
||||
{
|
||||
PyErr_SetString(PyExc_AttributeError, "Invalid attribute");
|
||||
|
@ -120,8 +125,12 @@ int UISprite::set_float_member(PyUISpriteObject* self, PyObject* value, void* cl
|
|||
self->data->setPosition(sf::Vector2f(val, self->data->getPosition().y));
|
||||
else if (member_ptr == 1) //y
|
||||
self->data->setPosition(sf::Vector2f(self->data->getPosition().x, val));
|
||||
else if (member_ptr == 2) // scale
|
||||
else if (member_ptr == 2) // scale (uniform)
|
||||
self->data->setScale(sf::Vector2f(val, val));
|
||||
else if (member_ptr == 3) // scale_x
|
||||
self->data->setScale(sf::Vector2f(val, self->data->getScale().y));
|
||||
else if (member_ptr == 4) // scale_y
|
||||
self->data->setScale(sf::Vector2f(self->data->getScale().x, val));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -151,6 +160,20 @@ int UISprite::set_int_member(PyUISpriteObject* self, PyObject* value, void* clos
|
|||
PyErr_SetString(PyExc_TypeError, "Value must be an integer.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Validate sprite index is within texture bounds
|
||||
auto texture = self->data->getTexture();
|
||||
if (texture) {
|
||||
int sprite_count = texture->getSpriteCount();
|
||||
|
||||
if (val < 0 || val >= sprite_count) {
|
||||
PyErr_Format(PyExc_ValueError,
|
||||
"Sprite index %d out of range. Texture has %d sprites (0-%d)",
|
||||
val, sprite_count, sprite_count - 1);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
self->data->setSpriteIndex(val);
|
||||
return 0;
|
||||
}
|
||||
|
@ -162,16 +185,59 @@ PyObject* UISprite::get_texture(PyUISpriteObject* self, void* closure)
|
|||
|
||||
int UISprite::set_texture(PyUISpriteObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
return -1;
|
||||
// Check if value is a Texture instance
|
||||
if (!PyObject_IsInstance(value, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))) {
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Get the texture from the Python object
|
||||
auto pytexture = (PyTextureObject*)value;
|
||||
if (!pytexture->data) {
|
||||
PyErr_SetString(PyExc_ValueError, "Invalid texture object");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Update the sprite's texture
|
||||
self->data->setTexture(pytexture->data);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyObject* UISprite::get_pos(PyUISpriteObject* self, void* closure)
|
||||
{
|
||||
auto type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Vector");
|
||||
auto obj = (PyVectorObject*)type->tp_alloc(type, 0);
|
||||
if (obj) {
|
||||
auto pos = self->data->getPosition();
|
||||
obj->data = sf::Vector2f(pos.x, pos.y);
|
||||
}
|
||||
return (PyObject*)obj;
|
||||
}
|
||||
|
||||
int UISprite::set_pos(PyUISpriteObject* self, PyObject* value, void* closure)
|
||||
{
|
||||
PyVectorObject* vec = PyVector::from_arg(value);
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or convertible to Vector");
|
||||
return -1;
|
||||
}
|
||||
self->data->setPosition(vec->data);
|
||||
return 0;
|
||||
}
|
||||
|
||||
PyGetSetDef UISprite::getsetters[] = {
|
||||
{"x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "X coordinate of top-left corner", (void*)0},
|
||||
{"y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Y coordinate of top-left corner", (void*)1},
|
||||
{"scale", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Size factor", (void*)2},
|
||||
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
|
||||
{"scale", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Uniform size factor", (void*)2},
|
||||
{"scale_x", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Horizontal scale factor", (void*)3},
|
||||
{"scale_y", (getter)UISprite::get_float_member, (setter)UISprite::set_float_member, "Vertical scale factor", (void*)4},
|
||||
{"sprite_index", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown", NULL},
|
||||
{"sprite_number", (getter)UISprite::get_int_member, (setter)UISprite::set_int_member, "Which sprite on the texture is shown (deprecated: use sprite_index)", 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},
|
||||
{"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UISPRITE},
|
||||
{"pos", (getter)UISprite::get_pos, (setter)UISprite::set_pos, "Position as a Vector", NULL},
|
||||
{NULL}
|
||||
};
|
||||
|
||||
|
@ -183,7 +249,7 @@ PyObject* UISprite::repr(PyUISpriteObject* self)
|
|||
//auto sprite = self->data->sprite;
|
||||
ss << "<Sprite (x=" << self->data->getPosition().x << ", y=" << self->data->getPosition().y << ", " <<
|
||||
"scale=" << self->data->getScale().x << ", " <<
|
||||
"sprite_number=" << self->data->getSpriteIndex() << ")>";
|
||||
"sprite_index=" << self->data->getSpriteIndex() << ")>";
|
||||
}
|
||||
std::string repr_str = ss.str();
|
||||
return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace");
|
||||
|
@ -194,24 +260,138 @@ int UISprite::init(PyUISpriteObject* self, PyObject* args, PyObject* kwds)
|
|||
//std::cout << "Init called\n";
|
||||
static const char* keywords[] = { "x", "y", "texture", "sprite_index", "scale", nullptr };
|
||||
float x = 0.0f, y = 0.0f, scale = 1.0f;
|
||||
int sprite_index;
|
||||
PyObject* texture;
|
||||
int sprite_index = 0;
|
||||
PyObject* texture = NULL;
|
||||
|
||||
// First try to parse as (x, y, texture, ...)
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|ffOif",
|
||||
const_cast<char**>(keywords), &x, &y, &texture, &sprite_index, &scale))
|
||||
{
|
||||
PyErr_Clear(); // Clear the error
|
||||
|
||||
// Try to parse as ((x,y), texture, ...) or (Vector, texture, ...)
|
||||
PyObject* pos_obj = nullptr;
|
||||
const char* alt_keywords[] = { "pos", "texture", "sprite_index", "scale", nullptr };
|
||||
|
||||
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOif", const_cast<char**>(alt_keywords),
|
||||
&pos_obj, &texture, &sprite_index, &scale))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Convert position argument to x, y
|
||||
if (pos_obj) {
|
||||
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||
if (!vec) {
|
||||
PyErr_SetString(PyExc_TypeError, "First argument must be a tuple (x, y) or Vector when not providing x, y separately");
|
||||
return -1;
|
||||
}
|
||||
x = vec->data.x;
|
||||
y = vec->data.y;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle texture - allow None or use default
|
||||
std::shared_ptr<PyTexture> texture_ptr = nullptr;
|
||||
if (texture != NULL && texture != Py_None && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance or None");
|
||||
return -1;
|
||||
} else if (texture != NULL && texture != Py_None) {
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
texture_ptr = pytexture->data;
|
||||
} else {
|
||||
// Use default texture when None or not provided
|
||||
texture_ptr = McRFPy_API::default_texture;
|
||||
}
|
||||
|
||||
if (!texture_ptr) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "No texture provided and no default texture available");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// check types for texture
|
||||
//if (texture != NULL && !PyObject_IsInstance(texture, (PyObject*)&PyTextureType)){
|
||||
if (texture != NULL && !PyObject_IsInstance(texture, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Texture"))){
|
||||
PyErr_SetString(PyExc_TypeError, "texture must be a mcrfpy.Texture instance");
|
||||
return -1;
|
||||
}
|
||||
auto pytexture = (PyTextureObject*)texture;
|
||||
self->data = std::make_shared<UISprite>(pytexture->data, 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));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Property system implementation for animations
|
||||
bool UISprite::setProperty(const std::string& name, float value) {
|
||||
if (name == "x") {
|
||||
sprite.setPosition(sf::Vector2f(value, sprite.getPosition().y));
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
sprite.setPosition(sf::Vector2f(sprite.getPosition().x, value));
|
||||
return true;
|
||||
}
|
||||
else if (name == "scale") {
|
||||
sprite.setScale(sf::Vector2f(value, value));
|
||||
return true;
|
||||
}
|
||||
else if (name == "scale_x") {
|
||||
sprite.setScale(sf::Vector2f(value, sprite.getScale().y));
|
||||
return true;
|
||||
}
|
||||
else if (name == "scale_y") {
|
||||
sprite.setScale(sf::Vector2f(sprite.getScale().x, value));
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
z_index = static_cast<int>(value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UISprite::setProperty(const std::string& name, int value) {
|
||||
if (name == "sprite_index" || name == "sprite_number") {
|
||||
setSpriteIndex(value);
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
z_index = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UISprite::getProperty(const std::string& name, float& value) const {
|
||||
if (name == "x") {
|
||||
value = sprite.getPosition().x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "y") {
|
||||
value = sprite.getPosition().y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "scale") {
|
||||
value = sprite.getScale().x; // Assuming uniform scale
|
||||
return true;
|
||||
}
|
||||
else if (name == "scale_x") {
|
||||
value = sprite.getScale().x;
|
||||
return true;
|
||||
}
|
||||
else if (name == "scale_y") {
|
||||
value = sprite.getScale().y;
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
value = static_cast<float>(z_index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UISprite::getProperty(const std::string& name, int& value) const {
|
||||
if (name == "sprite_index" || name == "sprite_number") {
|
||||
value = sprite_index;
|
||||
return true;
|
||||
}
|
||||
else if (name == "z_index") {
|
||||
value = z_index;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ public:
|
|||
void setPosition(sf::Vector2f);
|
||||
sf::Vector2f getPosition();
|
||||
void setScale(sf::Vector2f);
|
||||
sf::Vector2f getScale();
|
||||
sf::Vector2f getScale() const;
|
||||
void setSpriteIndex(int);
|
||||
int getSpriteIndex();
|
||||
|
||||
|
@ -42,6 +42,12 @@ public:
|
|||
|
||||
PyObjectsEnum derived_type() override final;
|
||||
|
||||
// Property system for animations
|
||||
bool setProperty(const std::string& name, float value) override;
|
||||
bool setProperty(const std::string& name, int value) override;
|
||||
bool getProperty(const std::string& name, float& value) const override;
|
||||
bool getProperty(const std::string& name, int& value) const override;
|
||||
|
||||
|
||||
static PyObject* get_float_member(PyUISpriteObject* self, void* closure);
|
||||
static int set_float_member(PyUISpriteObject* self, PyObject* value, void* closure);
|
||||
|
@ -49,6 +55,8 @@ public:
|
|||
static int set_int_member(PyUISpriteObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_texture(PyUISpriteObject* self, void* closure);
|
||||
static int set_texture(PyUISpriteObject* self, PyObject* value, void* closure);
|
||||
static PyObject* get_pos(PyUISpriteObject* self, void* closure);
|
||||
static int set_pos(PyUISpriteObject* self, PyObject* value, void* closure);
|
||||
static PyGetSetDef getsetters[];
|
||||
static PyObject* repr(PyUISpriteObject* self);
|
||||
static int init(PyUISpriteObject* self, PyObject* args, PyObject* kwds);
|
||||
|
|
|
@ -156,8 +156,8 @@ void UITestScene::doAction(std::string name, std::string type)
|
|||
|
||||
void UITestScene::render()
|
||||
{
|
||||
game->getWindow().clear();
|
||||
game->getWindow().draw(text);
|
||||
game->getRenderTarget().clear();
|
||||
game->getRenderTarget().draw(text);
|
||||
|
||||
// draw all UI elements
|
||||
//for (auto e: ui_elements)
|
||||
|
@ -175,7 +175,7 @@ void UITestScene::render()
|
|||
|
||||
//e1.render(sf::Vector2f(-100, -100));
|
||||
|
||||
game->getWindow().display();
|
||||
// Display is handled by GameEngine
|
||||
|
||||
//McRFPy_API::REPL();
|
||||
}
|
||||
|
|
202
src/main.cpp
|
@ -1,8 +1,204 @@
|
|||
#include <SFML/Graphics.hpp>
|
||||
#include "GameEngine.h"
|
||||
#include "CommandLineParser.h"
|
||||
#include "McRogueFaceConfig.h"
|
||||
#include "McRFPy_API.h"
|
||||
#include "PyFont.h"
|
||||
#include "PyTexture.h"
|
||||
#include <Python.h>
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
|
||||
int main()
|
||||
// Forward declarations
|
||||
int run_game_engine(const McRogueFaceConfig& config);
|
||||
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[]);
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
GameEngine g;
|
||||
g.run();
|
||||
McRogueFaceConfig config;
|
||||
CommandLineParser parser(argc, argv);
|
||||
|
||||
// Parse arguments
|
||||
auto parse_result = parser.parse(config);
|
||||
if (parse_result.should_exit) {
|
||||
return parse_result.exit_code;
|
||||
}
|
||||
|
||||
// Special handling for -m module: let Python handle modules properly
|
||||
if (!config.python_module.empty()) {
|
||||
config.python_mode = true;
|
||||
}
|
||||
|
||||
// Initialize based on configuration
|
||||
if (config.python_mode) {
|
||||
return run_python_interpreter(config, argc, argv);
|
||||
} else {
|
||||
return run_game_engine(config);
|
||||
}
|
||||
}
|
||||
|
||||
int run_game_engine(const McRogueFaceConfig& config)
|
||||
{
|
||||
GameEngine g(config);
|
||||
g.run();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int run_python_interpreter(const McRogueFaceConfig& config, int argc, char* argv[])
|
||||
{
|
||||
// Create a game engine with the requested configuration
|
||||
GameEngine* engine = new GameEngine(config);
|
||||
|
||||
// Initialize Python with configuration
|
||||
McRFPy_API::init_python_with_config(config, argc, argv);
|
||||
|
||||
// Import mcrfpy module and store reference
|
||||
McRFPy_API::mcrf_module = PyImport_ImportModule("mcrfpy");
|
||||
if (!McRFPy_API::mcrf_module) {
|
||||
PyErr_Print();
|
||||
std::cerr << "Failed to import mcrfpy module" << std::endl;
|
||||
} else {
|
||||
// Set up default_font and default_texture if not already done
|
||||
if (!McRFPy_API::default_font) {
|
||||
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);
|
||||
}
|
||||
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_font", McRFPy_API::default_font->pyObject());
|
||||
PyObject_SetAttrString(McRFPy_API::mcrf_module, "default_texture", McRFPy_API::default_texture->pyObject());
|
||||
}
|
||||
|
||||
// Handle different Python modes
|
||||
if (!config.python_command.empty()) {
|
||||
// Execute command from -c
|
||||
if (config.interactive_mode) {
|
||||
// Use PyRun_String to catch SystemExit
|
||||
PyObject* main_module = PyImport_AddModule("__main__");
|
||||
PyObject* main_dict = PyModule_GetDict(main_module);
|
||||
PyObject* result_obj = PyRun_String(config.python_command.c_str(),
|
||||
Py_file_input, main_dict, main_dict);
|
||||
|
||||
if (result_obj == NULL) {
|
||||
// Check if it's SystemExit
|
||||
if (PyErr_Occurred()) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
|
||||
// If it's SystemExit and we're in interactive mode, clear it
|
||||
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
|
||||
PyErr_Clear();
|
||||
} else {
|
||||
// Re-raise other exceptions
|
||||
PyErr_Restore(type, value, traceback);
|
||||
PyErr_Print();
|
||||
}
|
||||
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
}
|
||||
} else {
|
||||
Py_DECREF(result_obj);
|
||||
}
|
||||
// Continue to interactive mode below
|
||||
} else {
|
||||
int result = PyRun_SimpleString(config.python_command.c_str());
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else if (!config.python_module.empty()) {
|
||||
// Execute module using runpy
|
||||
std::string run_module_code =
|
||||
"import sys\n"
|
||||
"import runpy\n"
|
||||
"sys.argv = ['" + config.python_module + "'";
|
||||
|
||||
for (const auto& arg : config.script_args) {
|
||||
run_module_code += ", '" + arg + "'";
|
||||
}
|
||||
run_module_code += "]\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());
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return result;
|
||||
}
|
||||
else if (!config.script_path.empty()) {
|
||||
// Execute script file
|
||||
FILE* fp = fopen(config.script_path.string().c_str(), "r");
|
||||
if (!fp) {
|
||||
std::cerr << "mcrogueface: can't open file '" << config.script_path << "': ";
|
||||
std::cerr << "[Errno " << errno << "] " << strerror(errno) << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Set up sys.argv
|
||||
wchar_t** python_argv = new wchar_t*[config.script_args.size() + 1];
|
||||
python_argv[0] = Py_DecodeLocale(config.script_path.string().c_str(), nullptr);
|
||||
for (size_t i = 0; i < config.script_args.size(); i++) {
|
||||
python_argv[i + 1] = Py_DecodeLocale(config.script_args[i].c_str(), nullptr);
|
||||
}
|
||||
PySys_SetArgvEx(config.script_args.size() + 1, python_argv, 0);
|
||||
|
||||
int result = PyRun_SimpleFile(fp, config.script_path.string().c_str());
|
||||
fclose(fp);
|
||||
|
||||
// Clean up
|
||||
for (size_t i = 0; i <= config.script_args.size(); i++) {
|
||||
PyMem_RawFree(python_argv[i]);
|
||||
}
|
||||
delete[] python_argv;
|
||||
|
||||
if (config.interactive_mode) {
|
||||
// Even if script had SystemExit, continue to interactive mode
|
||||
if (result != 0) {
|
||||
// Check if it was SystemExit
|
||||
if (PyErr_Occurred()) {
|
||||
PyObject *type, *value, *traceback;
|
||||
PyErr_Fetch(&type, &value, &traceback);
|
||||
|
||||
if (PyErr_GivenExceptionMatches(type, PyExc_SystemExit)) {
|
||||
PyErr_Clear();
|
||||
result = 0; // Don't exit with error
|
||||
} else {
|
||||
PyErr_Restore(type, value, traceback);
|
||||
PyErr_Print();
|
||||
}
|
||||
|
||||
Py_XDECREF(type);
|
||||
Py_XDECREF(value);
|
||||
Py_XDECREF(traceback);
|
||||
}
|
||||
}
|
||||
// Run interactive mode after script
|
||||
PyRun_InteractiveLoop(stdin, "<stdin>");
|
||||
}
|
||||
|
||||
// Run the game engine after script execution
|
||||
engine->run();
|
||||
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return result;
|
||||
}
|
||||
else if (config.interactive_mode) {
|
||||
// Interactive Python interpreter (only if explicitly requested with -i)
|
||||
Py_InspectFlag = 1;
|
||||
PyRun_InteractiveLoop(stdin, "<stdin>");
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
else if (!config.exec_scripts.empty()) {
|
||||
// With --exec, run the game engine after scripts execute
|
||||
engine->run();
|
||||
Py_Finalize();
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
|
||||
delete engine;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Example of CORRECT test pattern using timer callbacks for automation"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
from datetime import datetime
|
||||
|
||||
def run_automation_tests():
|
||||
"""This runs AFTER the game loop has started and rendered frames"""
|
||||
print("\n=== Automation Test Running (1 second after start) ===")
|
||||
|
||||
# NOW we can take screenshots that will show content!
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"WORKING_screenshot_{timestamp}.png"
|
||||
|
||||
# Take screenshot - this should now show our red frame
|
||||
result = automation.screenshot(filename)
|
||||
print(f"Screenshot taken: {filename} - Result: {result}")
|
||||
|
||||
# Test clicking on the frame
|
||||
automation.click(200, 200) # Click in center of red frame
|
||||
|
||||
# Test keyboard input
|
||||
automation.typewrite("Hello from timer callback!")
|
||||
|
||||
# Take another screenshot to show any changes
|
||||
filename2 = f"WORKING_screenshot_after_click_{timestamp}.png"
|
||||
automation.screenshot(filename2)
|
||||
print(f"Second screenshot: {filename2}")
|
||||
|
||||
print("Test completed successfully!")
|
||||
print("\nThis works because:")
|
||||
print("1. The game loop has been running for 1 second")
|
||||
print("2. The scene has been rendered multiple times")
|
||||
print("3. The RenderTexture now contains actual rendered content")
|
||||
|
||||
# Cancel this timer so it doesn't repeat
|
||||
mcrfpy.delTimer("automation_test")
|
||||
|
||||
# Optional: exit after a moment
|
||||
def exit_game():
|
||||
print("Exiting...")
|
||||
mcrfpy.exit()
|
||||
mcrfpy.setTimer("exit", exit_game, 500) # Exit 500ms later
|
||||
|
||||
# This code runs during --exec script execution
|
||||
print("=== Setting Up Test Scene ===")
|
||||
|
||||
# Create scene with visible content
|
||||
mcrfpy.createScene("timer_test_scene")
|
||||
mcrfpy.setScene("timer_test_scene")
|
||||
ui = mcrfpy.sceneUI("timer_test_scene")
|
||||
|
||||
# Add a bright red frame that should be visible
|
||||
frame = mcrfpy.Frame(100, 100, 400, 300,
|
||||
fill_color=mcrfpy.Color(255, 0, 0), # Bright red
|
||||
outline_color=mcrfpy.Color(255, 255, 255), # White outline
|
||||
outline=5.0)
|
||||
ui.append(frame)
|
||||
|
||||
# Add text
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(150, 150),
|
||||
text="TIMER TEST - SHOULD BE VISIBLE",
|
||||
fill_color=mcrfpy.Color(255, 255, 255))
|
||||
caption.size = 24
|
||||
frame.children.append(caption)
|
||||
|
||||
# Add click handler to demonstrate interaction
|
||||
def frame_clicked(x, y, button):
|
||||
print(f"Frame clicked at ({x}, {y}) with button {button}")
|
||||
|
||||
frame.click = frame_clicked
|
||||
|
||||
print("Scene setup complete. Setting timer for automation tests...")
|
||||
|
||||
# THIS IS THE KEY: Set timer to run AFTER the game loop starts
|
||||
mcrfpy.setTimer("automation_test", run_automation_tests, 1000)
|
||||
|
||||
print("Timer set. Game loop will start after this script completes.")
|
||||
print("Automation tests will run 1 second later when content is visible.")
|
||||
|
||||
# Script ends here - game loop starts next
|
|
@ -0,0 +1,165 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Animation System Demo - Shows all animation capabilities"""
|
||||
|
||||
import mcrfpy
|
||||
import math
|
||||
|
||||
# Create main scene
|
||||
mcrfpy.createScene("animation_demo")
|
||||
ui = mcrfpy.sceneUI("animation_demo")
|
||||
mcrfpy.setScene("animation_demo")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption((400, 30), "McRogueFace Animation System Demo", mcrfpy.default_font)
|
||||
title.size = 24
|
||||
title.fill_color = (255, 255, 255)
|
||||
# Note: centered property doesn't exist for Caption
|
||||
ui.append(title)
|
||||
|
||||
# 1. Position Animation Demo
|
||||
pos_frame = mcrfpy.Frame(50, 100, 80, 80)
|
||||
pos_frame.fill_color = (255, 100, 100)
|
||||
pos_frame.outline = 2
|
||||
ui.append(pos_frame)
|
||||
|
||||
pos_label = mcrfpy.Caption((50, 80), "Position Animation", mcrfpy.default_font)
|
||||
pos_label.fill_color = (200, 200, 200)
|
||||
ui.append(pos_label)
|
||||
|
||||
# 2. Size Animation Demo
|
||||
size_frame = mcrfpy.Frame(200, 100, 50, 50)
|
||||
size_frame.fill_color = (100, 255, 100)
|
||||
size_frame.outline = 2
|
||||
ui.append(size_frame)
|
||||
|
||||
size_label = mcrfpy.Caption((200, 80), "Size Animation", mcrfpy.default_font)
|
||||
size_label.fill_color = (200, 200, 200)
|
||||
ui.append(size_label)
|
||||
|
||||
# 3. Color Animation Demo
|
||||
color_frame = mcrfpy.Frame(350, 100, 80, 80)
|
||||
color_frame.fill_color = (255, 0, 0)
|
||||
ui.append(color_frame)
|
||||
|
||||
color_label = mcrfpy.Caption((350, 80), "Color Animation", mcrfpy.default_font)
|
||||
color_label.fill_color = (200, 200, 200)
|
||||
ui.append(color_label)
|
||||
|
||||
# 4. Easing Functions Demo
|
||||
easing_y = 250
|
||||
easing_frames = []
|
||||
easings = ["linear", "easeIn", "easeOut", "easeInOut", "easeInElastic", "easeOutBounce"]
|
||||
|
||||
for i, easing in enumerate(easings):
|
||||
x = 50 + i * 120
|
||||
|
||||
frame = mcrfpy.Frame(x, easing_y, 20, 20)
|
||||
frame.fill_color = (100, 150, 255)
|
||||
ui.append(frame)
|
||||
easing_frames.append((frame, easing))
|
||||
|
||||
label = mcrfpy.Caption((x, easing_y - 20), easing, mcrfpy.default_font)
|
||||
label.size = 12
|
||||
label.fill_color = (200, 200, 200)
|
||||
ui.append(label)
|
||||
|
||||
# 5. Complex Animation Demo
|
||||
complex_frame = mcrfpy.Frame(300, 350, 100, 100)
|
||||
complex_frame.fill_color = (128, 128, 255)
|
||||
complex_frame.outline = 3
|
||||
ui.append(complex_frame)
|
||||
|
||||
complex_label = mcrfpy.Caption((300, 330), "Complex Multi-Property", mcrfpy.default_font)
|
||||
complex_label.fill_color = (200, 200, 200)
|
||||
ui.append(complex_label)
|
||||
|
||||
# Start animations
|
||||
def start_animations(runtime):
|
||||
# 1. Position animation - back and forth
|
||||
x_anim = mcrfpy.Animation("x", 500.0, 3.0, "easeInOut")
|
||||
x_anim.start(pos_frame)
|
||||
|
||||
# 2. Size animation - pulsing
|
||||
w_anim = mcrfpy.Animation("w", 150.0, 2.0, "easeInOut")
|
||||
h_anim = mcrfpy.Animation("h", 150.0, 2.0, "easeInOut")
|
||||
w_anim.start(size_frame)
|
||||
h_anim.start(size_frame)
|
||||
|
||||
# 3. Color animation - rainbow cycle
|
||||
color_anim = mcrfpy.Animation("fill_color", (0, 255, 255, 255), 2.0, "linear")
|
||||
color_anim.start(color_frame)
|
||||
|
||||
# 4. Easing demos - all move up with different easings
|
||||
for frame, easing in easing_frames:
|
||||
y_anim = mcrfpy.Animation("y", 150.0, 2.0, easing)
|
||||
y_anim.start(frame)
|
||||
|
||||
# 5. Complex animation - multiple properties
|
||||
cx_anim = mcrfpy.Animation("x", 500.0, 4.0, "easeInOut")
|
||||
cy_anim = mcrfpy.Animation("y", 400.0, 4.0, "easeOut")
|
||||
cw_anim = mcrfpy.Animation("w", 150.0, 4.0, "easeInElastic")
|
||||
ch_anim = mcrfpy.Animation("h", 150.0, 4.0, "easeInElastic")
|
||||
outline_anim = mcrfpy.Animation("outline", 10.0, 4.0, "linear")
|
||||
|
||||
cx_anim.start(complex_frame)
|
||||
cy_anim.start(complex_frame)
|
||||
cw_anim.start(complex_frame)
|
||||
ch_anim.start(complex_frame)
|
||||
outline_anim.start(complex_frame)
|
||||
|
||||
# Individual color component animations
|
||||
r_anim = mcrfpy.Animation("fill_color.r", 255.0, 4.0, "easeInOut")
|
||||
g_anim = mcrfpy.Animation("fill_color.g", 100.0, 4.0, "easeInOut")
|
||||
b_anim = mcrfpy.Animation("fill_color.b", 50.0, 4.0, "easeInOut")
|
||||
|
||||
r_anim.start(complex_frame)
|
||||
g_anim.start(complex_frame)
|
||||
b_anim.start(complex_frame)
|
||||
|
||||
print("All animations started!")
|
||||
|
||||
# Reverse some animations
|
||||
def reverse_animations(runtime):
|
||||
# Position back
|
||||
x_anim = mcrfpy.Animation("x", 50.0, 3.0, "easeInOut")
|
||||
x_anim.start(pos_frame)
|
||||
|
||||
# Size back
|
||||
w_anim = mcrfpy.Animation("w", 50.0, 2.0, "easeInOut")
|
||||
h_anim = mcrfpy.Animation("h", 50.0, 2.0, "easeInOut")
|
||||
w_anim.start(size_frame)
|
||||
h_anim.start(size_frame)
|
||||
|
||||
# Color cycle continues
|
||||
color_anim = mcrfpy.Animation("fill_color", (255, 0, 255, 255), 2.0, "linear")
|
||||
color_anim.start(color_frame)
|
||||
|
||||
# Easing frames back down
|
||||
for frame, easing in easing_frames:
|
||||
y_anim = mcrfpy.Animation("y", 250.0, 2.0, easing)
|
||||
y_anim.start(frame)
|
||||
|
||||
# Continue color cycle
|
||||
def cycle_colors(runtime):
|
||||
color_anim = mcrfpy.Animation("fill_color", (255, 255, 0, 255), 2.0, "linear")
|
||||
color_anim.start(color_frame)
|
||||
|
||||
# Info text
|
||||
info = mcrfpy.Caption((400, 550), "Watch as different properties animate with various easing functions!", mcrfpy.default_font)
|
||||
info.fill_color = (255, 255, 200)
|
||||
# Note: centered property doesn't exist for Caption
|
||||
ui.append(info)
|
||||
|
||||
# Schedule animations
|
||||
mcrfpy.setTimer("start", start_animations, 500)
|
||||
mcrfpy.setTimer("reverse", reverse_animations, 4000)
|
||||
mcrfpy.setTimer("cycle", cycle_colors, 2500)
|
||||
|
||||
# Exit handler
|
||||
def on_key(key):
|
||||
if key == "Escape":
|
||||
mcrfpy.exit()
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
||||
print("Animation demo started! Press Escape to exit.")
|
|
@ -0,0 +1,34 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for mcrfpy.createScene() method"""
|
||||
import mcrfpy
|
||||
|
||||
def test_createScene():
|
||||
"""Test creating a new scene"""
|
||||
# Test creating scenes
|
||||
test_scenes = ["test_scene1", "test_scene2", "special_chars_!@#"]
|
||||
|
||||
for scene_name in test_scenes:
|
||||
try:
|
||||
mcrfpy.createScene(scene_name)
|
||||
print(f"✓ Created scene: {scene_name}")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to create scene {scene_name}: {e}")
|
||||
return
|
||||
|
||||
# Try to set scene to verify it was created
|
||||
try:
|
||||
mcrfpy.setScene("test_scene1")
|
||||
current = mcrfpy.currentScene()
|
||||
if current == "test_scene1":
|
||||
print("✓ Scene switching works correctly")
|
||||
else:
|
||||
print(f"✗ Scene switch failed: expected 'test_scene1', got '{current}'")
|
||||
except Exception as e:
|
||||
print(f"✗ Scene switching error: {e}")
|
||||
|
||||
print("PASS")
|
||||
|
||||
# Run test immediately
|
||||
print("Running createScene test...")
|
||||
test_createScene()
|
||||
print("Test completed.")
|
|
@ -0,0 +1,92 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for mcrfpy.keypressScene() - Related to issue #61"""
|
||||
import mcrfpy
|
||||
|
||||
# Track keypresses for different scenes
|
||||
scene1_presses = []
|
||||
scene2_presses = []
|
||||
|
||||
def scene1_handler(key_code):
|
||||
"""Handle keyboard events for scene 1"""
|
||||
scene1_presses.append(key_code)
|
||||
print(f"Scene 1 key pressed: {key_code}")
|
||||
|
||||
def scene2_handler(key_code):
|
||||
"""Handle keyboard events for scene 2"""
|
||||
scene2_presses.append(key_code)
|
||||
print(f"Scene 2 key pressed: {key_code}")
|
||||
|
||||
def test_keypressScene():
|
||||
"""Test keyboard event handling for scenes"""
|
||||
print("=== Testing mcrfpy.keypressScene() ===")
|
||||
|
||||
# Test 1: Basic handler registration
|
||||
print("\n1. Basic handler registration:")
|
||||
mcrfpy.createScene("scene1")
|
||||
mcrfpy.setScene("scene1")
|
||||
|
||||
try:
|
||||
mcrfpy.keypressScene(scene1_handler)
|
||||
print("✓ Keypress handler registered for scene1")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to register handler: {e}")
|
||||
print("FAIL")
|
||||
return
|
||||
|
||||
# Test 2: Handler persists across scene changes
|
||||
print("\n2. Testing handler persistence:")
|
||||
mcrfpy.createScene("scene2")
|
||||
mcrfpy.setScene("scene2")
|
||||
|
||||
try:
|
||||
mcrfpy.keypressScene(scene2_handler)
|
||||
print("✓ Keypress handler registered for scene2")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to register handler for scene2: {e}")
|
||||
|
||||
# Switch back to scene1
|
||||
mcrfpy.setScene("scene1")
|
||||
current = mcrfpy.currentScene()
|
||||
print(f"✓ Switched back to: {current}")
|
||||
|
||||
# Test 3: Clear handler
|
||||
print("\n3. Testing handler clearing:")
|
||||
try:
|
||||
mcrfpy.keypressScene(None)
|
||||
print("✓ Handler cleared with None")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to clear handler: {e}")
|
||||
|
||||
# Test 4: Re-register handler
|
||||
print("\n4. Testing re-registration:")
|
||||
try:
|
||||
mcrfpy.keypressScene(scene1_handler)
|
||||
print("✓ Handler re-registered successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed to re-register: {e}")
|
||||
|
||||
# Test 5: Lambda functions
|
||||
print("\n5. Testing lambda functions:")
|
||||
try:
|
||||
mcrfpy.keypressScene(lambda k: print(f"Lambda key: {k}"))
|
||||
print("✓ Lambda function accepted as handler")
|
||||
except Exception as e:
|
||||
print(f"✗ Failed with lambda: {e}")
|
||||
|
||||
# Known issues
|
||||
print("\n⚠ Known Issues:")
|
||||
print("- Invalid argument (non-callable) causes segfault")
|
||||
print("- No way to query current handler")
|
||||
print("- Handler is global, not per-scene (issue #61)")
|
||||
|
||||
# Summary related to issue #61
|
||||
print("\n📋 Issue #61 Analysis:")
|
||||
print("Current: mcrfpy.keypressScene() sets a global handler")
|
||||
print("Proposed: Scene objects should encapsulate their own callbacks")
|
||||
print("Impact: Currently only one keypress handler active at a time")
|
||||
|
||||
print("\n=== Test Complete ===")
|
||||
print("PASS - API functions correctly within current limitations")
|
||||
|
||||
# Run test immediately
|
||||
test_keypressScene()
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for mcrfpy.sceneUI() method - Related to issue #28"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
from datetime import datetime
|
||||
|
||||
def test_sceneUI():
|
||||
"""Test getting UI collection from scene"""
|
||||
# Create a test scene
|
||||
mcrfpy.createScene("ui_test_scene")
|
||||
mcrfpy.setScene("ui_test_scene")
|
||||
|
||||
# Get initial UI collection (should be empty)
|
||||
try:
|
||||
ui_collection = mcrfpy.sceneUI("ui_test_scene")
|
||||
print(f"✓ sceneUI returned collection with {len(ui_collection)} items")
|
||||
except Exception as e:
|
||||
print(f"✗ sceneUI failed: {e}")
|
||||
print("FAIL")
|
||||
return
|
||||
|
||||
# Add some UI elements to the scene
|
||||
frame = mcrfpy.Frame(10, 10, 200, 150,
|
||||
fill_color=mcrfpy.Color(100, 100, 200),
|
||||
outline_color=mcrfpy.Color(255, 255, 255),
|
||||
outline=2.0)
|
||||
ui_collection.append(frame)
|
||||
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(220, 10),
|
||||
text="Test Caption",
|
||||
fill_color=mcrfpy.Color(255, 255, 0))
|
||||
ui_collection.append(caption)
|
||||
|
||||
# Skip sprite for now since it requires a texture
|
||||
# sprite = mcrfpy.Sprite(10, 170, scale=2.0)
|
||||
# ui_collection.append(sprite)
|
||||
|
||||
# Get UI collection again
|
||||
ui_collection2 = mcrfpy.sceneUI("ui_test_scene")
|
||||
print(f"✓ After adding elements: {len(ui_collection2)} items")
|
||||
|
||||
# Test iteration (Issue #28 - UICollectionIter)
|
||||
try:
|
||||
item_types = []
|
||||
for item in ui_collection2:
|
||||
item_types.append(type(item).__name__)
|
||||
print(f"✓ Iteration works, found types: {item_types}")
|
||||
except Exception as e:
|
||||
print(f"✗ Iteration failed (Issue #28): {e}")
|
||||
|
||||
# Test indexing
|
||||
try:
|
||||
first_item = ui_collection2[0]
|
||||
print(f"✓ Indexing works, first item type: {type(first_item).__name__}")
|
||||
except Exception as e:
|
||||
print(f"✗ Indexing failed: {e}")
|
||||
|
||||
# Test invalid scene name
|
||||
try:
|
||||
invalid_ui = mcrfpy.sceneUI("nonexistent_scene")
|
||||
print(f"✗ sceneUI should fail for nonexistent scene, got {len(invalid_ui)} items")
|
||||
except Exception as e:
|
||||
print(f"✓ sceneUI correctly fails for nonexistent scene: {e}")
|
||||
|
||||
# Take screenshot
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"test_sceneUI_{timestamp}.png"
|
||||
automation.screenshot(filename)
|
||||
print(f"Screenshot saved: {filename}")
|
||||
print("PASS")
|
||||
|
||||
# Set up timer to run test
|
||||
mcrfpy.setTimer("test", test_sceneUI, 1000)
|
||||
|
||||
# Cancel timer after running once
|
||||
def cleanup():
|
||||
mcrfpy.delTimer("test")
|
||||
mcrfpy.delTimer("cleanup")
|
||||
|
||||
mcrfpy.setTimer("cleanup", cleanup, 1100)
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for mcrfpy.setScene() and currentScene() methods"""
|
||||
import mcrfpy
|
||||
|
||||
print("Starting setScene/currentScene test...")
|
||||
|
||||
# Create test scenes first
|
||||
scenes = ["scene_A", "scene_B", "scene_C"]
|
||||
for scene in scenes:
|
||||
mcrfpy.createScene(scene)
|
||||
print(f"Created scene: {scene}")
|
||||
|
||||
results = []
|
||||
|
||||
# Test switching between scenes
|
||||
for scene in scenes:
|
||||
try:
|
||||
mcrfpy.setScene(scene)
|
||||
current = mcrfpy.currentScene()
|
||||
if current == scene:
|
||||
results.append(f"✓ setScene/currentScene works for '{scene}'")
|
||||
else:
|
||||
results.append(f"✗ Scene mismatch: set '{scene}', got '{current}'")
|
||||
except Exception as e:
|
||||
results.append(f"✗ Error with scene '{scene}': {e}")
|
||||
|
||||
# Test invalid scene - it should not change the current scene
|
||||
current_before = mcrfpy.currentScene()
|
||||
mcrfpy.setScene("nonexistent_scene")
|
||||
current_after = mcrfpy.currentScene()
|
||||
if current_before == current_after:
|
||||
results.append(f"✓ setScene correctly ignores nonexistent scene (stayed on '{current_after}')")
|
||||
else:
|
||||
results.append(f"✗ Scene changed unexpectedly from '{current_before}' to '{current_after}'")
|
||||
|
||||
# Print results
|
||||
for result in results:
|
||||
print(result)
|
||||
|
||||
# Determine pass/fail
|
||||
if all("✓" in r for r in results):
|
||||
print("PASS")
|
||||
else:
|
||||
print("FAIL")
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for mcrfpy.setTimer() and delTimer() methods"""
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_timers():
|
||||
"""Test timer API methods"""
|
||||
print("Testing mcrfpy timer methods...")
|
||||
|
||||
# Test 1: Create a simple timer
|
||||
try:
|
||||
call_count = [0]
|
||||
def simple_callback(runtime):
|
||||
call_count[0] += 1
|
||||
print(f"Timer callback called, count={call_count[0]}, runtime={runtime}")
|
||||
|
||||
mcrfpy.setTimer("test_timer", simple_callback, 100)
|
||||
print("✓ setTimer() called successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ setTimer() failed: {e}")
|
||||
print("FAIL")
|
||||
return
|
||||
|
||||
# Test 2: Delete the timer
|
||||
try:
|
||||
mcrfpy.delTimer("test_timer")
|
||||
print("✓ delTimer() called successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ delTimer() failed: {e}")
|
||||
print("FAIL")
|
||||
return
|
||||
|
||||
# Test 3: Delete non-existent timer (should not crash)
|
||||
try:
|
||||
mcrfpy.delTimer("nonexistent_timer")
|
||||
print("✓ delTimer() accepts non-existent timer names")
|
||||
except Exception as e:
|
||||
print(f"✗ delTimer() failed on non-existent timer: {e}")
|
||||
print("FAIL")
|
||||
return
|
||||
|
||||
# Test 4: Create multiple timers
|
||||
try:
|
||||
def callback1(rt): pass
|
||||
def callback2(rt): pass
|
||||
def callback3(rt): pass
|
||||
|
||||
mcrfpy.setTimer("timer1", callback1, 500)
|
||||
mcrfpy.setTimer("timer2", callback2, 750)
|
||||
mcrfpy.setTimer("timer3", callback3, 250)
|
||||
print("✓ Multiple timers created successfully")
|
||||
|
||||
# Clean up
|
||||
mcrfpy.delTimer("timer1")
|
||||
mcrfpy.delTimer("timer2")
|
||||
mcrfpy.delTimer("timer3")
|
||||
print("✓ Multiple timers deleted successfully")
|
||||
except Exception as e:
|
||||
print(f"✗ Multiple timer test failed: {e}")
|
||||
print("FAIL")
|
||||
return
|
||||
|
||||
print("\nAll timer API tests passed")
|
||||
print("PASS")
|
||||
|
||||
# Run the test
|
||||
test_timers()
|
||||
|
||||
# Exit cleanly
|
||||
sys.exit(0)
|
|
@ -0,0 +1,63 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Analysis of Issue #78: Middle Mouse Click sends 'C' keyboard event
|
||||
|
||||
BUG FOUND in GameEngine::processEvent() at src/GameEngine.cpp
|
||||
|
||||
The bug occurs in this code section:
|
||||
```cpp
|
||||
if (currentScene()->hasAction(actionCode))
|
||||
{
|
||||
std::string name = currentScene()->action(actionCode);
|
||||
currentScene()->doAction(name, actionType);
|
||||
}
|
||||
else if (currentScene()->key_callable)
|
||||
{
|
||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||
}
|
||||
```
|
||||
|
||||
ISSUE: When a middle mouse button event occurs and there's no registered action for it,
|
||||
the code falls through to the key_callable branch. However, it then tries to access
|
||||
`event.key.code` from what is actually a mouse button event!
|
||||
|
||||
Since it's a union, `event.key.code` reads garbage data from the mouse event structure.
|
||||
The middle mouse button has value 2, which coincidentally matches sf::Keyboard::C (also value 2),
|
||||
causing the spurious 'C' keyboard event.
|
||||
|
||||
SOLUTION: The code should check the event type before accessing event-specific fields:
|
||||
|
||||
```cpp
|
||||
else if (currentScene()->key_callable &&
|
||||
(event.type == sf::Event::KeyPressed || event.type == sf::Event::KeyReleased))
|
||||
{
|
||||
currentScene()->key_callable->call(ActionCode::key_str(event.key.code), actionType);
|
||||
}
|
||||
```
|
||||
|
||||
TEST STATUS:
|
||||
- Test Name: automation_click_issue78_test.py
|
||||
- Method Tested: Middle mouse click behavior
|
||||
- Pass/Fail: FAIL - Issue #78 confirmed to exist
|
||||
- Error: Middle mouse clicks incorrectly trigger 'C' keyboard events
|
||||
- Modifications: None needed - bug is in C++ code, not the test
|
||||
|
||||
The test correctly identifies the issue but cannot run in headless mode due to
|
||||
requiring actual event processing through the game loop.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
print(__doc__)
|
||||
|
||||
# Demonstrate the issue conceptually
|
||||
print("\nDemonstration of the bug:")
|
||||
print("1. Middle mouse button value in SFML: 2")
|
||||
print("2. Keyboard 'C' value in SFML: 2")
|
||||
print("3. When processEvent reads event.key.code from a mouse event,")
|
||||
print(" it gets the value 2, which ActionCode::key_str interprets as 'C'")
|
||||
|
||||
print("\nThe fix is simple: add an event type check before accessing key.code")
|
||||
|
||||
sys.exit(0)
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for automation click methods - Related to issue #78 (Middle click sends 'C')"""
|
||||
import mcrfpy
|
||||
from datetime import datetime
|
||||
|
||||
# Try to import automation, but handle if it doesn't exist
|
||||
try:
|
||||
from mcrfpy import automation
|
||||
HAS_AUTOMATION = True
|
||||
print("SUCCESS: mcrfpy.automation module imported successfully")
|
||||
except (ImportError, AttributeError) as e:
|
||||
HAS_AUTOMATION = False
|
||||
print(f"WARNING: mcrfpy.automation module not available - {e}")
|
||||
print("The automation module may not be implemented yet")
|
||||
|
||||
# Track events
|
||||
click_events = []
|
||||
key_events = []
|
||||
|
||||
def click_handler(x, y, button):
|
||||
"""Track click events"""
|
||||
click_events.append((x, y, button))
|
||||
print(f"Click received: ({x}, {y}, button={button})")
|
||||
|
||||
def key_handler(key, scancode=None):
|
||||
"""Track keyboard events"""
|
||||
key_events.append(key)
|
||||
print(f"Key received: {key} (scancode: {scancode})")
|
||||
|
||||
def test_clicks():
|
||||
"""Test various click types, especially middle click (Issue #78)"""
|
||||
if not HAS_AUTOMATION:
|
||||
print("SKIP - automation module not available")
|
||||
print("The automation module may not be implemented yet")
|
||||
return
|
||||
|
||||
# Create test scene
|
||||
mcrfpy.createScene("click_test")
|
||||
mcrfpy.setScene("click_test")
|
||||
ui = mcrfpy.sceneUI("click_test")
|
||||
|
||||
# Set up keyboard handler to detect Issue #78
|
||||
mcrfpy.keypressScene(key_handler)
|
||||
|
||||
# Create clickable frame
|
||||
frame = mcrfpy.Frame(50, 50, 300, 200,
|
||||
fill_color=mcrfpy.Color(100, 100, 200),
|
||||
outline_color=mcrfpy.Color(255, 255, 255),
|
||||
outline=2.0)
|
||||
frame.click = click_handler
|
||||
ui.append(frame)
|
||||
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(60, 60),
|
||||
text="Click Test Area",
|
||||
fill_color=mcrfpy.Color(255, 255, 255))
|
||||
frame.children.append(caption)
|
||||
|
||||
# Test different click types
|
||||
print("Testing click types...")
|
||||
|
||||
# Left click
|
||||
try:
|
||||
automation.click(200, 150)
|
||||
print("✓ Left click sent")
|
||||
except Exception as e:
|
||||
print(f"✗ Left click failed: {e}")
|
||||
|
||||
# Right click
|
||||
try:
|
||||
automation.rightClick(200, 150)
|
||||
print("✓ Right click sent")
|
||||
except Exception as e:
|
||||
print(f"✗ Right click failed: {e}")
|
||||
|
||||
# Middle click - This is Issue #78
|
||||
try:
|
||||
automation.middleClick(200, 150)
|
||||
print("✓ Middle click sent")
|
||||
except Exception as e:
|
||||
print(f"✗ Middle click failed: {e}")
|
||||
|
||||
# Double click
|
||||
try:
|
||||
automation.doubleClick(200, 150)
|
||||
print("✓ Double click sent")
|
||||
except Exception as e:
|
||||
print(f"✗ Double click failed: {e}")
|
||||
|
||||
# Triple click
|
||||
try:
|
||||
automation.tripleClick(200, 150)
|
||||
print("✓ Triple click sent")
|
||||
except Exception as e:
|
||||
print(f"✗ Triple click failed: {e}")
|
||||
|
||||
# Click with specific button parameter
|
||||
try:
|
||||
automation.click(200, 150, button='middle')
|
||||
print("✓ Click with button='middle' sent")
|
||||
except Exception as e:
|
||||
print(f"✗ Click with button parameter failed: {e}")
|
||||
|
||||
# Check results after a delay
|
||||
def check_results(runtime):
|
||||
print(f"\nClick events received: {len(click_events)}")
|
||||
print(f"Keyboard events received: {len(key_events)}")
|
||||
|
||||
# Check for Issue #78
|
||||
if any('C' in str(event) or ord('C') == event for event in key_events):
|
||||
print("✗ ISSUE #78 CONFIRMED: Middle click sent 'C' keyboard event!")
|
||||
else:
|
||||
print("✓ No spurious 'C' keyboard events detected")
|
||||
|
||||
# Analyze click events
|
||||
for event in click_events:
|
||||
print(f" Click: {event}")
|
||||
|
||||
# Take screenshot
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filename = f"test_clicks_issue78_{timestamp}.png"
|
||||
automation.screenshot(filename)
|
||||
print(f"Screenshot saved: {filename}")
|
||||
|
||||
if len(click_events) > 0:
|
||||
print("PASS - Clicks detected")
|
||||
else:
|
||||
print("FAIL - No clicks detected (may be headless limitation)")
|
||||
|
||||
mcrfpy.delTimer("check_results")
|
||||
|
||||
mcrfpy.setTimer("check_results", check_results, 2000)
|
||||
|
||||
# Set up timer to run test
|
||||
print("Setting up test timer...")
|
||||
mcrfpy.setTimer("test", test_clicks, 1000)
|
||||
|
||||
# Cancel timer after running once
|
||||
def cleanup():
|
||||
mcrfpy.delTimer("test")
|
||||
mcrfpy.delTimer("cleanup")
|
||||
|
||||
mcrfpy.setTimer("cleanup", cleanup, 1100)
|
||||
|
||||
# Exit after test completes
|
||||
def exit_test():
|
||||
print("\nTest completed - exiting")
|
||||
import sys
|
||||
sys.exit(0)
|
||||
|
||||
mcrfpy.setTimer("exit", exit_test, 5000)
|
||||
|
||||
print("Test script initialized, waiting for timers...")
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test for mcrfpy.automation.screenshot()"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
from datetime import datetime
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
runs = 0
|
||||
def test_screenshot(*args):
|
||||
"""Test screenshot functionality"""
|
||||
#global runs
|
||||
#runs += 1
|
||||
#if runs < 2:
|
||||
# print("tick")
|
||||
# return
|
||||
#print("tock")
|
||||
#mcrfpy.delTimer("timer1")
|
||||
# Create a scene with some visual elements
|
||||
mcrfpy.createScene("screenshot_test")
|
||||
mcrfpy.setScene("screenshot_test")
|
||||
ui = mcrfpy.sceneUI("screenshot_test")
|
||||
|
||||
# Add some colorful elements
|
||||
frame1 = mcrfpy.Frame(10, 10, 200, 150,
|
||||
fill_color=mcrfpy.Color(255, 0, 0),
|
||||
outline_color=mcrfpy.Color(255, 255, 255),
|
||||
outline=3.0)
|
||||
ui.append(frame1)
|
||||
|
||||
frame2 = mcrfpy.Frame(220, 10, 200, 150,
|
||||
fill_color=mcrfpy.Color(0, 255, 0),
|
||||
outline_color=mcrfpy.Color(0, 0, 0),
|
||||
outline=2.0)
|
||||
ui.append(frame2)
|
||||
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(10, 170),
|
||||
text="Screenshot Test Scene",
|
||||
fill_color=mcrfpy.Color(255, 255, 0))
|
||||
caption.size = 24
|
||||
ui.append(caption)
|
||||
|
||||
# Test multiple screenshots
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
filenames = []
|
||||
|
||||
# Test 1: Basic screenshot
|
||||
try:
|
||||
filename1 = f"test_screenshot_basic_{timestamp}.png"
|
||||
result = automation.screenshot(filename1)
|
||||
filenames.append(filename1)
|
||||
print(f"✓ Basic screenshot saved: {filename1} (result: {result})")
|
||||
except Exception as e:
|
||||
print(f"✗ Basic screenshot failed: {e}")
|
||||
print("FAIL")
|
||||
sys.exit(1)
|
||||
|
||||
# Test 2: Screenshot with special characters in filename
|
||||
try:
|
||||
filename2 = f"test_screenshot_special_chars_{timestamp}_test.png"
|
||||
result = automation.screenshot(filename2)
|
||||
filenames.append(filename2)
|
||||
print(f"✓ Screenshot with special filename saved: {filename2} (result: {result})")
|
||||
except Exception as e:
|
||||
print(f"✗ Special filename screenshot failed: {e}")
|
||||
|
||||
# Test 3: Invalid filename (if applicable)
|
||||
try:
|
||||
result = automation.screenshot("")
|
||||
print(f"✗ Empty filename should fail but returned: {result}")
|
||||
except Exception as e:
|
||||
print(f"✓ Empty filename correctly rejected: {e}")
|
||||
|
||||
# Check files exist immediately
|
||||
files_found = 0
|
||||
for filename in filenames:
|
||||
if os.path.exists(filename):
|
||||
size = os.path.getsize(filename)
|
||||
print(f"✓ File exists: {filename} ({size} bytes)")
|
||||
files_found += 1
|
||||
else:
|
||||
print(f"✗ File not found: {filename}")
|
||||
|
||||
if files_found == len(filenames):
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("FAIL")
|
||||
sys.exit(1)
|
||||
|
||||
print("Set callback")
|
||||
mcrfpy.setTimer("timer1", test_screenshot, 1000)
|
||||
# Run the test immediately
|
||||
#test_screenshot()
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Simple test for mcrfpy.automation.screenshot()"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Create a simple scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# Take a screenshot immediately
|
||||
try:
|
||||
filename = "test_screenshot.png"
|
||||
result = automation.screenshot(filename)
|
||||
print(f"Screenshot result: {result}")
|
||||
|
||||
# Check if file exists
|
||||
if os.path.exists(filename):
|
||||
size = os.path.getsize(filename)
|
||||
print(f"PASS - Screenshot saved: {filename} ({size} bytes)")
|
||||
else:
|
||||
print(f"FAIL - Screenshot file not created: {filename}")
|
||||
except Exception as e:
|
||||
print(f"FAIL - Screenshot error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Exit immediately
|
||||
sys.exit(0)
|
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Debug rendering to find why screenshots are transparent"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
# Check if we're in headless mode
|
||||
print("=== Debug Render Test ===")
|
||||
print(f"Module loaded: {mcrfpy}")
|
||||
print(f"Automation available: {'automation' in dir(mcrfpy)}")
|
||||
|
||||
# Try to understand the scene state
|
||||
print("\nCreating and checking scene...")
|
||||
mcrfpy.createScene("debug_scene")
|
||||
mcrfpy.setScene("debug_scene")
|
||||
current = mcrfpy.currentScene()
|
||||
print(f"Current scene: {current}")
|
||||
|
||||
# Get UI collection
|
||||
ui = mcrfpy.sceneUI("debug_scene")
|
||||
print(f"UI collection type: {type(ui)}")
|
||||
print(f"Initial UI elements: {len(ui)}")
|
||||
|
||||
# Add a simple frame
|
||||
frame = mcrfpy.Frame(0, 0, 100, 100,
|
||||
fill_color=mcrfpy.Color(255, 255, 255))
|
||||
ui.append(frame)
|
||||
print(f"After adding frame: {len(ui)} elements")
|
||||
|
||||
# Check if the issue is with timing
|
||||
print("\nTaking immediate screenshot...")
|
||||
result1 = automation.screenshot("debug_immediate.png")
|
||||
print(f"Immediate screenshot result: {result1}")
|
||||
|
||||
# Maybe we need to let the engine process the frame?
|
||||
# In headless mode with --exec, the game loop might not be running
|
||||
print("\nNote: In --exec mode, the game loop doesn't run continuously.")
|
||||
print("This might prevent rendering from occurring.")
|
||||
|
||||
# Let's also check what happens with multiple screenshots
|
||||
for i in range(3):
|
||||
result = automation.screenshot(f"debug_multi_{i}.png")
|
||||
print(f"Screenshot {i}: {result}")
|
||||
|
||||
print("\nConclusion: The issue appears to be that in --exec mode,")
|
||||
print("the render loop never runs, so nothing is drawn to the RenderTexture.")
|
||||
print("The screenshot captures an uninitialized/unrendered texture.")
|
||||
|
||||
sys.exit(0)
|
|
@ -0,0 +1,2 @@
|
|||
# This script is intentionally empty
|
||||
pass
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Test if calling mcrfpy.exit() prevents the >>> prompt"""
|
||||
import mcrfpy
|
||||
|
||||
print("Calling mcrfpy.exit() immediately...")
|
||||
mcrfpy.exit()
|
||||
print("This should not print if exit worked")
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Force Python to be non-interactive"""
|
||||
import sys
|
||||
import os
|
||||
|
||||
print("Attempting to force non-interactive mode...")
|
||||
|
||||
# Remove ps1/ps2 if they exist
|
||||
if hasattr(sys, 'ps1'):
|
||||
delattr(sys, 'ps1')
|
||||
if hasattr(sys, 'ps2'):
|
||||
delattr(sys, 'ps2')
|
||||
|
||||
# Set environment variable
|
||||
os.environ['PYTHONSTARTUP'] = ''
|
||||
|
||||
# Try to set stdin to non-interactive
|
||||
try:
|
||||
import fcntl
|
||||
import termios
|
||||
# Make stdin non-interactive by removing ICANON flag
|
||||
attrs = termios.tcgetattr(0)
|
||||
attrs[3] = attrs[3] & ~termios.ICANON
|
||||
termios.tcsetattr(0, termios.TCSANOW, attrs)
|
||||
print("Modified terminal attributes")
|
||||
except:
|
||||
print("Could not modify terminal attributes")
|
||||
|
||||
print("Script complete")
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate caption documentation screenshot with proper font"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def capture_caption(runtime):
|
||||
"""Capture caption example after render loop starts"""
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("mcrogueface.github.io/images/ui_caption_example.png")
|
||||
print("Caption screenshot saved!")
|
||||
|
||||
# Exit after capturing
|
||||
sys.exit(0)
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("captions")
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(400, 30, "Caption Examples")
|
||||
title.font = mcrfpy.default_font
|
||||
title.font_size = 28
|
||||
title.font_color = (255, 255, 255)
|
||||
|
||||
# Different sizes
|
||||
size_label = mcrfpy.Caption(100, 100, "Different Sizes:")
|
||||
size_label.font = mcrfpy.default_font
|
||||
size_label.font_color = (200, 200, 200)
|
||||
|
||||
large = mcrfpy.Caption(300, 100, "Large Text (24pt)")
|
||||
large.font = mcrfpy.default_font
|
||||
large.font_size = 24
|
||||
large.font_color = (255, 255, 255)
|
||||
|
||||
medium = mcrfpy.Caption(300, 140, "Medium Text (18pt)")
|
||||
medium.font = mcrfpy.default_font
|
||||
medium.font_size = 18
|
||||
medium.font_color = (255, 255, 255)
|
||||
|
||||
small = mcrfpy.Caption(300, 170, "Small Text (14pt)")
|
||||
small.font = mcrfpy.default_font
|
||||
small.font_size = 14
|
||||
small.font_color = (255, 255, 255)
|
||||
|
||||
# Different colors
|
||||
color_label = mcrfpy.Caption(100, 230, "Different Colors:")
|
||||
color_label.font = mcrfpy.default_font
|
||||
color_label.font_color = (200, 200, 200)
|
||||
|
||||
white_text = mcrfpy.Caption(300, 230, "White Text")
|
||||
white_text.font = mcrfpy.default_font
|
||||
white_text.font_color = (255, 255, 255)
|
||||
|
||||
green_text = mcrfpy.Caption(300, 260, "Green Text")
|
||||
green_text.font = mcrfpy.default_font
|
||||
green_text.font_color = (100, 255, 100)
|
||||
|
||||
red_text = mcrfpy.Caption(300, 290, "Red Text")
|
||||
red_text.font = mcrfpy.default_font
|
||||
red_text.font_color = (255, 100, 100)
|
||||
|
||||
blue_text = mcrfpy.Caption(300, 320, "Blue Text")
|
||||
blue_text.font = mcrfpy.default_font
|
||||
blue_text.font_color = (100, 150, 255)
|
||||
|
||||
# Caption with background
|
||||
bg_label = mcrfpy.Caption(100, 380, "With Background:")
|
||||
bg_label.font = mcrfpy.default_font
|
||||
bg_label.font_color = (200, 200, 200)
|
||||
|
||||
# Frame background
|
||||
frame = mcrfpy.Frame(280, 370, 250, 50)
|
||||
frame.bgcolor = (64, 64, 128)
|
||||
frame.outline = 2
|
||||
|
||||
framed_text = mcrfpy.Caption(405, 395, "Caption on Frame")
|
||||
framed_text.font = mcrfpy.default_font
|
||||
framed_text.font_size = 18
|
||||
framed_text.font_color = (255, 255, 255)
|
||||
framed_text.centered = True
|
||||
|
||||
# Centered text example
|
||||
center_label = mcrfpy.Caption(100, 460, "Centered Text:")
|
||||
center_label.font = mcrfpy.default_font
|
||||
center_label.font_color = (200, 200, 200)
|
||||
|
||||
centered = mcrfpy.Caption(400, 460, "This text is centered")
|
||||
centered.font = mcrfpy.default_font
|
||||
centered.font_size = 20
|
||||
centered.font_color = (255, 255, 100)
|
||||
centered.centered = True
|
||||
|
||||
# Multi-line example
|
||||
multi_label = mcrfpy.Caption(100, 520, "Multi-line:")
|
||||
multi_label.font = mcrfpy.default_font
|
||||
multi_label.font_color = (200, 200, 200)
|
||||
|
||||
multiline = mcrfpy.Caption(300, 520, "Line 1: McRogueFace\nLine 2: Game Engine\nLine 3: Python API")
|
||||
multiline.font = mcrfpy.default_font
|
||||
multiline.font_size = 14
|
||||
multiline.font_color = (255, 255, 255)
|
||||
|
||||
# Add all to scene
|
||||
ui = mcrfpy.sceneUI("captions")
|
||||
ui.append(title)
|
||||
ui.append(size_label)
|
||||
ui.append(large)
|
||||
ui.append(medium)
|
||||
ui.append(small)
|
||||
ui.append(color_label)
|
||||
ui.append(white_text)
|
||||
ui.append(green_text)
|
||||
ui.append(red_text)
|
||||
ui.append(blue_text)
|
||||
ui.append(bg_label)
|
||||
ui.append(frame)
|
||||
ui.append(framed_text)
|
||||
ui.append(center_label)
|
||||
ui.append(centered)
|
||||
ui.append(multi_label)
|
||||
ui.append(multiline)
|
||||
|
||||
# Switch to scene
|
||||
mcrfpy.setScene("captions")
|
||||
|
||||
# Set timer to capture after rendering starts
|
||||
mcrfpy.setTimer("capture", capture_caption, 100)
|
|
@ -0,0 +1,451 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate documentation screenshots for McRogueFace UI elements"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Crypt of Sokoban color scheme
|
||||
FRAME_COLOR = mcrfpy.Color(64, 64, 128)
|
||||
SHADOW_COLOR = mcrfpy.Color(64, 64, 86)
|
||||
BOX_COLOR = mcrfpy.Color(96, 96, 160)
|
||||
WHITE = mcrfpy.Color(255, 255, 255)
|
||||
BLACK = mcrfpy.Color(0, 0, 0)
|
||||
GREEN = mcrfpy.Color(0, 255, 0)
|
||||
RED = mcrfpy.Color(255, 0, 0)
|
||||
|
||||
# Create texture for sprites
|
||||
sprite_texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
|
||||
# Output directory - create it during setup
|
||||
output_dir = "mcrogueface.github.io/images"
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
def create_caption(x, y, text, font_size=16, text_color=WHITE, outline_color=BLACK):
|
||||
"""Helper function to create captions with common settings"""
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(x, y), text=text)
|
||||
caption.size = font_size
|
||||
caption.fill_color = text_color
|
||||
caption.outline_color = outline_color
|
||||
return caption
|
||||
|
||||
def create_caption_example():
|
||||
"""Create a scene showing Caption UI element examples"""
|
||||
mcrfpy.createScene("caption_example")
|
||||
ui = mcrfpy.sceneUI("caption_example")
|
||||
|
||||
# Background frame
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
# Title caption
|
||||
title = create_caption(200, 50, "Caption Examples", 32)
|
||||
ui.append(title)
|
||||
|
||||
# Different sized captions
|
||||
caption1 = create_caption(100, 150, "Large Caption (24pt)", 24)
|
||||
ui.append(caption1)
|
||||
|
||||
caption2 = create_caption(100, 200, "Medium Caption (18pt)", 18, GREEN)
|
||||
ui.append(caption2)
|
||||
|
||||
caption3 = create_caption(100, 240, "Small Caption (14pt)", 14, RED)
|
||||
ui.append(caption3)
|
||||
|
||||
# Caption with background
|
||||
caption_bg = mcrfpy.Frame(100, 300, 300, 50, fill_color=BOX_COLOR)
|
||||
ui.append(caption_bg)
|
||||
caption4 = create_caption(110, 315, "Caption with Background", 16)
|
||||
ui.append(caption4)
|
||||
|
||||
def create_sprite_example():
|
||||
"""Create a scene showing Sprite UI element examples"""
|
||||
mcrfpy.createScene("sprite_example")
|
||||
ui = mcrfpy.sceneUI("sprite_example")
|
||||
|
||||
# Background frame
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = create_caption(250, 50, "Sprite Examples", 32)
|
||||
ui.append(title)
|
||||
|
||||
# Create a grid background for sprites
|
||||
sprite_bg = mcrfpy.Frame(100, 150, 600, 300, fill_color=BOX_COLOR)
|
||||
ui.append(sprite_bg)
|
||||
|
||||
# Player sprite (84)
|
||||
player_label = create_caption(150, 180, "Player", 14)
|
||||
ui.append(player_label)
|
||||
player_sprite = mcrfpy.Sprite(150, 200, sprite_texture, 84, 3.0)
|
||||
ui.append(player_sprite)
|
||||
|
||||
# Enemy sprites
|
||||
enemy_label = create_caption(250, 180, "Enemies", 14)
|
||||
ui.append(enemy_label)
|
||||
enemy1 = mcrfpy.Sprite(250, 200, sprite_texture, 123, 3.0) # Basic enemy
|
||||
ui.append(enemy1)
|
||||
enemy2 = mcrfpy.Sprite(300, 200, sprite_texture, 107, 3.0) # Different enemy
|
||||
ui.append(enemy2)
|
||||
|
||||
# Boulder sprite (66)
|
||||
boulder_label = create_caption(400, 180, "Boulder", 14)
|
||||
ui.append(boulder_label)
|
||||
boulder_sprite = mcrfpy.Sprite(400, 200, sprite_texture, 66, 3.0)
|
||||
ui.append(boulder_sprite)
|
||||
|
||||
# Exit sprites
|
||||
exit_label = create_caption(500, 180, "Exit States", 14)
|
||||
ui.append(exit_label)
|
||||
exit_locked = mcrfpy.Sprite(500, 200, sprite_texture, 45, 3.0) # Locked
|
||||
ui.append(exit_locked)
|
||||
exit_open = mcrfpy.Sprite(550, 200, sprite_texture, 21, 3.0) # Open
|
||||
ui.append(exit_open)
|
||||
|
||||
# Item sprites
|
||||
item_label = create_caption(150, 300, "Items", 14)
|
||||
ui.append(item_label)
|
||||
treasure = mcrfpy.Sprite(150, 320, sprite_texture, 89, 3.0) # Treasure
|
||||
ui.append(treasure)
|
||||
sword = mcrfpy.Sprite(200, 320, sprite_texture, 222, 3.0) # Sword
|
||||
ui.append(sword)
|
||||
potion = mcrfpy.Sprite(250, 320, sprite_texture, 175, 3.0) # Potion
|
||||
ui.append(potion)
|
||||
|
||||
# Button sprite
|
||||
button_label = create_caption(350, 300, "Button", 14)
|
||||
ui.append(button_label)
|
||||
button = mcrfpy.Sprite(350, 320, sprite_texture, 250, 3.0)
|
||||
ui.append(button)
|
||||
|
||||
def create_frame_example():
|
||||
"""Create a scene showing Frame UI element examples"""
|
||||
mcrfpy.createScene("frame_example")
|
||||
ui = mcrfpy.sceneUI("frame_example")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = create_caption(250, 30, "Frame Examples", 32)
|
||||
ui.append(title)
|
||||
|
||||
# Basic frame
|
||||
frame1 = mcrfpy.Frame(50, 100, 200, 150, fill_color=FRAME_COLOR)
|
||||
ui.append(frame1)
|
||||
label1 = create_caption(60, 110, "Basic Frame", 16)
|
||||
ui.append(label1)
|
||||
|
||||
# Frame with outline
|
||||
frame2 = mcrfpy.Frame(300, 100, 200, 150, fill_color=BOX_COLOR,
|
||||
outline_color=WHITE, outline=2.0)
|
||||
ui.append(frame2)
|
||||
label2 = create_caption(310, 110, "Frame with Outline", 16)
|
||||
ui.append(label2)
|
||||
|
||||
# Nested frames
|
||||
frame3 = mcrfpy.Frame(550, 100, 200, 150, fill_color=FRAME_COLOR,
|
||||
outline_color=WHITE, outline=1)
|
||||
ui.append(frame3)
|
||||
inner_frame = mcrfpy.Frame(570, 130, 160, 90, fill_color=BOX_COLOR)
|
||||
ui.append(inner_frame)
|
||||
label3 = create_caption(560, 110, "Nested Frames", 16)
|
||||
ui.append(label3)
|
||||
|
||||
# Complex layout with frames
|
||||
main_frame = mcrfpy.Frame(50, 300, 700, 250, fill_color=FRAME_COLOR,
|
||||
outline_color=WHITE, outline=2)
|
||||
ui.append(main_frame)
|
||||
|
||||
# Add some UI elements inside
|
||||
ui_label = create_caption(60, 310, "Complex UI Layout", 18)
|
||||
ui.append(ui_label)
|
||||
|
||||
# Status panel
|
||||
status_frame = mcrfpy.Frame(70, 350, 150, 180, fill_color=BOX_COLOR)
|
||||
ui.append(status_frame)
|
||||
status_label = create_caption(80, 360, "Status", 14)
|
||||
ui.append(status_label)
|
||||
|
||||
# Inventory panel
|
||||
inv_frame = mcrfpy.Frame(240, 350, 300, 180, fill_color=BOX_COLOR)
|
||||
ui.append(inv_frame)
|
||||
inv_label = create_caption(250, 360, "Inventory", 14)
|
||||
ui.append(inv_label)
|
||||
|
||||
# Actions panel
|
||||
action_frame = mcrfpy.Frame(560, 350, 170, 180, fill_color=BOX_COLOR)
|
||||
ui.append(action_frame)
|
||||
action_label = create_caption(570, 360, "Actions", 14)
|
||||
ui.append(action_label)
|
||||
|
||||
def create_grid_example():
|
||||
"""Create a scene showing Grid UI element examples"""
|
||||
mcrfpy.createScene("grid_example")
|
||||
ui = mcrfpy.sceneUI("grid_example")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = create_caption(250, 30, "Grid Example", 32)
|
||||
ui.append(title)
|
||||
|
||||
# Create a grid showing a small dungeon
|
||||
grid = mcrfpy.Grid(20, 15, sprite_texture,
|
||||
mcrfpy.Vector(100, 100), mcrfpy.Vector(320, 240))
|
||||
|
||||
# Set up dungeon tiles
|
||||
# Floor tiles (index 48)
|
||||
# Wall tiles (index 3)
|
||||
for x in range(20):
|
||||
for y in range(15):
|
||||
if x == 0 or x == 19 or y == 0 or y == 14:
|
||||
# Walls around edge
|
||||
grid.at((x, y)).tilesprite = 3
|
||||
grid.at((x, y)).walkable = False
|
||||
else:
|
||||
# Floor
|
||||
grid.at((x, y)).tilesprite = 48
|
||||
grid.at((x, y)).walkable = True
|
||||
|
||||
# Add some internal walls
|
||||
for x in range(5, 15):
|
||||
grid.at((x, 7)).tilesprite = 3
|
||||
grid.at((x, 7)).walkable = False
|
||||
for y in range(3, 8):
|
||||
grid.at((10, y)).tilesprite = 3
|
||||
grid.at((10, y)).walkable = False
|
||||
|
||||
# Add a door
|
||||
grid.at((10, 7)).tilesprite = 131 # Door tile
|
||||
grid.at((10, 7)).walkable = True
|
||||
|
||||
# Add to UI
|
||||
ui.append(grid)
|
||||
|
||||
# Label
|
||||
grid_label = create_caption(100, 480, "20x15 Grid with 2x scale - Simple Dungeon Layout", 16)
|
||||
ui.append(grid_label)
|
||||
|
||||
def create_entity_example():
|
||||
"""Create a scene showing Entity examples in a Grid"""
|
||||
mcrfpy.createScene("entity_example")
|
||||
ui = mcrfpy.sceneUI("entity_example")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = create_caption(200, 30, "Entity Collection Example", 32)
|
||||
ui.append(title)
|
||||
|
||||
# Create a grid for the entities
|
||||
grid = mcrfpy.Grid(15, 10, sprite_texture,
|
||||
mcrfpy.Vector(150, 100), mcrfpy.Vector(360, 240))
|
||||
|
||||
# Set all tiles to floor
|
||||
for x in range(15):
|
||||
for y in range(10):
|
||||
grid.at((x, y)).tilesprite = 48
|
||||
grid.at((x, y)).walkable = True
|
||||
|
||||
# Add walls
|
||||
for x in range(15):
|
||||
grid.at((x, 0)).tilesprite = 3
|
||||
grid.at((x, 0)).walkable = False
|
||||
grid.at((x, 9)).tilesprite = 3
|
||||
grid.at((x, 9)).walkable = False
|
||||
for y in range(10):
|
||||
grid.at((0, y)).tilesprite = 3
|
||||
grid.at((0, y)).walkable = False
|
||||
grid.at((14, y)).tilesprite = 3
|
||||
grid.at((14, y)).walkable = False
|
||||
|
||||
ui.append(grid)
|
||||
|
||||
# Add entities to the grid
|
||||
# Player entity
|
||||
player = mcrfpy.Entity(mcrfpy.Vector(3, 3), sprite_texture, 84, grid)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Enemy entities
|
||||
enemy1 = mcrfpy.Entity(mcrfpy.Vector(7, 4), sprite_texture, 123, grid)
|
||||
grid.entities.append(enemy1)
|
||||
|
||||
enemy2 = mcrfpy.Entity(mcrfpy.Vector(10, 6), sprite_texture, 107, grid)
|
||||
grid.entities.append(enemy2)
|
||||
|
||||
# Boulder
|
||||
boulder = mcrfpy.Entity(mcrfpy.Vector(5, 5), sprite_texture, 66, grid)
|
||||
grid.entities.append(boulder)
|
||||
|
||||
# Treasure
|
||||
treasure = mcrfpy.Entity(mcrfpy.Vector(12, 2), sprite_texture, 89, grid)
|
||||
grid.entities.append(treasure)
|
||||
|
||||
# Exit (locked)
|
||||
exit_door = mcrfpy.Entity(mcrfpy.Vector(12, 8), sprite_texture, 45, grid)
|
||||
grid.entities.append(exit_door)
|
||||
|
||||
# Button
|
||||
button = mcrfpy.Entity(mcrfpy.Vector(3, 7), sprite_texture, 250, grid)
|
||||
grid.entities.append(button)
|
||||
|
||||
# Items
|
||||
sword = mcrfpy.Entity(mcrfpy.Vector(8, 2), sprite_texture, 222, grid)
|
||||
grid.entities.append(sword)
|
||||
|
||||
potion = mcrfpy.Entity(mcrfpy.Vector(6, 8), sprite_texture, 175, grid)
|
||||
grid.entities.append(potion)
|
||||
|
||||
# Label
|
||||
entity_label = create_caption(150, 500, "Grid with Entity Collection - Game Objects", 16)
|
||||
ui.append(entity_label)
|
||||
|
||||
def create_combined_example():
|
||||
"""Create a scene showing all UI elements combined"""
|
||||
mcrfpy.createScene("combined_example")
|
||||
ui = mcrfpy.sceneUI("combined_example")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = create_caption(200, 20, "McRogueFace UI Elements", 28)
|
||||
ui.append(title)
|
||||
|
||||
# Main game area frame
|
||||
game_frame = mcrfpy.Frame(20, 70, 500, 400, fill_color=FRAME_COLOR,
|
||||
outline_color=WHITE, outline=2)
|
||||
ui.append(game_frame)
|
||||
|
||||
# Grid inside game frame
|
||||
grid = mcrfpy.Grid(12, 10, sprite_texture,
|
||||
mcrfpy.Vector(30, 80), mcrfpy.Vector(480, 400))
|
||||
for x in range(12):
|
||||
for y in range(10):
|
||||
if x == 0 or x == 11 or y == 0 or y == 9:
|
||||
grid.at((x, y)).tilesprite = 3
|
||||
grid.at((x, y)).walkable = False
|
||||
else:
|
||||
grid.at((x, y)).tilesprite = 48
|
||||
grid.at((x, y)).walkable = True
|
||||
|
||||
# Add some entities
|
||||
player = mcrfpy.Entity(mcrfpy.Vector(2, 2), sprite_texture, 84, grid)
|
||||
grid.entities.append(player)
|
||||
enemy = mcrfpy.Entity(mcrfpy.Vector(8, 6), sprite_texture, 123, grid)
|
||||
grid.entities.append(enemy)
|
||||
boulder = mcrfpy.Entity(mcrfpy.Vector(5, 4), sprite_texture, 66, grid)
|
||||
grid.entities.append(boulder)
|
||||
|
||||
ui.append(grid)
|
||||
|
||||
# Status panel
|
||||
status_frame = mcrfpy.Frame(540, 70, 240, 200, fill_color=BOX_COLOR,
|
||||
outline_color=WHITE, outline=1)
|
||||
ui.append(status_frame)
|
||||
|
||||
status_title = create_caption(550, 80, "Status", 20)
|
||||
ui.append(status_title)
|
||||
|
||||
hp_label = create_caption(550, 120, "HP: 10/10", 16, GREEN)
|
||||
ui.append(hp_label)
|
||||
|
||||
level_label = create_caption(550, 150, "Level: 1", 16)
|
||||
ui.append(level_label)
|
||||
|
||||
# Inventory panel
|
||||
inv_frame = mcrfpy.Frame(540, 290, 240, 180, fill_color=BOX_COLOR,
|
||||
outline_color=WHITE, outline=1)
|
||||
ui.append(inv_frame)
|
||||
|
||||
inv_title = create_caption(550, 300, "Inventory", 20)
|
||||
ui.append(inv_title)
|
||||
|
||||
# Add some item sprites
|
||||
item1 = mcrfpy.Sprite(560, 340, sprite_texture, 222, 2.0)
|
||||
ui.append(item1)
|
||||
item2 = mcrfpy.Sprite(610, 340, sprite_texture, 175, 2.0)
|
||||
ui.append(item2)
|
||||
|
||||
# Message log
|
||||
log_frame = mcrfpy.Frame(20, 490, 760, 90, fill_color=BOX_COLOR,
|
||||
outline_color=WHITE, outline=1)
|
||||
ui.append(log_frame)
|
||||
|
||||
log_msg = create_caption(30, 500, "Welcome to McRogueFace!", 14)
|
||||
ui.append(log_msg)
|
||||
|
||||
# Set up all the scenes
|
||||
print("Creating UI example scenes...")
|
||||
create_caption_example()
|
||||
create_sprite_example()
|
||||
create_frame_example()
|
||||
create_grid_example()
|
||||
create_entity_example()
|
||||
create_combined_example()
|
||||
|
||||
# Screenshot state
|
||||
current_screenshot = 0
|
||||
screenshots = [
|
||||
("caption_example", "ui_caption_example.png"),
|
||||
("sprite_example", "ui_sprite_example.png"),
|
||||
("frame_example", "ui_frame_example.png"),
|
||||
("grid_example", "ui_grid_example.png"),
|
||||
("entity_example", "ui_entity_example.png"),
|
||||
("combined_example", "ui_combined_example.png")
|
||||
]
|
||||
|
||||
def take_screenshots(runtime):
|
||||
"""Timer callback to take screenshots sequentially"""
|
||||
global current_screenshot
|
||||
|
||||
if current_screenshot >= len(screenshots):
|
||||
print("\nAll screenshots captured successfully!")
|
||||
print(f"Screenshots saved to: {output_dir}/")
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
scene_name, filename = screenshots[current_screenshot]
|
||||
|
||||
# Switch to the scene
|
||||
mcrfpy.setScene(scene_name)
|
||||
|
||||
# Take screenshot after a short delay to ensure rendering
|
||||
def capture():
|
||||
global current_screenshot
|
||||
full_path = f"{output_dir}/{filename}"
|
||||
result = automation.screenshot(full_path)
|
||||
print(f"Screenshot {current_screenshot + 1}/{len(screenshots)}: {filename} - {'Success' if result else 'Failed'}")
|
||||
|
||||
current_screenshot += 1
|
||||
|
||||
# Schedule next screenshot
|
||||
mcrfpy.setTimer("next_screenshot", take_screenshots, 200)
|
||||
|
||||
# Give scene time to render
|
||||
mcrfpy.setTimer("capture", lambda r: capture(), 100)
|
||||
|
||||
# Start with the first scene
|
||||
mcrfpy.setScene("caption_example")
|
||||
|
||||
# Start the screenshot process
|
||||
print(f"\nStarting screenshot capture of {len(screenshots)} scenes...")
|
||||
mcrfpy.setTimer("start", take_screenshots, 500)
|
||||
|
||||
# Safety timeout
|
||||
def safety_exit(runtime):
|
||||
print("\nERROR: Safety timeout reached! Exiting...")
|
||||
mcrfpy.exit()
|
||||
|
||||
mcrfpy.setTimer("safety", safety_exit, 30000)
|
||||
|
||||
print("Setup complete. Game loop starting...")
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate documentation screenshots for McRogueFace UI elements - Simple version"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Crypt of Sokoban color scheme
|
||||
FRAME_COLOR = mcrfpy.Color(64, 64, 128)
|
||||
SHADOW_COLOR = mcrfpy.Color(64, 64, 86)
|
||||
BOX_COLOR = mcrfpy.Color(96, 96, 160)
|
||||
WHITE = mcrfpy.Color(255, 255, 255)
|
||||
BLACK = mcrfpy.Color(0, 0, 0)
|
||||
GREEN = mcrfpy.Color(0, 255, 0)
|
||||
RED = mcrfpy.Color(255, 0, 0)
|
||||
|
||||
# Create texture for sprites
|
||||
sprite_texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
|
||||
# Output directory
|
||||
output_dir = "mcrogueface.github.io/images"
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
def create_caption(x, y, text, font_size=16, text_color=WHITE, outline_color=BLACK):
|
||||
"""Helper function to create captions with common settings"""
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(x, y), text=text)
|
||||
caption.size = font_size
|
||||
caption.fill_color = text_color
|
||||
caption.outline_color = outline_color
|
||||
return caption
|
||||
|
||||
# Screenshot counter
|
||||
screenshot_count = 0
|
||||
total_screenshots = 4
|
||||
|
||||
def screenshot_and_continue(runtime):
|
||||
"""Take a screenshot and move to the next scene"""
|
||||
global screenshot_count
|
||||
|
||||
if screenshot_count == 0:
|
||||
# Caption example
|
||||
print("Creating Caption example...")
|
||||
mcrfpy.createScene("caption_example")
|
||||
ui = mcrfpy.sceneUI("caption_example")
|
||||
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
title = create_caption(200, 50, "Caption Examples", 32)
|
||||
ui.append(title)
|
||||
|
||||
caption1 = create_caption(100, 150, "Large Caption (24pt)", 24)
|
||||
ui.append(caption1)
|
||||
|
||||
caption2 = create_caption(100, 200, "Medium Caption (18pt)", 18, GREEN)
|
||||
ui.append(caption2)
|
||||
|
||||
caption3 = create_caption(100, 240, "Small Caption (14pt)", 14, RED)
|
||||
ui.append(caption3)
|
||||
|
||||
caption_bg = mcrfpy.Frame(100, 300, 300, 50, fill_color=BOX_COLOR)
|
||||
ui.append(caption_bg)
|
||||
caption4 = create_caption(110, 315, "Caption with Background", 16)
|
||||
ui.append(caption4)
|
||||
|
||||
mcrfpy.setScene("caption_example")
|
||||
mcrfpy.setTimer("next1", lambda r: capture_screenshot("ui_caption_example.png"), 200)
|
||||
|
||||
elif screenshot_count == 1:
|
||||
# Sprite example
|
||||
print("Creating Sprite example...")
|
||||
mcrfpy.createScene("sprite_example")
|
||||
ui = mcrfpy.sceneUI("sprite_example")
|
||||
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
title = create_caption(250, 50, "Sprite Examples", 32)
|
||||
ui.append(title)
|
||||
|
||||
sprite_bg = mcrfpy.Frame(100, 150, 600, 300, fill_color=BOX_COLOR)
|
||||
ui.append(sprite_bg)
|
||||
|
||||
player_label = create_caption(150, 180, "Player", 14)
|
||||
ui.append(player_label)
|
||||
player_sprite = mcrfpy.Sprite(150, 200, sprite_texture, 84, 3.0)
|
||||
ui.append(player_sprite)
|
||||
|
||||
enemy_label = create_caption(250, 180, "Enemies", 14)
|
||||
ui.append(enemy_label)
|
||||
enemy1 = mcrfpy.Sprite(250, 200, sprite_texture, 123, 3.0)
|
||||
ui.append(enemy1)
|
||||
enemy2 = mcrfpy.Sprite(300, 200, sprite_texture, 107, 3.0)
|
||||
ui.append(enemy2)
|
||||
|
||||
boulder_label = create_caption(400, 180, "Boulder", 14)
|
||||
ui.append(boulder_label)
|
||||
boulder_sprite = mcrfpy.Sprite(400, 200, sprite_texture, 66, 3.0)
|
||||
ui.append(boulder_sprite)
|
||||
|
||||
exit_label = create_caption(500, 180, "Exit States", 14)
|
||||
ui.append(exit_label)
|
||||
exit_locked = mcrfpy.Sprite(500, 200, sprite_texture, 45, 3.0)
|
||||
ui.append(exit_locked)
|
||||
exit_open = mcrfpy.Sprite(550, 200, sprite_texture, 21, 3.0)
|
||||
ui.append(exit_open)
|
||||
|
||||
mcrfpy.setScene("sprite_example")
|
||||
mcrfpy.setTimer("next2", lambda r: capture_screenshot("ui_sprite_example.png"), 200)
|
||||
|
||||
elif screenshot_count == 2:
|
||||
# Frame example
|
||||
print("Creating Frame example...")
|
||||
mcrfpy.createScene("frame_example")
|
||||
ui = mcrfpy.sceneUI("frame_example")
|
||||
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=SHADOW_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
title = create_caption(250, 30, "Frame Examples", 32)
|
||||
ui.append(title)
|
||||
|
||||
frame1 = mcrfpy.Frame(50, 100, 200, 150, fill_color=FRAME_COLOR)
|
||||
ui.append(frame1)
|
||||
label1 = create_caption(60, 110, "Basic Frame", 16)
|
||||
ui.append(label1)
|
||||
|
||||
frame2 = mcrfpy.Frame(300, 100, 200, 150, fill_color=BOX_COLOR,
|
||||
outline_color=WHITE, outline=2.0)
|
||||
ui.append(frame2)
|
||||
label2 = create_caption(310, 110, "Frame with Outline", 16)
|
||||
ui.append(label2)
|
||||
|
||||
frame3 = mcrfpy.Frame(550, 100, 200, 150, fill_color=FRAME_COLOR,
|
||||
outline_color=WHITE, outline=1)
|
||||
ui.append(frame3)
|
||||
inner_frame = mcrfpy.Frame(570, 130, 160, 90, fill_color=BOX_COLOR)
|
||||
ui.append(inner_frame)
|
||||
label3 = create_caption(560, 110, "Nested Frames", 16)
|
||||
ui.append(label3)
|
||||
|
||||
mcrfpy.setScene("frame_example")
|
||||
mcrfpy.setTimer("next3", lambda r: capture_screenshot("ui_frame_example.png"), 200)
|
||||
|
||||
elif screenshot_count == 3:
|
||||
# Grid example
|
||||
print("Creating Grid example...")
|
||||
mcrfpy.createScene("grid_example")
|
||||
ui = mcrfpy.sceneUI("grid_example")
|
||||
|
||||
bg = mcrfpy.Frame(0, 0, 800, 600, fill_color=FRAME_COLOR)
|
||||
ui.append(bg)
|
||||
|
||||
title = create_caption(250, 30, "Grid Example", 32)
|
||||
ui.append(title)
|
||||
|
||||
grid = mcrfpy.Grid(20, 15, sprite_texture,
|
||||
mcrfpy.Vector(100, 100), mcrfpy.Vector(320, 240))
|
||||
|
||||
# Set up dungeon tiles
|
||||
for x in range(20):
|
||||
for y in range(15):
|
||||
if x == 0 or x == 19 or y == 0 or y == 14:
|
||||
# Walls
|
||||
grid.at((x, y)).tilesprite = 3
|
||||
grid.at((x, y)).walkable = False
|
||||
else:
|
||||
# Floor
|
||||
grid.at((x, y)).tilesprite = 48
|
||||
grid.at((x, y)).walkable = True
|
||||
|
||||
# Add some internal walls
|
||||
for x in range(5, 15):
|
||||
grid.at((x, 7)).tilesprite = 3
|
||||
grid.at((x, 7)).walkable = False
|
||||
for y in range(3, 8):
|
||||
grid.at((10, y)).tilesprite = 3
|
||||
grid.at((10, y)).walkable = False
|
||||
|
||||
# Add a door
|
||||
grid.at((10, 7)).tilesprite = 131
|
||||
grid.at((10, 7)).walkable = True
|
||||
|
||||
ui.append(grid)
|
||||
|
||||
grid_label = create_caption(100, 480, "20x15 Grid - Simple Dungeon Layout", 16)
|
||||
ui.append(grid_label)
|
||||
|
||||
mcrfpy.setScene("grid_example")
|
||||
mcrfpy.setTimer("next4", lambda r: capture_screenshot("ui_grid_example.png"), 200)
|
||||
|
||||
else:
|
||||
print("\nAll screenshots captured successfully!")
|
||||
print(f"Screenshots saved to: {output_dir}/")
|
||||
mcrfpy.exit()
|
||||
return
|
||||
|
||||
def capture_screenshot(filename):
|
||||
"""Capture a screenshot"""
|
||||
global screenshot_count
|
||||
full_path = f"{output_dir}/{filename}"
|
||||
result = automation.screenshot(full_path)
|
||||
print(f"Screenshot {screenshot_count + 1}/{total_screenshots}: {filename} - {'Success' if result else 'Failed'}")
|
||||
screenshot_count += 1
|
||||
|
||||
# Schedule next scene
|
||||
mcrfpy.setTimer("continue", screenshot_and_continue, 300)
|
||||
|
||||
# Start the process
|
||||
print("Starting screenshot generation...")
|
||||
mcrfpy.setTimer("start", screenshot_and_continue, 500)
|
||||
|
||||
# Safety timeout
|
||||
mcrfpy.setTimer("safety", lambda r: mcrfpy.exit(), 30000)
|
||||
|
||||
print("Setup complete. Game loop starting...")
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate entity documentation screenshot with proper font loading"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def capture_entity(runtime):
|
||||
"""Capture entity example after render loop starts"""
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("mcrogueface.github.io/images/ui_entity_example.png")
|
||||
print("Entity screenshot saved!")
|
||||
|
||||
# Exit after capturing
|
||||
sys.exit(0)
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("entities")
|
||||
|
||||
# Use the default font which is already loaded
|
||||
# Instead of: font = mcrfpy.Font("assets/JetbrainsMono.ttf")
|
||||
# We use: mcrfpy.default_font (which is already loaded by the engine)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption((400, 30), "Entity Example - Roguelike Characters", font=mcrfpy.default_font)
|
||||
#title.font = mcrfpy.default_font
|
||||
#title.font_size = 24
|
||||
title.size=24
|
||||
#title.font_color = (255, 255, 255)
|
||||
#title.text_color = (255,255,255)
|
||||
|
||||
# Create a grid background
|
||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
|
||||
# Create grid with entities - using 2x scale (32x32 pixel tiles)
|
||||
#grid = mcrfpy.Grid((100, 100), (20, 15), texture, 16, 16) # I can never get the args right for this thing
|
||||
t = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
grid = mcrfpy.Grid(20, 15, t, (10, 10), (1014, 758))
|
||||
grid.zoom = 2.0
|
||||
#grid.texture = texture
|
||||
|
||||
# Define tile types
|
||||
FLOOR = 58 # Stone floor
|
||||
WALL = 11 # Stone wall
|
||||
|
||||
# Fill with floor
|
||||
for x in range(20):
|
||||
for y in range(15):
|
||||
grid.at((x, y)).tilesprite = WALL
|
||||
|
||||
# Add walls around edges
|
||||
for x in range(20):
|
||||
grid.at((x, 0)).tilesprite = WALL
|
||||
grid.at((x, 14)).tilesprite = WALL
|
||||
for y in range(15):
|
||||
grid.at((0, y)).tilesprite = WALL
|
||||
grid.at((19, y)).tilesprite = WALL
|
||||
|
||||
# Create entities
|
||||
# Player at center
|
||||
player = mcrfpy.Entity((10, 7), t, 84)
|
||||
#player.texture = texture
|
||||
#player.sprite_index = 84 # Player sprite
|
||||
|
||||
# Enemies
|
||||
rat1 = mcrfpy.Entity((5, 5), t, 123)
|
||||
#rat1.texture = texture
|
||||
#rat1.sprite_index = 123 # Rat
|
||||
|
||||
rat2 = mcrfpy.Entity((15, 5), t, 123)
|
||||
#rat2.texture = texture
|
||||
#rat2.sprite_index = 123 # Rat
|
||||
|
||||
big_rat = mcrfpy.Entity((7, 10), t, 130)
|
||||
#big_rat.texture = texture
|
||||
#big_rat.sprite_index = 130 # Big rat
|
||||
|
||||
cyclops = mcrfpy.Entity((13, 10), t, 109)
|
||||
#cyclops.texture = texture
|
||||
#cyclops.sprite_index = 109 # Cyclops
|
||||
|
||||
# Items
|
||||
chest = mcrfpy.Entity((3, 3), t, 89)
|
||||
#chest.texture = texture
|
||||
#chest.sprite_index = 89 # Chest
|
||||
|
||||
boulder = mcrfpy.Entity((10, 5), t, 66)
|
||||
#boulder.texture = texture
|
||||
#boulder.sprite_index = 66 # Boulder
|
||||
key = mcrfpy.Entity((17, 12), t, 384)
|
||||
#key.texture = texture
|
||||
#key.sprite_index = 384 # Key
|
||||
|
||||
# Add all entities to grid
|
||||
grid.entities.append(player)
|
||||
grid.entities.append(rat1)
|
||||
grid.entities.append(rat2)
|
||||
grid.entities.append(big_rat)
|
||||
grid.entities.append(cyclops)
|
||||
grid.entities.append(chest)
|
||||
grid.entities.append(boulder)
|
||||
grid.entities.append(key)
|
||||
|
||||
# Labels
|
||||
entity_label = mcrfpy.Caption((100, 580), "Entities move independently on the grid. Grid scale: 2x (32x32 pixels)")
|
||||
#entity_label.font = mcrfpy.default_font
|
||||
#entity_label.font_color = (255, 255, 255)
|
||||
|
||||
info = mcrfpy.Caption((100, 600), "Player (center), Enemies (rats, cyclops), Items (chest, boulder, key)")
|
||||
#info.font = mcrfpy.default_font
|
||||
#info.font_size = 14
|
||||
#info.font_color = (200, 200, 200)
|
||||
|
||||
# Legend frame
|
||||
legend_frame = mcrfpy.Frame(50, 50, 200, 150)
|
||||
#legend_frame.bgcolor = (64, 64, 128)
|
||||
#legend_frame.outline = 2
|
||||
|
||||
legend_title = mcrfpy.Caption((150, 60), "Entity Types")
|
||||
#legend_title.font = mcrfpy.default_font
|
||||
#legend_title.font_color = (255, 255, 255)
|
||||
#legend_title.centered = True
|
||||
|
||||
#legend_text = mcrfpy.Caption((60, 90), "Player: @\nRat: r\nBig Rat: R\nCyclops: C\nChest: $\nBoulder: O\nKey: k")
|
||||
#legend_text.font = mcrfpy.default_font
|
||||
#legend_text.font_size = 12
|
||||
#legend_text.font_color = (255, 255, 255)
|
||||
|
||||
# Add all to scene
|
||||
ui = mcrfpy.sceneUI("entities")
|
||||
ui.append(grid)
|
||||
ui.append(title)
|
||||
ui.append(entity_label)
|
||||
ui.append(info)
|
||||
ui.append(legend_frame)
|
||||
ui.append(legend_title)
|
||||
#ui.append(legend_text)
|
||||
|
||||
# Switch to scene
|
||||
mcrfpy.setScene("entities")
|
||||
|
||||
# Set timer to capture after rendering starts
|
||||
mcrfpy.setTimer("capture", capture_entity, 100)
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate grid documentation screenshot for McRogueFace"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def capture_grid(runtime):
|
||||
"""Capture grid example after render loop starts"""
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("mcrogueface.github.io/images/ui_grid_example.png")
|
||||
print("Grid screenshot saved!")
|
||||
|
||||
# Exit after capturing
|
||||
sys.exit(0)
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("grid")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(400, 30, "Grid Example - Dungeon View")
|
||||
title.font = mcrfpy.default_font
|
||||
title.font_size = 24
|
||||
title.font_color = (255, 255, 255)
|
||||
|
||||
# Create main grid (20x15 tiles, each 32x32 pixels)
|
||||
grid = mcrfpy.Grid(100, 100, 20, 15, texture, 32, 32)
|
||||
grid.texture = texture
|
||||
|
||||
# Define tile types from Crypt of Sokoban
|
||||
FLOOR = 58 # Stone floor
|
||||
WALL = 11 # Stone wall
|
||||
DOOR = 28 # Closed door
|
||||
CHEST = 89 # Treasure chest
|
||||
BUTTON = 250 # Floor button
|
||||
EXIT = 45 # Locked exit
|
||||
BOULDER = 66 # Boulder
|
||||
|
||||
# Create a simple dungeon room layout
|
||||
# Fill with walls first
|
||||
for x in range(20):
|
||||
for y in range(15):
|
||||
grid.set_tile(x, y, WALL)
|
||||
|
||||
# Carve out room
|
||||
for x in range(2, 18):
|
||||
for y in range(2, 13):
|
||||
grid.set_tile(x, y, FLOOR)
|
||||
|
||||
# Add door
|
||||
grid.set_tile(10, 2, DOOR)
|
||||
|
||||
# Add some features
|
||||
grid.set_tile(5, 5, CHEST)
|
||||
grid.set_tile(15, 10, BUTTON)
|
||||
grid.set_tile(10, 12, EXIT)
|
||||
grid.set_tile(8, 8, BOULDER)
|
||||
grid.set_tile(12, 8, BOULDER)
|
||||
|
||||
# Create some entities on the grid
|
||||
# Player entity
|
||||
player = mcrfpy.Entity(5, 7)
|
||||
player.texture = texture
|
||||
player.sprite_index = 84 # Player sprite
|
||||
|
||||
# Enemy entities
|
||||
rat1 = mcrfpy.Entity(12, 5)
|
||||
rat1.texture = texture
|
||||
rat1.sprite_index = 123 # Rat
|
||||
|
||||
rat2 = mcrfpy.Entity(14, 9)
|
||||
rat2.texture = texture
|
||||
rat2.sprite_index = 123 # Rat
|
||||
|
||||
cyclops = mcrfpy.Entity(10, 10)
|
||||
cyclops.texture = texture
|
||||
cyclops.sprite_index = 109 # Cyclops
|
||||
|
||||
# Add entities to grid
|
||||
grid.entities.append(player)
|
||||
grid.entities.append(rat1)
|
||||
grid.entities.append(rat2)
|
||||
grid.entities.append(cyclops)
|
||||
|
||||
# Create a smaller grid showing tile palette
|
||||
palette_label = mcrfpy.Caption(100, 600, "Tile Types:")
|
||||
palette_label.font = mcrfpy.default_font
|
||||
palette_label.font_color = (255, 255, 255)
|
||||
|
||||
palette = mcrfpy.Grid(250, 580, 7, 1, texture, 32, 32)
|
||||
palette.texture = texture
|
||||
palette.set_tile(0, 0, FLOOR)
|
||||
palette.set_tile(1, 0, WALL)
|
||||
palette.set_tile(2, 0, DOOR)
|
||||
palette.set_tile(3, 0, CHEST)
|
||||
palette.set_tile(4, 0, BUTTON)
|
||||
palette.set_tile(5, 0, EXIT)
|
||||
palette.set_tile(6, 0, BOULDER)
|
||||
|
||||
# Labels for palette
|
||||
labels = ["Floor", "Wall", "Door", "Chest", "Button", "Exit", "Boulder"]
|
||||
for i, label in enumerate(labels):
|
||||
l = mcrfpy.Caption(250 + i * 32, 615, label)
|
||||
l.font = mcrfpy.default_font
|
||||
l.font_size = 10
|
||||
l.font_color = (255, 255, 255)
|
||||
mcrfpy.sceneUI("grid").append(l)
|
||||
|
||||
# Add info caption
|
||||
info = mcrfpy.Caption(100, 680, "Grid supports tiles and entities. Entities can move independently of the tile grid.")
|
||||
info.font = mcrfpy.default_font
|
||||
info.font_size = 14
|
||||
info.font_color = (200, 200, 200)
|
||||
|
||||
# Add all elements to scene
|
||||
ui = mcrfpy.sceneUI("grid")
|
||||
ui.append(title)
|
||||
ui.append(grid)
|
||||
ui.append(palette_label)
|
||||
ui.append(palette)
|
||||
ui.append(info)
|
||||
|
||||
# Switch to scene
|
||||
mcrfpy.setScene("grid")
|
||||
|
||||
# Set timer to capture after rendering starts
|
||||
mcrfpy.setTimer("capture", capture_grid, 100)
|
|
@ -0,0 +1,160 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate sprite documentation screenshots for McRogueFace"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def capture_sprites(runtime):
|
||||
"""Capture sprite examples after render loop starts"""
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("mcrogueface.github.io/images/ui_sprite_example.png")
|
||||
print("Sprite screenshot saved!")
|
||||
|
||||
# Exit after capturing
|
||||
sys.exit(0)
|
||||
|
||||
# Create scene
|
||||
mcrfpy.createScene("sprites")
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture("assets/kenney_TD_MR_IP.png", 16, 16)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(400, 30, "Sprite Examples")
|
||||
title.font = mcrfpy.default_font
|
||||
title.font_size = 24
|
||||
title.font_color = (255, 255, 255)
|
||||
|
||||
# Create a frame background
|
||||
frame = mcrfpy.Frame(50, 80, 700, 500)
|
||||
frame.bgcolor = (64, 64, 128)
|
||||
frame.outline = 2
|
||||
|
||||
# Player sprite
|
||||
player_label = mcrfpy.Caption(100, 120, "Player")
|
||||
player_label.font = mcrfpy.default_font
|
||||
player_label.font_color = (255, 255, 255)
|
||||
|
||||
player = mcrfpy.Sprite(120, 150)
|
||||
player.texture = texture
|
||||
player.sprite_index = 84 # Player sprite
|
||||
player.scale = (3.0, 3.0)
|
||||
|
||||
# Enemy sprites
|
||||
enemy_label = mcrfpy.Caption(250, 120, "Enemies")
|
||||
enemy_label.font = mcrfpy.default_font
|
||||
enemy_label.font_color = (255, 255, 255)
|
||||
|
||||
rat = mcrfpy.Sprite(250, 150)
|
||||
rat.texture = texture
|
||||
rat.sprite_index = 123 # Rat
|
||||
rat.scale = (3.0, 3.0)
|
||||
|
||||
big_rat = mcrfpy.Sprite(320, 150)
|
||||
big_rat.texture = texture
|
||||
big_rat.sprite_index = 130 # Big rat
|
||||
big_rat.scale = (3.0, 3.0)
|
||||
|
||||
cyclops = mcrfpy.Sprite(390, 150)
|
||||
cyclops.texture = texture
|
||||
cyclops.sprite_index = 109 # Cyclops
|
||||
cyclops.scale = (3.0, 3.0)
|
||||
|
||||
# Items row
|
||||
items_label = mcrfpy.Caption(100, 250, "Items")
|
||||
items_label.font = mcrfpy.default_font
|
||||
items_label.font_color = (255, 255, 255)
|
||||
|
||||
# Boulder
|
||||
boulder = mcrfpy.Sprite(100, 280)
|
||||
boulder.texture = texture
|
||||
boulder.sprite_index = 66 # Boulder
|
||||
boulder.scale = (3.0, 3.0)
|
||||
|
||||
# Chest
|
||||
chest = mcrfpy.Sprite(170, 280)
|
||||
chest.texture = texture
|
||||
chest.sprite_index = 89 # Closed chest
|
||||
chest.scale = (3.0, 3.0)
|
||||
|
||||
# Key
|
||||
key = mcrfpy.Sprite(240, 280)
|
||||
key.texture = texture
|
||||
key.sprite_index = 384 # Key
|
||||
key.scale = (3.0, 3.0)
|
||||
|
||||
# Button
|
||||
button = mcrfpy.Sprite(310, 280)
|
||||
button.texture = texture
|
||||
button.sprite_index = 250 # Button
|
||||
button.scale = (3.0, 3.0)
|
||||
|
||||
# UI elements row
|
||||
ui_label = mcrfpy.Caption(100, 380, "UI Elements")
|
||||
ui_label.font = mcrfpy.default_font
|
||||
ui_label.font_color = (255, 255, 255)
|
||||
|
||||
# Hearts
|
||||
heart_full = mcrfpy.Sprite(100, 410)
|
||||
heart_full.texture = texture
|
||||
heart_full.sprite_index = 210 # Full heart
|
||||
heart_full.scale = (3.0, 3.0)
|
||||
|
||||
heart_half = mcrfpy.Sprite(170, 410)
|
||||
heart_half.texture = texture
|
||||
heart_half.sprite_index = 209 # Half heart
|
||||
heart_half.scale = (3.0, 3.0)
|
||||
|
||||
heart_empty = mcrfpy.Sprite(240, 410)
|
||||
heart_empty.texture = texture
|
||||
heart_empty.sprite_index = 208 # Empty heart
|
||||
heart_empty.scale = (3.0, 3.0)
|
||||
|
||||
# Armor
|
||||
armor = mcrfpy.Sprite(340, 410)
|
||||
armor.texture = texture
|
||||
armor.sprite_index = 211 # Armor
|
||||
armor.scale = (3.0, 3.0)
|
||||
|
||||
# Scale demonstration
|
||||
scale_label = mcrfpy.Caption(500, 120, "Scale Demo")
|
||||
scale_label.font = mcrfpy.default_font
|
||||
scale_label.font_color = (255, 255, 255)
|
||||
|
||||
# Same sprite at different scales
|
||||
for i, scale in enumerate([1.0, 2.0, 3.0, 4.0]):
|
||||
s = mcrfpy.Sprite(500 + i * 60, 150)
|
||||
s.texture = texture
|
||||
s.sprite_index = 84 # Player
|
||||
s.scale = (scale, scale)
|
||||
mcrfpy.sceneUI("sprites").append(s)
|
||||
|
||||
# Add all elements to scene
|
||||
ui = mcrfpy.sceneUI("sprites")
|
||||
ui.append(frame)
|
||||
ui.append(title)
|
||||
ui.append(player_label)
|
||||
ui.append(player)
|
||||
ui.append(enemy_label)
|
||||
ui.append(rat)
|
||||
ui.append(big_rat)
|
||||
ui.append(cyclops)
|
||||
ui.append(items_label)
|
||||
ui.append(boulder)
|
||||
ui.append(chest)
|
||||
ui.append(key)
|
||||
ui.append(button)
|
||||
ui.append(ui_label)
|
||||
ui.append(heart_full)
|
||||
ui.append(heart_half)
|
||||
ui.append(heart_empty)
|
||||
ui.append(armor)
|
||||
ui.append(scale_label)
|
||||
|
||||
# Switch to scene
|
||||
mcrfpy.setScene("sprites")
|
||||
|
||||
# Set timer to capture after rendering starts
|
||||
mcrfpy.setTimer("capture", capture_sprites, 100)
|
|
@ -0,0 +1,136 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test for Issue #12: Forbid GridPoint/GridPointState instantiation
|
||||
|
||||
This test verifies that GridPoint and GridPointState cannot be instantiated
|
||||
directly from Python, as they should only be created internally by the C++ code.
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
def test_gridpoint_instantiation():
|
||||
"""Test that GridPoint and GridPointState cannot be instantiated"""
|
||||
print("=== Testing GridPoint/GridPointState Instantiation Prevention (Issue #12) ===\n")
|
||||
|
||||
tests_passed = 0
|
||||
tests_total = 0
|
||||
|
||||
# Test 1: Try to instantiate GridPoint
|
||||
print("--- Test 1: GridPoint instantiation ---")
|
||||
tests_total += 1
|
||||
try:
|
||||
point = mcrfpy.GridPoint()
|
||||
print("✗ FAIL: GridPoint() should not be allowed")
|
||||
except TypeError as e:
|
||||
print(f"✓ PASS: GridPoint instantiation correctly prevented: {e}")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ FAIL: Unexpected error: {e}")
|
||||
|
||||
# Test 2: Try to instantiate GridPointState
|
||||
print("\n--- Test 2: GridPointState instantiation ---")
|
||||
tests_total += 1
|
||||
try:
|
||||
state = mcrfpy.GridPointState()
|
||||
print("✗ FAIL: GridPointState() should not be allowed")
|
||||
except TypeError as e:
|
||||
print(f"✓ PASS: GridPointState instantiation correctly prevented: {e}")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ FAIL: Unexpected error: {e}")
|
||||
|
||||
# Test 3: Verify GridPoint can still be obtained from Grid
|
||||
print("\n--- Test 3: GridPoint obtained from Grid.at() ---")
|
||||
tests_total += 1
|
||||
try:
|
||||
grid = mcrfpy.Grid(10, 10)
|
||||
point = grid.at(5, 5)
|
||||
print(f"✓ PASS: GridPoint obtained from Grid.at(): {point}")
|
||||
print(f" Type: {type(point).__name__}")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ FAIL: Could not get GridPoint from Grid: {e}")
|
||||
|
||||
# Test 4: Verify GridPointState can still be obtained from GridPoint
|
||||
print("\n--- Test 4: GridPointState obtained from GridPoint ---")
|
||||
tests_total += 1
|
||||
try:
|
||||
# GridPointState is accessed through GridPoint's click handler
|
||||
# Let's check if we can access point properties that would use GridPointState
|
||||
if hasattr(point, 'walkable'):
|
||||
print(f"✓ PASS: GridPoint has expected properties")
|
||||
print(f" walkable: {point.walkable}")
|
||||
print(f" transparent: {point.transparent}")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("✗ FAIL: GridPoint missing expected properties")
|
||||
except Exception as e:
|
||||
print(f"✗ FAIL: Error accessing GridPoint properties: {e}")
|
||||
|
||||
# Test 5: Try to call the types directly (alternative syntax)
|
||||
print("\n--- Test 5: Alternative instantiation attempts ---")
|
||||
tests_total += 1
|
||||
all_prevented = True
|
||||
|
||||
# Try various ways to instantiate
|
||||
attempts = [
|
||||
("mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)",
|
||||
lambda: mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)),
|
||||
("type(point)()",
|
||||
lambda: type(point)() if 'point' in locals() else None),
|
||||
]
|
||||
|
||||
for desc, func in attempts:
|
||||
try:
|
||||
if func:
|
||||
result = func()
|
||||
print(f"✗ FAIL: {desc} should not be allowed")
|
||||
all_prevented = False
|
||||
except (TypeError, AttributeError) as e:
|
||||
print(f" ✓ Correctly prevented: {desc}")
|
||||
except Exception as e:
|
||||
print(f" ? Unexpected error for {desc}: {e}")
|
||||
|
||||
if all_prevented:
|
||||
print("✓ PASS: All alternative instantiation attempts prevented")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print("✗ FAIL: Some instantiation attempts succeeded")
|
||||
|
||||
# Summary
|
||||
print(f"\n=== SUMMARY ===")
|
||||
print(f"Tests passed: {tests_passed}/{tests_total}")
|
||||
|
||||
if tests_passed == tests_total:
|
||||
print("\nIssue #12 FIXED: GridPoint/GridPointState instantiation properly forbidden!")
|
||||
else:
|
||||
print("\nIssue #12: Some tests failed")
|
||||
|
||||
return tests_passed == tests_total
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback to run the test"""
|
||||
try:
|
||||
# First verify the types exist
|
||||
print("Checking that GridPoint and GridPointState types exist...")
|
||||
print(f"GridPoint type: {mcrfpy.GridPoint}")
|
||||
print(f"GridPointState type: {mcrfpy.GridPointState}")
|
||||
print()
|
||||
|
||||
success = test_gridpoint_instantiation()
|
||||
print("\nOverall result: " + ("PASS" if success else "FAIL"))
|
||||
except Exception as e:
|
||||
print(f"\nTest error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("\nOverall result: FAIL")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
# Set up the test scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# Schedule test to run after game loop starts
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
|
@ -0,0 +1,337 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive test for Issues #26 & #28: Iterator implementation for collections
|
||||
|
||||
This test covers both UICollection and UIEntityCollection iterator implementations,
|
||||
testing all aspects of the Python sequence protocol.
|
||||
|
||||
Issues:
|
||||
- #26: Iterator support for UIEntityCollection
|
||||
- #28: Iterator support for UICollection
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
import gc
|
||||
|
||||
def test_sequence_protocol(collection, name, expected_types=None):
|
||||
"""Test all sequence protocol operations on a collection"""
|
||||
print(f"\n=== Testing {name} ===")
|
||||
|
||||
tests_passed = 0
|
||||
tests_total = 0
|
||||
|
||||
# Test 1: len()
|
||||
tests_total += 1
|
||||
try:
|
||||
length = len(collection)
|
||||
print(f"✓ len() works: {length} items")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ len() failed: {e}")
|
||||
return tests_passed, tests_total
|
||||
|
||||
# Test 2: Basic iteration
|
||||
tests_total += 1
|
||||
try:
|
||||
items = []
|
||||
types = []
|
||||
for item in collection:
|
||||
items.append(item)
|
||||
types.append(type(item).__name__)
|
||||
print(f"✓ Iteration works: found {len(items)} items")
|
||||
print(f" Types: {types}")
|
||||
if expected_types and types != expected_types:
|
||||
print(f" WARNING: Expected types {expected_types}")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ Iteration failed (Issue #26/#28): {e}")
|
||||
|
||||
# Test 3: Indexing (positive)
|
||||
tests_total += 1
|
||||
try:
|
||||
if length > 0:
|
||||
first = collection[0]
|
||||
last = collection[length-1]
|
||||
print(f"✓ Positive indexing works: [0]={type(first).__name__}, [{length-1}]={type(last).__name__}")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(" Skipping indexing test - empty collection")
|
||||
except Exception as e:
|
||||
print(f"✗ Positive indexing failed: {e}")
|
||||
|
||||
# Test 4: Negative indexing
|
||||
tests_total += 1
|
||||
try:
|
||||
if length > 0:
|
||||
last = collection[-1]
|
||||
first = collection[-length]
|
||||
print(f"✓ Negative indexing works: [-1]={type(last).__name__}, [-{length}]={type(first).__name__}")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(" Skipping negative indexing test - empty collection")
|
||||
except Exception as e:
|
||||
print(f"✗ Negative indexing failed: {e}")
|
||||
|
||||
# Test 5: Out of bounds indexing
|
||||
tests_total += 1
|
||||
try:
|
||||
_ = collection[length + 10]
|
||||
print(f"✗ Out of bounds indexing should raise IndexError but didn't")
|
||||
except IndexError:
|
||||
print(f"✓ Out of bounds indexing correctly raises IndexError")
|
||||
tests_passed += 1
|
||||
except Exception as e:
|
||||
print(f"✗ Out of bounds indexing raised wrong exception: {type(e).__name__}: {e}")
|
||||
|
||||
# Test 6: Slicing
|
||||
tests_total += 1
|
||||
try:
|
||||
if length >= 2:
|
||||
slice_result = collection[0:2]
|
||||
print(f"✓ Slicing works: [0:2] returned {len(slice_result)} items")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(" Skipping slicing test - not enough items")
|
||||
except NotImplementedError:
|
||||
print(f"✗ Slicing not implemented")
|
||||
except Exception as e:
|
||||
print(f"✗ Slicing failed: {e}")
|
||||
|
||||
# Test 7: Contains operator
|
||||
tests_total += 1
|
||||
try:
|
||||
if length > 0:
|
||||
first_item = collection[0]
|
||||
if first_item in collection:
|
||||
print(f"✓ 'in' operator works")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(f"✗ 'in' operator returned False for existing item")
|
||||
else:
|
||||
print(" Skipping 'in' operator test - empty collection")
|
||||
except NotImplementedError:
|
||||
print(f"✗ 'in' operator not implemented")
|
||||
except Exception as e:
|
||||
print(f"✗ 'in' operator failed: {e}")
|
||||
|
||||
# Test 8: Multiple iterations
|
||||
tests_total += 1
|
||||
try:
|
||||
count1 = sum(1 for _ in collection)
|
||||
count2 = sum(1 for _ in collection)
|
||||
if count1 == count2 == length:
|
||||
print(f"✓ Multiple iterations work correctly")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(f"✗ Multiple iterations inconsistent: {count1} vs {count2} vs {length}")
|
||||
except Exception as e:
|
||||
print(f"✗ Multiple iterations failed: {e}")
|
||||
|
||||
# Test 9: Iterator state independence
|
||||
tests_total += 1
|
||||
try:
|
||||
iter1 = iter(collection)
|
||||
iter2 = iter(collection)
|
||||
|
||||
# Advance iter1
|
||||
next(iter1)
|
||||
|
||||
# iter2 should still be at the beginning
|
||||
item1_from_iter2 = next(iter2)
|
||||
item1_from_collection = collection[0]
|
||||
|
||||
if type(item1_from_iter2).__name__ == type(item1_from_collection).__name__:
|
||||
print(f"✓ Iterator state independence maintained")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(f"✗ Iterator states are not independent")
|
||||
except Exception as e:
|
||||
print(f"✗ Iterator state test failed: {e}")
|
||||
|
||||
# Test 10: List conversion
|
||||
tests_total += 1
|
||||
try:
|
||||
as_list = list(collection)
|
||||
if len(as_list) == length:
|
||||
print(f"✓ list() conversion works: {len(as_list)} items")
|
||||
tests_passed += 1
|
||||
else:
|
||||
print(f"✗ list() conversion wrong length: {len(as_list)} vs {length}")
|
||||
except Exception as e:
|
||||
print(f"✗ list() conversion failed: {e}")
|
||||
|
||||
return tests_passed, tests_total
|
||||
|
||||
def test_modification_during_iteration(collection, name):
|
||||
"""Test collection modification during iteration"""
|
||||
print(f"\n=== Testing {name} Modification During Iteration ===")
|
||||
|
||||
# This is a tricky case - some implementations might crash
|
||||
# or behave unexpectedly when the collection is modified during iteration
|
||||
|
||||
if len(collection) < 2:
|
||||
print(" Skipping - need at least 2 items")
|
||||
return
|
||||
|
||||
try:
|
||||
count = 0
|
||||
for i, item in enumerate(collection):
|
||||
count += 1
|
||||
if i == 0 and hasattr(collection, 'remove'):
|
||||
# Try to remove an item during iteration
|
||||
# This might raise an exception or cause undefined behavior
|
||||
pass # Don't actually modify to avoid breaking the test
|
||||
print(f"✓ Iteration completed without modification: {count} items")
|
||||
except Exception as e:
|
||||
print(f" Note: Iteration with modification would fail: {e}")
|
||||
|
||||
def run_comprehensive_test():
|
||||
"""Run comprehensive iterator tests for both collection types"""
|
||||
print("=== Testing Collection Iterator Implementation (Issues #26 & #28) ===")
|
||||
|
||||
total_passed = 0
|
||||
total_tests = 0
|
||||
|
||||
# Test UICollection
|
||||
print("\n--- Testing UICollection ---")
|
||||
|
||||
# Create UI elements
|
||||
scene_ui = mcrfpy.sceneUI("test")
|
||||
|
||||
# Add various UI elements
|
||||
frame = mcrfpy.Frame(10, 10, 200, 150,
|
||||
fill_color=mcrfpy.Color(100, 100, 200),
|
||||
outline_color=mcrfpy.Color(255, 255, 255))
|
||||
caption = mcrfpy.Caption(mcrfpy.Vector(220, 10),
|
||||
text="Test Caption",
|
||||
fill_color=mcrfpy.Color(255, 255, 0))
|
||||
|
||||
scene_ui.append(frame)
|
||||
scene_ui.append(caption)
|
||||
|
||||
# Test UICollection
|
||||
passed, total = test_sequence_protocol(scene_ui, "UICollection",
|
||||
expected_types=["Frame", "Caption"])
|
||||
total_passed += passed
|
||||
total_tests += total
|
||||
|
||||
test_modification_during_iteration(scene_ui, "UICollection")
|
||||
|
||||
# Test UICollection with children
|
||||
print("\n--- Testing UICollection Children (Nested) ---")
|
||||
child_caption = mcrfpy.Caption(mcrfpy.Vector(10, 10),
|
||||
text="Child",
|
||||
fill_color=mcrfpy.Color(200, 200, 200))
|
||||
frame.children.append(child_caption)
|
||||
|
||||
passed, total = test_sequence_protocol(frame.children, "Frame.children",
|
||||
expected_types=["Caption"])
|
||||
total_passed += passed
|
||||
total_tests += total
|
||||
|
||||
# Test UIEntityCollection
|
||||
print("\n--- Testing UIEntityCollection ---")
|
||||
|
||||
# Create a grid with entities
|
||||
grid = mcrfpy.Grid(30, 30)
|
||||
grid.x = 10
|
||||
grid.y = 200
|
||||
grid.w = 600
|
||||
grid.h = 400
|
||||
scene_ui.append(grid)
|
||||
|
||||
# Add various entities
|
||||
entity1 = mcrfpy.Entity(5, 5)
|
||||
entity2 = mcrfpy.Entity(10, 10)
|
||||
entity3 = mcrfpy.Entity(15, 15)
|
||||
|
||||
grid.entities.append(entity1)
|
||||
grid.entities.append(entity2)
|
||||
grid.entities.append(entity3)
|
||||
|
||||
passed, total = test_sequence_protocol(grid.entities, "UIEntityCollection",
|
||||
expected_types=["Entity", "Entity", "Entity"])
|
||||
total_passed += passed
|
||||
total_tests += total
|
||||
|
||||
test_modification_during_iteration(grid.entities, "UIEntityCollection")
|
||||
|
||||
# Test empty collections
|
||||
print("\n--- Testing Empty Collections ---")
|
||||
empty_grid = mcrfpy.Grid(10, 10)
|
||||
|
||||
passed, total = test_sequence_protocol(empty_grid.entities, "Empty UIEntityCollection")
|
||||
total_passed += passed
|
||||
total_tests += total
|
||||
|
||||
empty_frame = mcrfpy.Frame(0, 0, 50, 50)
|
||||
passed, total = test_sequence_protocol(empty_frame.children, "Empty UICollection")
|
||||
total_passed += passed
|
||||
total_tests += total
|
||||
|
||||
# Test large collection
|
||||
print("\n--- Testing Large Collection ---")
|
||||
large_grid = mcrfpy.Grid(50, 50)
|
||||
for i in range(100):
|
||||
large_grid.entities.append(mcrfpy.Entity(i % 50, i // 50))
|
||||
|
||||
print(f"Created large collection with {len(large_grid.entities)} entities")
|
||||
|
||||
# Just test basic iteration performance
|
||||
import time
|
||||
start = time.time()
|
||||
count = sum(1 for _ in large_grid.entities)
|
||||
elapsed = time.time() - start
|
||||
print(f"✓ Large collection iteration: {count} items in {elapsed:.3f}s")
|
||||
|
||||
# Edge case: Single item collection
|
||||
print("\n--- Testing Single Item Collection ---")
|
||||
single_grid = mcrfpy.Grid(5, 5)
|
||||
single_grid.entities.append(mcrfpy.Entity(1, 1))
|
||||
|
||||
passed, total = test_sequence_protocol(single_grid.entities, "Single Item UIEntityCollection")
|
||||
total_passed += passed
|
||||
total_tests += total
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("/tmp/issue_26_28_iterator_test.png")
|
||||
|
||||
# Summary
|
||||
print(f"\n=== SUMMARY ===")
|
||||
print(f"Total tests passed: {total_passed}/{total_tests}")
|
||||
|
||||
if total_passed < total_tests:
|
||||
print("\nIssues found:")
|
||||
print("- Issue #26: UIEntityCollection may not fully implement iterator protocol")
|
||||
print("- Issue #28: UICollection may not fully implement iterator protocol")
|
||||
print("\nThe iterator implementation should support:")
|
||||
print("1. Forward iteration with 'for item in collection'")
|
||||
print("2. Multiple independent iterators")
|
||||
print("3. Proper cleanup when iteration completes")
|
||||
print("4. Integration with Python's sequence protocol")
|
||||
else:
|
||||
print("\nAll iterator tests passed!")
|
||||
|
||||
return total_passed == total_tests
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback to run the test"""
|
||||
try:
|
||||
success = run_comprehensive_test()
|
||||
print("\nOverall result: " + ("PASS" if success else "FAIL"))
|
||||
except Exception as e:
|
||||
print(f"\nTest error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
print("\nOverall result: FAIL")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
# Set up the test scene
|
||||
mcrfpy.createScene("test")
|
||||
mcrfpy.setScene("test")
|
||||
|
||||
# Schedule test to run after game loop starts
|
||||
mcrfpy.setTimer("test", run_test, 100)
|
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple test for Issue #37: Verify script loading works from executable directory
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
import mcrfpy
|
||||
|
||||
# This script runs as --exec, which means it's loaded after Python initialization
|
||||
# and after game.py. If we got here, script loading is working.
|
||||
|
||||
print("Issue #37 test: Script execution verified")
|
||||
print(f"Current working directory: {os.getcwd()}")
|
||||
print(f"Script location: {__file__}")
|
||||
|
||||
# Create a simple scene to verify everything is working
|
||||
mcrfpy.createScene("issue37_test")
|
||||
|
||||
print("PASS: Issue #37 - Script loading working correctly")
|
||||
sys.exit(0)
|
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test for Issue #37: Windows scripts subdirectory not checked for .py files
|
||||
|
||||
This test checks if the game can find and load scripts/game.py from different working directories.
|
||||
On Windows, this often fails because fopen uses relative paths without resolving them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
def test_script_loading():
|
||||
# Create a temporary directory to test from
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
print(f"Testing from directory: {tmpdir}")
|
||||
|
||||
# Get the build directory (assuming we're running from the repo root)
|
||||
build_dir = os.path.abspath("build")
|
||||
mcrogueface_exe = os.path.join(build_dir, "mcrogueface")
|
||||
if os.name == "nt": # Windows
|
||||
mcrogueface_exe += ".exe"
|
||||
|
||||
# Create a simple test script that the game should load
|
||||
test_script = """
|
||||
import mcrfpy
|
||||
print("TEST SCRIPT LOADED SUCCESSFULLY")
|
||||
mcrfpy.createScene("test_scene")
|
||||
"""
|
||||
|
||||
# Save the original game.py
|
||||
game_py_path = os.path.join(build_dir, "scripts", "game.py")
|
||||
game_py_backup = game_py_path + ".backup"
|
||||
if os.path.exists(game_py_path):
|
||||
shutil.copy(game_py_path, game_py_backup)
|
||||
|
||||
try:
|
||||
# Replace game.py with our test script
|
||||
os.makedirs(os.path.dirname(game_py_path), exist_ok=True)
|
||||
with open(game_py_path, "w") as f:
|
||||
f.write(test_script)
|
||||
|
||||
# Test 1: Run from build directory (should work)
|
||||
print("\nTest 1: Running from build directory...")
|
||||
result = subprocess.run(
|
||||
[mcrogueface_exe, "--headless", "-c", "print('Test 1 complete')"],
|
||||
cwd=build_dir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if "TEST SCRIPT LOADED SUCCESSFULLY" in result.stdout:
|
||||
print("✓ Test 1 PASSED: Script loaded from build directory")
|
||||
else:
|
||||
print("✗ Test 1 FAILED: Script not loaded from build directory")
|
||||
print(f"stdout: {result.stdout}")
|
||||
print(f"stderr: {result.stderr}")
|
||||
|
||||
# Test 2: Run from temporary directory (often fails on Windows)
|
||||
print("\nTest 2: Running from different working directory...")
|
||||
result = subprocess.run(
|
||||
[mcrogueface_exe, "--headless", "-c", "print('Test 2 complete')"],
|
||||
cwd=tmpdir,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5
|
||||
)
|
||||
if "TEST SCRIPT LOADED SUCCESSFULLY" in result.stdout:
|
||||
print("✓ Test 2 PASSED: Script loaded from different directory")
|
||||
else:
|
||||
print("✗ Test 2 FAILED: Script not loaded from different directory")
|
||||
print(f"stdout: {result.stdout}")
|
||||
print(f"stderr: {result.stderr}")
|
||||
print("\nThis is the bug described in Issue #37!")
|
||||
|
||||
finally:
|
||||
# Restore original game.py
|
||||
if os.path.exists(game_py_backup):
|
||||
shutil.move(game_py_backup, game_py_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_script_loading()
|