Compare commits
30 commits
master
...
interprete
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd1561bfc | |||
| 43321487eb | |||
| 90c318104b | |||
| 2a48138011 | |||
| e4482e7189 | |||
| 38d44777f5 | |||
| 70cf44f8f0 | |||
| dd3c64784d | |||
| 05bddae511 | |||
| 0d26d51bc3 | |||
| af6a5e090b | |||
| 281800cd23 | |||
| cc8a7d20e8 | |||
| ff83fd8bb1 | |||
| dae400031f | |||
| cb0130b46e | |||
| 1e7f5e9e7e | |||
| 923350137d | |||
| 6134869371 | |||
| 4715356b5e | |||
| 6dd1cec600 | |||
| f82b861bcd | |||
| 59e6f8d53d | |||
| 1c71d8d4f7 | |||
| 18cfe93a44 | |||
| 9ad0b6850d | |||
| 7ec4698653 | |||
| 68c1a016b0 | |||
| 763fa201f0 | |||
| a44b8c93e9 |
99
.archive/entity_property_setters_test.py
Normal file
|
|
@ -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)
|
||||
61
.archive/entity_setter_simple_test.py
Normal file
|
|
@ -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)
|
||||
105
.archive/issue27_entity_extend_test.py
Normal file
|
|
@ -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)
|
||||
111
.archive/issue33_sprite_index_validation_test.py
Normal file
|
|
@ -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)
|
||||
101
.archive/issue73_entity_index_test.py
Normal file
|
|
@ -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)
|
||||
77
.archive/issue73_simple_index_test.py
Normal file
|
|
@ -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)
|
||||
60
.archive/issue74_grid_xy_properties_test.py
Normal file
|
|
@ -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)
|
||||
87
.archive/issue78_middle_click_fix_test.py
Normal file
|
|
@ -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)
|
||||
BIN
.archive/sequence_demo_screenshot.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
.archive/sequence_protocol_test.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
73
.archive/sprite_texture_setter_test.py
Normal file
|
|
@ -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)
|
||||
17
.gitignore
vendored
|
|
@ -8,31 +8,22 @@ PCbuild
|
|||
obj
|
||||
build
|
||||
lib
|
||||
__pycache__
|
||||
obj
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.md
|
||||
*.zip
|
||||
__lib/
|
||||
__lib_windows/
|
||||
build-windows/
|
||||
build_windows/
|
||||
_oldscripts/
|
||||
assets/
|
||||
cellular_automata_fire/
|
||||
*.txt
|
||||
deps/
|
||||
fetch_issues_txt.py
|
||||
forest_fire_CA.py
|
||||
mcrogueface.github.io
|
||||
scripts/
|
||||
tcod_reference
|
||||
.archive
|
||||
.mcp.json
|
||||
dist/
|
||||
|
||||
# Keep important documentation and tests
|
||||
!CLAUDE.md
|
||||
!README.md
|
||||
!tests/
|
||||
test_*
|
||||
|
|
|
|||
7
.gitmodules
vendored
|
|
@ -10,7 +10,6 @@
|
|||
[submodule "modules/SFML"]
|
||||
path = modules/SFML
|
||||
url = git@github.com:SFML/SFML.git
|
||||
[submodule "modules/libtcod-headless"]
|
||||
path = modules/libtcod-headless
|
||||
url = git@github.com:jmccardle/libtcod-headless.git
|
||||
branch = 2.2.1-headless
|
||||
[submodule "modules/libtcod"]
|
||||
path = modules/libtcod
|
||||
url = git@github.com:libtcod/libtcod.git
|
||||
|
|
|
|||
|
|
@ -1,306 +0,0 @@
|
|||
# Building McRogueFace from Source
|
||||
|
||||
This document describes how to build McRogueFace from a fresh clone.
|
||||
|
||||
## Build Options
|
||||
|
||||
There are two ways to build McRogueFace:
|
||||
|
||||
1. **Quick Build** (recommended): Use pre-built dependency libraries from a `build_deps` archive
|
||||
2. **Full Build**: Compile all dependencies from submodules
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### System Dependencies
|
||||
|
||||
Install these packages before building:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install \
|
||||
build-essential \
|
||||
cmake \
|
||||
git \
|
||||
zlib1g-dev \
|
||||
libx11-dev \
|
||||
libxrandr-dev \
|
||||
libxcursor-dev \
|
||||
libfreetype-dev \
|
||||
libudev-dev \
|
||||
libvorbis-dev \
|
||||
libflac-dev \
|
||||
libgl-dev \
|
||||
libopenal-dev
|
||||
```
|
||||
|
||||
**Note:** SDL is NOT required - McRogueFace uses libtcod-headless which has no SDL dependency.
|
||||
|
||||
---
|
||||
|
||||
## Option 1: Quick Build (Using Pre-built Dependencies)
|
||||
|
||||
If you have a `build_deps.tar.gz` or `build_deps.zip` archive:
|
||||
|
||||
```bash
|
||||
# Clone McRogueFace (no submodules needed)
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
|
||||
# Extract pre-built dependencies
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
# Or for zip: unzip /path/to/build_deps.zip
|
||||
|
||||
# Build McRogueFace
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Run
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
The `build_deps` archive contains:
|
||||
- `__lib/` - Pre-built shared libraries (Python, SFML, libtcod-headless)
|
||||
- `deps/` - Header symlinks for compilation
|
||||
|
||||
**Total build time: ~30 seconds**
|
||||
|
||||
---
|
||||
|
||||
## Option 2: Full Build (Compiling All Dependencies)
|
||||
|
||||
### 1. Clone with Submodules
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
```
|
||||
|
||||
If submodules weren't cloned:
|
||||
```bash
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
**Note:** imgui/imgui-sfml submodules may fail - this is fine, they're not used.
|
||||
|
||||
### 2. Create Dependency Symlinks
|
||||
|
||||
```bash
|
||||
cd deps
|
||||
ln -sf ../modules/cpython cpython
|
||||
ln -sf ../modules/libtcod-headless/src/libtcod libtcod
|
||||
ln -sf ../modules/cpython/Include Python
|
||||
ln -sf ../modules/SFML/include/SFML SFML
|
||||
cd ..
|
||||
```
|
||||
|
||||
### 3. Build libtcod-headless
|
||||
|
||||
libtcod-headless is our SDL-free fork with vendored dependencies:
|
||||
|
||||
```bash
|
||||
cd modules/libtcod-headless
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
That's it! No special flags needed - libtcod-headless defaults to:
|
||||
- `LIBTCOD_SDL3=disable` (no SDL dependency)
|
||||
- Vendored lodepng, utf8proc, stb
|
||||
|
||||
### 4. Build Python 3.12
|
||||
|
||||
```bash
|
||||
cd modules/cpython
|
||||
./configure --enable-shared
|
||||
make -j$(nproc)
|
||||
cd ../..
|
||||
```
|
||||
|
||||
### 5. Build SFML 2.6
|
||||
|
||||
```bash
|
||||
cd modules/SFML
|
||||
mkdir build && cd build
|
||||
|
||||
cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=ON
|
||||
|
||||
make -j$(nproc)
|
||||
cd ../../..
|
||||
```
|
||||
|
||||
### 6. Copy Libraries
|
||||
|
||||
```bash
|
||||
mkdir -p __lib
|
||||
|
||||
# Python
|
||||
cp modules/cpython/libpython3.12.so* __lib/
|
||||
|
||||
# SFML
|
||||
cp modules/SFML/build/lib/libsfml-*.so* __lib/
|
||||
|
||||
# libtcod-headless
|
||||
cp modules/libtcod-headless/build/bin/libtcod.so* __lib/
|
||||
|
||||
# Python standard library
|
||||
cp -r modules/cpython/Lib __lib/Python
|
||||
```
|
||||
|
||||
### 7. Build McRogueFace
|
||||
|
||||
```bash
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### 8. Run
|
||||
|
||||
```bash
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Submodule Versions
|
||||
|
||||
| Submodule | Version | Notes |
|
||||
|-----------|---------|-------|
|
||||
| SFML | 2.6.1 | Graphics, audio, windowing |
|
||||
| cpython | 3.12.2 | Embedded Python interpreter |
|
||||
| libtcod-headless | 2.2.1 | SDL-free fork for FOV, pathfinding |
|
||||
|
||||
---
|
||||
|
||||
## Creating a build_deps Archive
|
||||
|
||||
To create a `build_deps` archive for distribution:
|
||||
|
||||
```bash
|
||||
cd McRogueFace
|
||||
|
||||
# Create archive directory
|
||||
mkdir -p build_deps_staging
|
||||
|
||||
# Copy libraries
|
||||
cp -r __lib build_deps_staging/
|
||||
|
||||
# Copy/create deps symlinks as actual directories with only needed headers
|
||||
mkdir -p build_deps_staging/deps
|
||||
cp -rL deps/libtcod build_deps_staging/deps/ # Follow symlink
|
||||
cp -rL deps/Python build_deps_staging/deps/
|
||||
cp -rL deps/SFML build_deps_staging/deps/
|
||||
cp -r deps/platform build_deps_staging/deps/
|
||||
|
||||
# Create archives
|
||||
cd build_deps_staging
|
||||
tar -czf ../build_deps.tar.gz __lib deps
|
||||
zip -r ../build_deps.zip __lib deps
|
||||
cd ..
|
||||
|
||||
# Cleanup
|
||||
rm -rf build_deps_staging
|
||||
```
|
||||
|
||||
The resulting archive can be distributed alongside releases for users who want to build McRogueFace without compiling dependencies.
|
||||
|
||||
**Archive contents:**
|
||||
```
|
||||
build_deps.tar.gz
|
||||
├── __lib/
|
||||
│ ├── libpython3.12.so*
|
||||
│ ├── libsfml-*.so*
|
||||
│ ├── libtcod.so*
|
||||
│ └── Python/ # Python standard library
|
||||
└── deps/
|
||||
├── libtcod/ # libtcod headers
|
||||
├── Python/ # Python headers
|
||||
├── SFML/ # SFML headers
|
||||
└── platform/ # Platform-specific configs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verify the Build
|
||||
|
||||
```bash
|
||||
cd build
|
||||
|
||||
# Check version
|
||||
./mcrogueface --version
|
||||
|
||||
# Test headless mode
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Success')"
|
||||
|
||||
# Verify no SDL dependencies
|
||||
ldd mcrogueface | grep -i sdl # Should output nothing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OpenAL not found
|
||||
```bash
|
||||
sudo apt install libopenal-dev
|
||||
```
|
||||
|
||||
### FreeType not found
|
||||
```bash
|
||||
sudo apt install libfreetype-dev
|
||||
```
|
||||
|
||||
### X11/Xrandr not found
|
||||
```bash
|
||||
sudo apt install libx11-dev libxrandr-dev
|
||||
```
|
||||
|
||||
### Python standard library missing
|
||||
Ensure `__lib/Python` contains the standard library:
|
||||
```bash
|
||||
ls __lib/Python/os.py # Should exist
|
||||
```
|
||||
|
||||
### libtcod symbols not found
|
||||
Ensure libtcod.so is in `__lib/` with correct version:
|
||||
```bash
|
||||
ls -la __lib/libtcod.so*
|
||||
# Should show libtcod.so -> libtcod.so.2 -> libtcod.so.2.2.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Times (approximate)
|
||||
|
||||
On a typical 4-core system:
|
||||
|
||||
| Component | Time |
|
||||
|-----------|------|
|
||||
| libtcod-headless | ~30 seconds |
|
||||
| Python 3.12 | ~3-5 minutes |
|
||||
| SFML 2.6 | ~1 minute |
|
||||
| McRogueFace | ~30 seconds |
|
||||
| **Full build total** | **~5-7 minutes** |
|
||||
| **Quick build (pre-built deps)** | **~30 seconds** |
|
||||
|
||||
---
|
||||
|
||||
## Runtime Dependencies
|
||||
|
||||
The built executable requires these system libraries:
|
||||
- `libz.so.1` (zlib)
|
||||
- `libopenal.so.1` (OpenAL)
|
||||
- `libX11.so.6`, `libXrandr.so.2` (X11)
|
||||
- `libfreetype.so.6` (FreeType)
|
||||
- `libGL.so.1` (OpenGL)
|
||||
|
||||
All other dependencies (Python, SFML, libtcod) are bundled in `lib/`.
|
||||
724
CLAUDE.md
|
|
@ -2,294 +2,26 @@
|
|||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Gitea-First Workflow
|
||||
|
||||
**IMPORTANT**: This project uses Gitea for issue tracking, documentation, and project management. Always consult and update Gitea resources before and during development work.
|
||||
|
||||
**Gitea Instance**: https://gamedev.ffwf.net/gitea/john/McRogueFace
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Gitea is the Single Source of Truth**
|
||||
- Issue tracker contains current tasks, bugs, and feature requests
|
||||
- Wiki contains living documentation and architecture decisions
|
||||
- Use Gitea MCP tools to query and update issues programmatically
|
||||
|
||||
2. **Always Check Gitea First**
|
||||
- Before starting work: Check open issues for related tasks or blockers
|
||||
- Before implementing: Read relevant wiki pages per the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) consultation table
|
||||
- When using `/roadmap` command: Query Gitea for up-to-date issue status
|
||||
- When researching a feature: Search Gitea wiki and issues before grepping codebase
|
||||
- When encountering a bug: Check if an issue already exists
|
||||
|
||||
3. **Create Granular Issues**
|
||||
- Break large features into separate, focused issues
|
||||
- Each issue should address one specific problem or enhancement
|
||||
- Tag issues appropriately: `[Bugfix]`, `[Major Feature]`, `[Minor Feature]`, etc.
|
||||
- Link related issues using dependencies or blocking relationships
|
||||
|
||||
4. **Document as You Go**
|
||||
- When work on one issue interacts with another system: Add notes to related issues
|
||||
- When discovering undocumented behavior: Note it for wiki update
|
||||
- When documentation misleads you: Note it for wiki correction
|
||||
- After committing code changes: Update relevant wiki pages (with user permission)
|
||||
- Follow the [Development Workflow](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Development-Workflow) for wiki update procedures
|
||||
|
||||
5. **Cross-Reference Everything**
|
||||
- Commit messages should reference issue numbers (e.g., "Fixes #104", "Addresses #125")
|
||||
- Issue comments should link to commits when work is done
|
||||
- Wiki pages should reference relevant issues for implementation details
|
||||
- Issues should link to each other when dependencies exist
|
||||
|
||||
### Workflow Pattern
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 1. Check Gitea Issues & Wiki │
|
||||
│ - Is there an existing issue for this? │
|
||||
│ - What's the current status? │
|
||||
│ - Are there related issues or blockers? │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 2. Create Issues (if needed) │
|
||||
│ - Break work into granular tasks │
|
||||
│ - Tag appropriately │
|
||||
│ - Link dependencies │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 3. Do the Work │
|
||||
│ - Implement/fix/document │
|
||||
│ - Write tests first (TDD) │
|
||||
│ - Add inline documentation │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 4. Update Gitea │
|
||||
│ - Add notes to affected issues │
|
||||
│ - Create follow-up issues for discovered work │
|
||||
│ - Update wiki if architecture/APIs changed │
|
||||
│ - Add documentation correction tasks │
|
||||
└─────────────────┬───────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 5. Commit & Reference │
|
||||
│ - Commit messages reference issue numbers │
|
||||
│ - Close issues or update status │
|
||||
│ - Add commit links to issue comments │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Benefits of Gitea-First Approach
|
||||
|
||||
- **Reduced Context Switching**: Check brief issue descriptions instead of re-reading entire codebase
|
||||
- **Better Planning**: Issues provide roadmap; avoid duplicate or contradictory work
|
||||
- **Living Documentation**: Wiki and issues stay current as work progresses
|
||||
- **Historical Context**: Issue comments capture why decisions were made
|
||||
- **Efficiency**: MCP tools allow programmatic access to project state
|
||||
|
||||
### MCP Tools Available
|
||||
|
||||
Claude Code has access to Gitea MCP tools for:
|
||||
- `list_repo_issues` - Query current issues with filtering
|
||||
- `get_issue` - Get detailed issue information
|
||||
- `create_issue` - Create new issues programmatically
|
||||
- `create_issue_comment` - Add comments to issues
|
||||
- `edit_issue` - Update issue status, title, body
|
||||
- `add_issue_labels` - Tag issues appropriately
|
||||
- `add_issue_dependency` / `add_issue_blocking` - Link related issues
|
||||
- Plus wiki, milestone, and label management tools
|
||||
|
||||
Use these tools liberally to keep the project organized!
|
||||
|
||||
### Gitea Label System
|
||||
|
||||
**IMPORTANT**: Always apply appropriate labels when creating new issues!
|
||||
|
||||
The project uses a structured label system to organize issues:
|
||||
|
||||
**Label Categories:**
|
||||
|
||||
1. **System Labels** (identify affected codebase area):
|
||||
- `system:rendering` - Rendering pipeline and visuals
|
||||
- `system:ui-hierarchy` - UI component hierarchy and composition
|
||||
- `system:grid` - Grid system and spatial containers
|
||||
- `system:animation` - Animation and property interpolation
|
||||
- `system:python-binding` - Python/C++ binding layer
|
||||
- `system:input` - Input handling and events
|
||||
- `system:performance` - Performance optimization and profiling
|
||||
- `system:documentation` - Documentation infrastructure
|
||||
|
||||
2. **Priority Labels** (development timeline):
|
||||
- `priority:tier1-active` - Current development focus - critical path to v1.0
|
||||
- `priority:tier2-foundation` - Important foundation work - not blocking v1.0
|
||||
- `priority:tier3-future` - Future features - deferred until after v1.0
|
||||
|
||||
3. **Type/Scope Labels** (effort and complexity):
|
||||
- `Major Feature` - Significant time and effort required
|
||||
- `Minor Feature` - Some effort required to create or overhaul functionality
|
||||
- `Tiny Feature` - Quick and easy - a few lines or little interconnection
|
||||
- `Bugfix` - Fixes incorrect behavior
|
||||
- `Refactoring & Cleanup` - No new functionality, just improving codebase
|
||||
- `Documentation` - Documentation work
|
||||
- `Demo Target` - Functionality to demonstrate
|
||||
|
||||
4. **Workflow Labels** (current blockers/needs):
|
||||
- `workflow:blocked` - Blocked by other work - waiting on dependencies
|
||||
- `workflow:needs-documentation` - Needs documentation before or after implementation
|
||||
- `workflow:needs-benchmark` - Needs performance testing and benchmarks
|
||||
- `Alpha Release Requirement` - Blocker to 0.1 Alpha release
|
||||
|
||||
**When creating issues:**
|
||||
- Apply at least one `system:*` label (what part of codebase)
|
||||
- Apply one `priority:tier*` label (when to address it)
|
||||
- Apply one type label (`Major Feature`, `Minor Feature`, `Tiny Feature`, or `Bugfix`)
|
||||
- Apply `workflow:*` labels if applicable (blocked, needs docs, needs benchmarks)
|
||||
|
||||
**Example label combinations:**
|
||||
- New rendering feature: `system:rendering`, `priority:tier2-foundation`, `Major Feature`
|
||||
- Python API improvement: `system:python-binding`, `priority:tier1-active`, `Minor Feature`
|
||||
- Performance work: `system:performance`, `priority:tier1-active`, `Major Feature`, `workflow:needs-benchmark`
|
||||
|
||||
**⚠️ CRITICAL BUG**: The Gitea MCP tool (v0.07) has a label application bug documented in `GITEA_MCP_LABEL_BUG_REPORT.md`:
|
||||
- `add_issue_labels` and `replace_issue_labels` behave inconsistently
|
||||
- Single ID arrays produce different results than multi-ID arrays for the SAME IDs
|
||||
- Label IDs do not map reliably to actual labels
|
||||
|
||||
**Workaround Options:**
|
||||
1. **Best**: Apply labels manually via web interface: `https://gamedev.ffwf.net/gitea/john/McRogueFace/issues/<number>`
|
||||
2. **Automated**: Apply labels ONE AT A TIME using single-element arrays (slower but more reliable)
|
||||
3. **Use single-ID mapping** (documented below)
|
||||
|
||||
**Label ID Reference** (for documentation purposes - see issue #131 for details):
|
||||
```
|
||||
1=Major Feature, 2=Alpha Release, 3=Bugfix, 4=Demo Target, 5=Documentation,
|
||||
6=Minor Feature, 7=tier1-active, 8=tier2-foundation, 9=tier3-future,
|
||||
10=Refactoring, 11=animation, 12=docs, 13=grid, 14=input, 15=performance,
|
||||
16=python-binding, 17=rendering, 18=ui-hierarchy, 19=Tiny Feature,
|
||||
20=blocked, 21=needs-benchmark, 22=needs-documentation
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
McRogueFace uses a unified Makefile for both Linux native builds and Windows cross-compilation.
|
||||
|
||||
**IMPORTANT**: All `make` commands must be run from the **project root directory** (`/home/john/Development/McRogueFace/`), not from `build/` or any subdirectory.
|
||||
|
||||
### Quick Reference
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
# Linux builds
|
||||
make # Build for Linux (default target)
|
||||
make linux # Same as above
|
||||
make run # Build and run
|
||||
make clean # Remove Linux build artifacts
|
||||
# Build the project (compiles to ./build directory)
|
||||
make
|
||||
|
||||
# Windows cross-compilation (requires MinGW-w64)
|
||||
make windows # Release build for Windows
|
||||
make windows-debug # Debug build with console output
|
||||
make clean-windows # Remove Windows build artifacts
|
||||
# Or use the build script directly
|
||||
./build.sh
|
||||
|
||||
# Distribution packages
|
||||
make package-linux-light # Linux with minimal stdlib (~25 MB)
|
||||
make package-linux-full # Linux with full stdlib (~26 MB)
|
||||
make package-windows-light # Windows with minimal stdlib
|
||||
make package-windows-full # Windows with full stdlib
|
||||
make package-all # All platform/preset combinations
|
||||
# Run the game
|
||||
make run
|
||||
|
||||
# Cleanup
|
||||
make clean-all # Remove all builds and packages
|
||||
make clean-dist # Remove only distribution packages
|
||||
# Clean build artifacts
|
||||
make clean
|
||||
|
||||
# The executable and all assets are in ./build/
|
||||
cd build
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
### Build Outputs
|
||||
|
||||
| Command | Output Directory | Executable |
|
||||
|---------|------------------|------------|
|
||||
| `make` / `make linux` | `build/` | `build/mcrogueface` |
|
||||
| `make windows` | `build-windows/` | `build-windows/mcrogueface.exe` |
|
||||
| `make windows-debug` | `build-windows-debug/` | `build-windows-debug/mcrogueface.exe` |
|
||||
| `make package-*` | `dist/` | `.tar.gz` or `.zip` archives |
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**Linux build:**
|
||||
- CMake 3.14+
|
||||
- GCC/G++ with C++17 support
|
||||
- SFML 2.6 development libraries
|
||||
- Libraries in `__lib/` directory (libpython3.14, libtcod, etc.)
|
||||
|
||||
**Windows cross-compilation:**
|
||||
- MinGW-w64 (`x86_64-w64-mingw32-g++-posix`)
|
||||
- Libraries in `__lib_windows/` directory
|
||||
- Toolchain file: `cmake/toolchains/mingw-w64-x86_64.cmake`
|
||||
|
||||
### Library Dependencies
|
||||
|
||||
The build expects pre-built libraries in:
|
||||
- `__lib/` - Linux shared libraries (libpython3.14.so, libsfml-*.so, libtcod.so)
|
||||
- `__lib/Python/Lib/` - Python standard library source
|
||||
- `__lib/Python/lib.linux-x86_64-3.14/` - Python extension modules (.so)
|
||||
- `__lib_windows/` - Windows DLLs and libraries
|
||||
|
||||
### Manual CMake Build
|
||||
|
||||
If you need more control over the build:
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Windows cross-compile
|
||||
mkdir build-windows && cd build-windows
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# Windows debug with console
|
||||
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_WINDOWS_CONSOLE=ON
|
||||
```
|
||||
|
||||
### Distribution Packaging
|
||||
|
||||
The packaging system creates self-contained archives with:
|
||||
- Executable
|
||||
- Required shared libraries
|
||||
- Assets (sprites, fonts, audio)
|
||||
- Python scripts
|
||||
- Filtered Python stdlib (light or full variant)
|
||||
|
||||
**Light variant** (~25 MB): Core + gamedev + utility modules only
|
||||
**Full variant** (~26 MB): Includes networking, async, debugging modules
|
||||
|
||||
Packaging tools:
|
||||
- `tools/package.sh` - Main packaging orchestrator
|
||||
- `tools/package_stdlib.py` - Creates filtered stdlib archives
|
||||
- `tools/stdlib_modules.yaml` - Module categorization config
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**"No rule to make target 'linux'"**: You're in the wrong directory. Run `make` from project root.
|
||||
|
||||
**Library linking errors**: Ensure `__lib/` contains all required .so files. Check `CMakeLists.txt` for `link_directories(${CMAKE_SOURCE_DIR}/__lib)`.
|
||||
|
||||
**Windows build fails**: Verify MinGW-w64 is installed with posix thread model: `x86_64-w64-mingw32-g++-posix --version`
|
||||
|
||||
### Legacy Build Scripts
|
||||
|
||||
The following are deprecated but kept for reference:
|
||||
- `build.sh` - Original Linux build script (use `make` instead)
|
||||
- `GNUmakefile.legacy` - Old wrapper makefile (renamed to avoid conflicts)
|
||||
|
||||
## Project Architecture
|
||||
|
||||
McRogueFace is a C++ game engine with Python scripting support, designed for creating roguelike games. The architecture consists of:
|
||||
|
|
@ -309,12 +41,12 @@ McRogueFace is a C++ game engine with Python scripting support, designed for cre
|
|||
|
||||
### Key Python API (`mcrfpy` module)
|
||||
The C++ engine exposes these primary functions to Python:
|
||||
- Scene Management: `Scene("name")` object
|
||||
- Scene Management: `createScene()`, `setScene()`, `sceneUI()`
|
||||
- Entity Creation: `Entity()` with position and sprite properties
|
||||
- Grid Management: `Grid()` for tilemap rendering
|
||||
- Input Handling: `keypressScene()` for keyboard events
|
||||
- Audio: `createSoundBuffer()`, `playSound()`, `setVolume()`
|
||||
- Timers: `Timer("name")` object for event scheduling
|
||||
- Timers: `setTimer()`, `delTimer()` for event scheduling
|
||||
|
||||
## Development Workflow
|
||||
|
||||
|
|
@ -335,85 +67,44 @@ After building, the executable expects:
|
|||
2. Expose to Python using the existing binding pattern
|
||||
3. Update Python scripts to use new functionality
|
||||
|
||||
## Testing
|
||||
## Testing Game Changes
|
||||
|
||||
### Test Suite Structure
|
||||
|
||||
The `tests/` directory contains the comprehensive test suite:
|
||||
|
||||
```
|
||||
tests/
|
||||
├── run_tests.py # Test runner - executes all tests with timeout
|
||||
├── unit/ # Unit tests for individual components (105+ tests)
|
||||
├── integration/ # Integration tests for system interactions
|
||||
├── regression/ # Bug regression tests (issue_XX_*.py)
|
||||
├── benchmarks/ # Performance benchmarks
|
||||
├── demo/ # Feature demonstration system
|
||||
│ ├── demo_main.py # Interactive demo runner
|
||||
│ ├── screens/ # Per-feature demo screens
|
||||
│ └── screenshots/ # Generated demo screenshots
|
||||
└── notes/ # Analysis files and documentation
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run the full test suite (from tests/ directory)
|
||||
cd tests && python3 run_tests.py
|
||||
|
||||
# Run a specific test
|
||||
cd build && ./mcrogueface --headless --exec ../tests/unit/some_test.py
|
||||
|
||||
# Run the demo system interactively
|
||||
cd build && ./mcrogueface ../tests/demo/demo_main.py
|
||||
|
||||
# Generate demo screenshots (headless)
|
||||
cd build && ./mcrogueface --headless --exec ../tests/demo/demo_main.py
|
||||
```
|
||||
|
||||
### Reading Tests as Examples
|
||||
|
||||
**IMPORTANT**: Before implementing a feature or fixing a bug, check existing tests for API usage examples:
|
||||
|
||||
- `tests/unit/` - Shows correct usage of individual mcrfpy classes and functions
|
||||
- `tests/demo/screens/` - Complete working examples of UI components
|
||||
- `tests/regression/` - Documents edge cases and bug scenarios
|
||||
|
||||
Example: To understand Animation API:
|
||||
```bash
|
||||
grep -r "Animation" tests/unit/
|
||||
cat tests/demo/screens/animation_demo.py
|
||||
```
|
||||
|
||||
### Writing Tests
|
||||
|
||||
**Always write tests when adding features or fixing bugs:**
|
||||
|
||||
1. **For new features**: Create `tests/unit/feature_name_test.py`
|
||||
2. **For bug fixes**: Create `tests/regression/issue_XX_description_test.py`
|
||||
3. **For demos**: Add to `tests/demo/screens/` if it showcases a feature
|
||||
Currently no automated test suite. Manual testing workflow:
|
||||
1. Build with `make`
|
||||
2. Run `make run` or `cd build && ./mcrogueface`
|
||||
3. Test specific features through gameplay
|
||||
4. Check console output for Python errors
|
||||
|
||||
### Quick Testing Commands
|
||||
```bash
|
||||
# Test headless mode with inline Python
|
||||
# Test basic functionality
|
||||
make test
|
||||
|
||||
# Run in Python interactive mode
|
||||
make python
|
||||
|
||||
# Test headless mode
|
||||
cd build
|
||||
./mcrogueface --headless -c "import mcrfpy; print('Headless test')"
|
||||
|
||||
# Run specific test with output
|
||||
./mcrogueface --headless --exec ../tests/unit/my_test.py 2>&1
|
||||
```
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Compiling McRogueFace
|
||||
|
||||
See the [Build System](#build-system) section above for comprehensive build instructions.
|
||||
|
||||
```bash
|
||||
# Quick reference (run from project root!)
|
||||
make # Linux build
|
||||
make windows # Windows cross-compile
|
||||
make clean && make # Full rebuild
|
||||
# Standard build (to ./build directory)
|
||||
make
|
||||
|
||||
# Full rebuild
|
||||
make clean && make
|
||||
|
||||
# Manual CMake build
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
|
||||
# The library path issue: if linking fails, check that libraries are in __lib/
|
||||
# CMakeLists.txt expects: link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
```
|
||||
|
||||
### Running and Capturing Output
|
||||
|
|
@ -463,7 +154,7 @@ cp my_test.py scripts/game.py
|
|||
mv scripts/game.py.bak scripts/game.py
|
||||
|
||||
# Option 3: For quick tests, create minimal game.py
|
||||
echo 'import mcrfpy; print("Test"); scene = mcrfpy.Scene("test"); scene.activate()' > scripts/game.py
|
||||
echo 'import mcrfpy; print("Test"); mcrfpy.createScene("test")' > scripts/game.py
|
||||
```
|
||||
|
||||
### Understanding Key Macros and Patterns
|
||||
|
|
@ -525,323 +216,68 @@ build/
|
|||
## Testing Guidelines
|
||||
|
||||
### Test-Driven Development
|
||||
- **Always write tests first**: Create tests in `./tests/` for all bugs and new features
|
||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix
|
||||
- **Read existing tests**: Check `tests/unit/` and `tests/demo/screens/` for API examples before writing code
|
||||
- **Close the loop**: Reproduce issue → change code → recompile → run test → verify
|
||||
- **Always write tests first**: Create automation tests in `./tests/` for all bugs and new features
|
||||
- **Practice TDD**: Write tests that fail to demonstrate the issue, then pass after the fix is applied
|
||||
- **Close the loop**: Reproduce issue → change code → recompile → verify behavior change
|
||||
|
||||
### Two Types of Tests
|
||||
|
||||
#### 1. Direct Execution Tests (No Game Loop)
|
||||
For tests that only need class initialization or direct code execution:
|
||||
```python
|
||||
# tests/unit/my_feature_test.py
|
||||
# These tests can treat McRogueFace like a Python interpreter
|
||||
import mcrfpy
|
||||
import sys
|
||||
|
||||
# Test code - runs immediately
|
||||
frame = mcrfpy.Frame(pos=(0,0), size=(100,100))
|
||||
assert frame.x == 0
|
||||
assert frame.w == 100
|
||||
|
||||
print("PASS")
|
||||
sys.exit(0)
|
||||
# Test code here
|
||||
result = mcrfpy.some_function()
|
||||
assert result == expected_value
|
||||
print("PASS" if condition else "FAIL")
|
||||
```
|
||||
|
||||
#### 2. Game Loop Tests (Timer-Based)
|
||||
For tests requiring rendering, screenshots, or elapsed time:
|
||||
For tests requiring rendering, game state, or elapsed time:
|
||||
```python
|
||||
# tests/unit/my_visual_test.py
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
def run_test(runtime):
|
||||
"""Timer callback - runs after game loop starts"""
|
||||
# Now rendering is active, screenshots will work
|
||||
automation.screenshot("test_result.png")
|
||||
# Validate results...
|
||||
print("PASS")
|
||||
|
||||
# Run your tests here
|
||||
automation.click(100, 100)
|
||||
|
||||
# Always exit at the end
|
||||
print("PASS" if success else "FAIL")
|
||||
sys.exit(0)
|
||||
|
||||
test_scene = mcrfpy.Scene("test")
|
||||
ui = test_scene.children
|
||||
ui.append(mcrfpy.Frame(pos=(50,50), size=(100,100)))
|
||||
mcrfpy.current_scene = test_scene
|
||||
timer = mcrfpy.Timer("test", run_test, 100)
|
||||
# Set up the test scene
|
||||
mcrfpy.createScene("test")
|
||||
# ... add UI elements ...
|
||||
|
||||
# Schedule test to run after game loop starts
|
||||
mcrfpy.setTimer("test", run_test, 100) # 0.1 seconds
|
||||
```
|
||||
|
||||
### Key Testing Principles
|
||||
- **Timer callbacks are essential**: Screenshots only work after the render loop starts
|
||||
- **Use automation API**: `automation.screenshot()`, `automation.click()` for visual testing
|
||||
- **Exit properly**: Always call `sys.exit(0)` for PASS or `sys.exit(1)` for FAIL
|
||||
- **Headless mode**: Use `--headless --exec` for CI/automated testing
|
||||
- **Check examples first**: Read `tests/demo/screens/*.py` for correct API usage
|
||||
|
||||
### API Quick Reference (from tests)
|
||||
```python
|
||||
# Scene: create and activate a scene, or create another scene
|
||||
mcrfpy.current_scene = mcrfpy.Scene("test")
|
||||
demo_scene = mcrfpy.Scene("demo")
|
||||
|
||||
# Animation: (property, target_value, duration, easing)
|
||||
# direct use of Animation object: deprecated
|
||||
#anim = mcrfpy.Animation("x", 500.0, 2.0, "easeInOut")
|
||||
#anim.start(frame)
|
||||
|
||||
# preferred: create animations directly against the targeted object; use Enum of easing functions
|
||||
frame.animate("x", 500.0, 2.0, mcrfpy.Easing.EASE_IN_OUT)
|
||||
|
||||
# Caption: use keyword arguments to avoid positional conflicts
|
||||
cap = mcrfpy.Caption(text="Hello", pos=(100, 100))
|
||||
|
||||
# Grid center: uses pixel coordinates, not cell coordinates
|
||||
grid = mcrfpy.Grid(grid_size=(15, 10), pos=(50, 50), size=(400, 300))
|
||||
grid.center = (120, 80) # pixels: (cells * cell_size / 2)
|
||||
# grid center defaults to the position that puts (0, 0) in the top left corner of the grid's visible area.
|
||||
# set grid.center to focus on that position. To position the camera in tile coordinates, use grid.center_camera():
|
||||
grid.center_camera((14.5, 8.5)) # offset of 0.5 tiles to point at the middle of the tile
|
||||
|
||||
# Keyboard handler: key names are "Num1", "Num2", "Escape", "Q", etc.
|
||||
def on_key(key, state):
|
||||
if key == "Num1" and state == "start":
|
||||
demo_scene.activate()
|
||||
```
|
||||
|
||||
## Development Best Practices
|
||||
|
||||
### Testing and Deployment
|
||||
- **Keep tests in ./tests, not ./build/tests** - ./build gets shipped, tests shouldn't be included
|
||||
- **Run full suite before commits**: `cd tests && python3 run_tests.py`
|
||||
|
||||
## Documentation Guidelines
|
||||
|
||||
### Documentation Macro System
|
||||
|
||||
**As of 2025-10-30, McRogueFace uses a macro-based documentation system** (`src/McRFPy_Doc.h`) that ensures consistent, complete docstrings across all Python bindings.
|
||||
|
||||
#### Include the Header
|
||||
|
||||
```cpp
|
||||
#include "McRFPy_Doc.h"
|
||||
```
|
||||
|
||||
#### Documenting Methods
|
||||
|
||||
For methods in PyMethodDef arrays, use `MCRF_METHOD`:
|
||||
|
||||
```cpp
|
||||
{"method_name", (PyCFunction)Class::method, METH_VARARGS,
|
||||
MCRF_METHOD(ClassName, method_name,
|
||||
MCRF_SIG("(arg1: type, arg2: type)", "return_type"),
|
||||
MCRF_DESC("Brief description of what the method does."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("arg1", "Description of first argument")
|
||||
MCRF_ARG("arg2", "Description of second argument")
|
||||
MCRF_RETURNS("Description of return value")
|
||||
MCRF_RAISES("ValueError", "Condition that raises this exception")
|
||||
MCRF_NOTE("Important notes or caveats")
|
||||
MCRF_LINK("docs/guide.md", "Related Documentation")
|
||||
)},
|
||||
```
|
||||
|
||||
#### Documenting Properties
|
||||
|
||||
For properties in PyGetSetDef arrays, use `MCRF_PROPERTY`:
|
||||
|
||||
```cpp
|
||||
{"property_name", (getter)getter_func, (setter)setter_func,
|
||||
MCRF_PROPERTY(property_name,
|
||||
"Brief description of the property. "
|
||||
"Additional details about valid values, side effects, etc."
|
||||
), NULL},
|
||||
```
|
||||
|
||||
#### Available Macros
|
||||
|
||||
- `MCRF_SIG(params, ret)` - Method signature
|
||||
- `MCRF_DESC(text)` - Description paragraph
|
||||
- `MCRF_ARGS_START` - Begin arguments section
|
||||
- `MCRF_ARG(name, desc)` - Individual argument
|
||||
- `MCRF_RETURNS(text)` - Return value description
|
||||
- `MCRF_RAISES(exception, condition)` - Exception documentation
|
||||
- `MCRF_NOTE(text)` - Important notes
|
||||
- `MCRF_LINK(path, text)` - Reference to external documentation
|
||||
|
||||
#### Documentation Prose Guidelines
|
||||
|
||||
**Keep C++ docstrings concise** (1-2 sentences per section). For complex topics, use `MCRF_LINK` to reference external guides:
|
||||
|
||||
```cpp
|
||||
MCRF_LINK("docs/animation-guide.md", "Animation System Tutorial")
|
||||
```
|
||||
|
||||
**External documentation** (in `docs/`) can be verbose with examples, tutorials, and design rationale.
|
||||
|
||||
### Regenerating Documentation
|
||||
|
||||
After modifying C++ inline documentation with MCRF_* macros:
|
||||
|
||||
1. **Rebuild the project**: `make -j$(nproc)`
|
||||
|
||||
2. **Generate all documentation** (recommended - single command):
|
||||
```bash
|
||||
./tools/generate_all_docs.sh
|
||||
```
|
||||
This creates:
|
||||
- `docs/api_reference_dynamic.html` - HTML API reference
|
||||
- `docs/API_REFERENCE_DYNAMIC.md` - Markdown API reference
|
||||
- `docs/mcrfpy.3` - Unix man page (section 3)
|
||||
- `stubs/mcrfpy.pyi` - Type stubs for IDE support
|
||||
|
||||
3. **Or generate individually**:
|
||||
```bash
|
||||
# API docs (HTML + Markdown)
|
||||
./build/mcrogueface --headless --exec tools/generate_dynamic_docs.py
|
||||
|
||||
# Type stubs (manually-maintained with @overload support)
|
||||
./build/mcrogueface --headless --exec tools/generate_stubs_v2.py
|
||||
|
||||
# Man page (requires pandoc)
|
||||
./tools/generate_man_page.sh
|
||||
```
|
||||
|
||||
**System Requirements:**
|
||||
- `pandoc` must be installed for man page generation: `sudo apt-get install pandoc`
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **Single source of truth**: Documentation lives in C++ source files via MCRF_* macros
|
||||
- **McRogueFace as Python interpreter**: Documentation scripts MUST be run using McRogueFace itself, not system Python
|
||||
- **Use --headless --exec**: For non-interactive documentation generation
|
||||
- **Link transformation**: `MCRF_LINK` references are transformed to appropriate format (HTML, Markdown, etc.)
|
||||
- **No manual dictionaries**: The old hardcoded documentation system has been removed
|
||||
|
||||
### Documentation Pipeline Architecture
|
||||
|
||||
1. **C++ Source** → MCRF_* macros in PyMethodDef/PyGetSetDef arrays
|
||||
2. **Compilation** → Macros expand to complete docstrings embedded in module
|
||||
3. **Introspection** → Scripts use `dir()`, `getattr()`, `__doc__` to extract
|
||||
4. **Generation** → HTML/Markdown/Stub files created with transformed links
|
||||
5. **No drift**: Impossible for docs and code to disagree - they're the same file!
|
||||
|
||||
The macro system ensures complete, consistent documentation across all Python bindings.
|
||||
|
||||
### Adding Documentation for New Python Types
|
||||
|
||||
When adding a new Python class/type to the engine, follow these steps to ensure it's properly documented:
|
||||
|
||||
#### 1. Class Docstring (tp_doc)
|
||||
|
||||
In the `PyTypeObject` definition (usually in the header file), set `tp_doc` with a comprehensive docstring:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.h
|
||||
.tp_doc = PyDoc_STR(
|
||||
"MyClass(arg1: type, arg2: type)\n\n"
|
||||
"Brief description of what this class does.\n\n"
|
||||
"Args:\n"
|
||||
" arg1: Description of first argument.\n"
|
||||
" arg2: Description of second argument.\n\n"
|
||||
"Properties:\n"
|
||||
" prop1 (type, read-only): Description of property.\n"
|
||||
" prop2 (type): Description of writable property.\n\n"
|
||||
"Example:\n"
|
||||
" obj = mcrfpy.MyClass('example', 42)\n"
|
||||
" print(obj.prop1)\n"
|
||||
),
|
||||
```
|
||||
|
||||
#### 2. Method Documentation (PyMethodDef)
|
||||
|
||||
For each method in the `methods[]` array, use the MCRF_* macros:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.cpp
|
||||
PyMethodDef PyMyClass::methods[] = {
|
||||
{"do_something", (PyCFunction)do_something, METH_VARARGS,
|
||||
MCRF_METHOD(MyClass, do_something,
|
||||
MCRF_SIG("(value: int)", "bool"),
|
||||
MCRF_DESC("Does something with the value."),
|
||||
MCRF_ARGS_START
|
||||
MCRF_ARG("value", "The value to process")
|
||||
MCRF_RETURNS("True if successful, False otherwise")
|
||||
)},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Property Documentation (PyGetSetDef)
|
||||
|
||||
For each property in the `getsetters[]` array, include a docstring:
|
||||
|
||||
```cpp
|
||||
// In PyMyClass.cpp
|
||||
PyGetSetDef PyMyClass::getsetters[] = {
|
||||
{"property_name", (getter)get_property, (setter)set_property,
|
||||
"Property description. Include (type, read-only) if not writable.",
|
||||
NULL},
|
||||
{NULL} // Sentinel
|
||||
};
|
||||
```
|
||||
|
||||
**Important for read-only properties:** Include "read-only" in the docstring so the doc generator detects it:
|
||||
```cpp
|
||||
{"name", (getter)get_name, NULL, // NULL setter = read-only
|
||||
"Object name (str, read-only). Unique identifier.",
|
||||
NULL},
|
||||
```
|
||||
|
||||
#### 4. Register Type in Module
|
||||
|
||||
Ensure the type is properly registered in `McRFPy_API.cpp` and its methods/getsetters are assigned:
|
||||
|
||||
```cpp
|
||||
// Set methods and getsetters before PyType_Ready
|
||||
mcrfpydef::PyMyClassType.tp_methods = PyMyClass::methods;
|
||||
mcrfpydef::PyMyClassType.tp_getset = PyMyClass::getsetters;
|
||||
|
||||
// Then call PyType_Ready and add to module
|
||||
```
|
||||
|
||||
#### 5. Regenerate Documentation
|
||||
|
||||
After adding the new type, regenerate all docs:
|
||||
- **Timer callbacks are essential**: Screenshots and UI interactions only work after the render loop starts
|
||||
- **Use automation API**: Always create and examine screenshots when visual feedback is required
|
||||
- **Exit properly**: Call `sys.exit()` at the end of timer-based tests to prevent hanging
|
||||
- **Headless mode**: Use `--exec` flag for automated testing: `./mcrogueface --headless --exec tests/my_test.py`
|
||||
|
||||
### Example Test Pattern
|
||||
```bash
|
||||
make -j4 # Rebuild with new documentation
|
||||
cd build
|
||||
./mcrogueface --headless --exec ../tools/generate_dynamic_docs.py
|
||||
cp docs/API_REFERENCE_DYNAMIC.md ../docs/
|
||||
cp docs/api_reference_dynamic.html ../docs/
|
||||
```
|
||||
# Run a test that requires game loop
|
||||
./build/mcrogueface --headless --exec tests/issue_78_middle_click_test.py
|
||||
|
||||
#### 6. Update Type Stubs (Optional)
|
||||
|
||||
For IDE support, update `stubs/mcrfpy.pyi` with the new class:
|
||||
|
||||
```python
|
||||
class MyClass:
|
||||
"""Brief description."""
|
||||
def __init__(self, arg1: str, arg2: int) -> None: ...
|
||||
@property
|
||||
def prop1(self) -> str: ...
|
||||
def do_something(self, value: int) -> bool: ...
|
||||
```
|
||||
|
||||
### Documentation Extraction Details
|
||||
|
||||
The doc generator (`tools/generate_dynamic_docs.py`) uses Python introspection:
|
||||
|
||||
- **Classes**: Detected via `inspect.isclass()`, docstring from `cls.__doc__`
|
||||
- **Methods**: Detected via `callable()` check on class attributes
|
||||
- **Properties**: Detected via `types.GetSetDescriptorType` (C++ extension) or `property` (Python)
|
||||
- **Read-only detection**: Checks if "read-only" appears in property docstring
|
||||
|
||||
If documentation isn't appearing, verify:
|
||||
1. The type is exported to the `mcrfpy` module
|
||||
2. Methods/getsetters arrays are properly assigned before `PyType_Ready()`
|
||||
3. Docstrings don't contain null bytes or invalid UTF-8
|
||||
|
||||
---
|
||||
|
||||
- Close issues automatically in gitea by adding to the commit message "closes #X", where X is the issue number. This associates the issue closure with the specific commit, so granular commits are preferred. You should only use the MCP tool to close issues directly when discovering that the issue is already complete; when committing changes, always such "closes" (or the opposite, "reopens") references to related issues. If on a feature branch, the issue will be referenced by the commit, and when merged to master, the issue will be actually closed (or reopened).
|
||||
# The test will:
|
||||
# 1. Set up the scene during script execution
|
||||
# 2. Register a timer callback
|
||||
# 3. Game loop starts
|
||||
# 4. Timer fires after 100ms
|
||||
# 5. Test runs with full rendering available
|
||||
# 6. Test takes screenshots and validates behavior
|
||||
# 7. Test calls sys.exit() to terminate
|
||||
```
|
||||
201
CMakeLists.txt
|
|
@ -8,157 +8,49 @@ project(McRogueFace)
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
# Detect cross-compilation for Windows (MinGW)
|
||||
if(CMAKE_CROSSCOMPILING AND WIN32)
|
||||
set(MCRF_CROSS_WINDOWS TRUE)
|
||||
message(STATUS "Cross-compiling for Windows using MinGW")
|
||||
endif()
|
||||
|
||||
# Add include directories
|
||||
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
#include_directories(${CMAKE_SOURCE_DIR}/deps_linux/Python-3.11.1)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||
|
||||
# Python includes: use different paths for Windows vs Linux
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# Windows cross-compilation: use cpython headers with PC/pyconfig.h
|
||||
# Problem: Python.h uses #include "pyconfig.h" which finds Include/pyconfig.h (Linux) first
|
||||
# Solution: Use -include to force Windows pyconfig.h to be included first
|
||||
# This defines MS_WINDOWS before Python.h is processed, ensuring correct struct layouts
|
||||
add_compile_options(-include ${CMAKE_SOURCE_DIR}/deps/cpython/PC/pyconfig.h)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/PC) # For other Windows-specific headers
|
||||
# Also include SFML and libtcod Windows headers
|
||||
include_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/include)
|
||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/include)
|
||||
else()
|
||||
# Native builds (Linux/Windows): use existing Python setup
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
endif()
|
||||
|
||||
# ImGui and ImGui-SFML include directories
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/imgui-sfml)
|
||||
|
||||
# ImGui source files
|
||||
set(IMGUI_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_draw.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_tables.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui/imgui_widgets.cpp
|
||||
${CMAKE_SOURCE_DIR}/modules/imgui-sfml/imgui-SFML.cpp
|
||||
)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/Python)
|
||||
|
||||
# Collect all the source files
|
||||
file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||
|
||||
# Add ImGui sources to the build
|
||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||
|
||||
# Find OpenGL (required by ImGui-SFML)
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# For cross-compilation, OpenGL is provided by MinGW
|
||||
set(OPENGL_LIBRARIES opengl32)
|
||||
else()
|
||||
find_package(OpenGL REQUIRED)
|
||||
set(OPENGL_LIBRARIES OpenGL::GL)
|
||||
endif()
|
||||
|
||||
# Create a list of libraries to link against
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# MinGW cross-compilation: use full library names
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
libtcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
|
||||
# Add Windows system libraries needed by SFML and MinGW
|
||||
list(APPEND LINK_LIBS
|
||||
winmm # Windows multimedia (for audio)
|
||||
gdi32 # Graphics Device Interface
|
||||
ws2_32 # Winsock (networking, used by some deps)
|
||||
ole32 # OLE support
|
||||
oleaut32 # OLE automation
|
||||
uuid # UUID library
|
||||
comdlg32 # Common dialogs
|
||||
imm32 # Input Method Manager
|
||||
version # Version info
|
||||
)
|
||||
set(LINK_LIBS
|
||||
m
|
||||
dl
|
||||
util
|
||||
pthread
|
||||
python3.12
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod)
|
||||
|
||||
# On Windows, add any additional libs and include directories
|
||||
if(WIN32)
|
||||
# Add the necessary Windows-specific libraries and include directories
|
||||
# include_directories(path_to_additional_includes)
|
||||
# link_directories(path_to_additional_libs)
|
||||
# list(APPEND LINK_LIBS additional_windows_libs)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
|
||||
# Link directories for cross-compiled Windows libs
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/sfml/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
elseif(WIN32)
|
||||
# Native Windows build (MSVC)
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python314
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
else()
|
||||
# Unix/Linux build
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
|
||||
# Add the directory where the linker should look for the libraries
|
||||
#link_directories(${CMAKE_SOURCE_DIR}/deps_linux)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
|
||||
# Define the executable target before linking libraries
|
||||
add_executable(mcrogueface ${SOURCES})
|
||||
|
||||
# Define NO_SDL for libtcod-headless headers (excludes SDL-dependent code)
|
||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||
|
||||
# On Windows, define Py_ENABLE_SHARED for proper Python DLL imports
|
||||
# Py_PYCONFIG_H prevents Include/pyconfig.h (Linux config) from being included
|
||||
# (PC/pyconfig.h already defines HAVE_DECLSPEC_DLL and MS_WINDOWS)
|
||||
if(WIN32 OR MCRF_CROSS_WINDOWS)
|
||||
target_compile_definitions(mcrogueface PRIVATE Py_ENABLE_SHARED Py_PYCONFIG_H)
|
||||
endif()
|
||||
|
||||
# On Windows, set subsystem to WINDOWS to hide console (release builds only)
|
||||
# Use -DMCRF_WINDOWS_CONSOLE=ON for debug builds with console output
|
||||
option(MCRF_WINDOWS_CONSOLE "Keep console window visible for debugging" OFF)
|
||||
|
||||
if(WIN32 AND NOT MCRF_CROSS_WINDOWS)
|
||||
# MSVC-specific flags
|
||||
if(NOT MCRF_WINDOWS_CONSOLE)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
LINK_FLAGS "/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup")
|
||||
endif()
|
||||
elseif(MCRF_CROSS_WINDOWS)
|
||||
# MinGW cross-compilation
|
||||
if(NOT MCRF_WINDOWS_CONSOLE)
|
||||
# Release: use -mwindows to hide console
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
WIN32_EXECUTABLE TRUE
|
||||
LINK_FLAGS "-mwindows")
|
||||
else()
|
||||
# Debug: keep console for stdout/stderr output
|
||||
message(STATUS "Windows console enabled for debugging")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Now the linker will find the libraries in the specified directory
|
||||
target_link_libraries(mcrogueface ${LINK_LIBS})
|
||||
|
||||
|
|
@ -177,42 +69,7 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
|||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
|
||||
# On Windows, copy DLLs to executable directory
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
# Cross-compilation: copy DLLs from __lib_windows
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/sfml/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/bin $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python3.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/vcruntime140_1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
/usr/x86_64-w64-mingw32/lib/libwinpthread-1.dll $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Windows DLLs to executable directory")
|
||||
|
||||
# Copy Python standard library zip
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_windows/python314.zip $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied Python stdlib")
|
||||
elseif(WIN32)
|
||||
# Native Windows build: copy DLLs from __lib
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>
|
||||
COMMAND ${CMAKE_COMMAND} -E echo "Copied DLLs to executable directory")
|
||||
endif()
|
||||
|
||||
# rpath for including shared libraries (Linux/Unix only)
|
||||
if(NOT WIN32)
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/./lib")
|
||||
endif()
|
||||
# rpath for including shared libraries
|
||||
set_target_properties(mcrogueface PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/./lib")
|
||||
|
||||
|
|
|
|||
54
GNUmakefile
Normal file
|
|
@ -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"
|
||||
147
README.md
|
|
@ -1,32 +1,23 @@
|
|||
# McRogueFace
|
||||
*Blame my wife for the name*
|
||||
|
||||
A Python-powered 2D game engine for creating roguelike games, built with C++ and SFML.
|
||||
|
||||
* Core roguelike logic from libtcod: field of view, pathfinding
|
||||
* Animate sprites with multiple frames. Smooth transitions for positions, sizes, zoom, and camera
|
||||
* Simple GUI element system allows keyboard and mouse input, composition
|
||||
* No compilation or installation necessary. The runtime is a full Python environment; "Zip And Ship"
|
||||
**Latest Release**: Successfully completed 7DRL 2025 with *"Crypt of Sokoban"* - a unique roguelike that blends Sokoban puzzle mechanics with dungeon crawling!
|
||||
|
||||
![ Image ]()
|
||||
## Features
|
||||
|
||||
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
|
||||
- **Python-First Design**: Write your game logic in Python while leveraging C++ performance
|
||||
- **Rich UI System**: Sprites, Grids, Frames, and Captions with full animation support
|
||||
- **Entity-Component Architecture**: Flexible game object system with Python integration
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
|
||||
- **Automation API**: PyAutoGUI-compatible testing and demo recording
|
||||
- **Interactive Development**: Python REPL integration for live game debugging
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Download**:
|
||||
|
||||
- The entire McRogueFace visual framework:
|
||||
- **Sprite**: an image file or one sprite from a shared sprite sheet
|
||||
- **Caption**: load a font, display text
|
||||
- **Frame**: A rectangle; put other things on it to move or manage GUIs as modules
|
||||
- **Grid**: A 2D array of tiles with zoom + position control
|
||||
- **Entity**: Lives on a Grid, displays a sprite, and can have a perspective or move along a path
|
||||
- **Animation**: Change any property on any of the above over time
|
||||
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone <wherever you found this repo>
|
||||
git clone https://github.com/jmcb/McRogueFace.git
|
||||
cd McRogueFace
|
||||
make
|
||||
|
||||
|
|
@ -35,141 +26,53 @@ cd build
|
|||
./mcrogueface
|
||||
```
|
||||
|
||||
## Building from Source
|
||||
|
||||
For most users, pre-built releases are available. If you need to build from source:
|
||||
|
||||
### Quick Build (with pre-built dependencies)
|
||||
|
||||
Download `build_deps.tar.gz` from the releases page, then:
|
||||
|
||||
```bash
|
||||
git clone <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
tar -xzf /path/to/build_deps.tar.gz
|
||||
mkdir build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
make -j$(nproc)
|
||||
```
|
||||
|
||||
### Full Build (compiling all dependencies)
|
||||
|
||||
```bash
|
||||
git clone --recursive <repository-url> McRogueFace
|
||||
cd McRogueFace
|
||||
# See BUILD_FROM_SOURCE.md for complete instructions
|
||||
```
|
||||
|
||||
**[BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md)** - Complete build guide including:
|
||||
- System dependency installation
|
||||
- Compiling SFML, Python, and libtcod-headless from source
|
||||
- Creating `build_deps` archives for distribution
|
||||
- Troubleshooting common build issues
|
||||
|
||||
### System Requirements
|
||||
|
||||
- **Linux**: Debian/Ubuntu tested; other distros should work
|
||||
- **Windows**: Supported (see build guide for details)
|
||||
- **macOS**: Untested
|
||||
|
||||
## Example: Creating a Simple Scene
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a new scene
|
||||
intro = mcrfpy.Scene("intro")
|
||||
mcrfpy.createScene("intro")
|
||||
|
||||
# Add a text caption
|
||||
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
|
||||
caption.size = 48
|
||||
caption.fill_color = (255, 255, 255)
|
||||
caption = mcrfpy.Caption(50, 50, "Welcome to McRogueFace!")
|
||||
caption.font = mcrfpy.default_font
|
||||
caption.font_color = (255, 255, 255)
|
||||
|
||||
# Add to scene
|
||||
intro.children.append(caption)
|
||||
mcrfpy.sceneUI("intro").append(caption)
|
||||
|
||||
# Switch to the scene
|
||||
intro.activate()
|
||||
mcrfpy.setScene("intro")
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
### 📚 Developer Documentation
|
||||
For comprehensive documentation, tutorials, and API reference, visit:
|
||||
**[https://mcrogueface.github.io](https://mcrogueface.github.io)**
|
||||
|
||||
For comprehensive documentation about systems, architecture, and development workflows:
|
||||
|
||||
**[Project Wiki](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki)**
|
||||
|
||||
Key wiki pages:
|
||||
|
||||
- **[Home](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
|
||||
- **[Grid System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
|
||||
- **[Python Binding System](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
|
||||
- **[Performance and Profiling](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
|
||||
- **[Adding Python Bindings](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
|
||||
- **[Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap)** - All open issues organized by system
|
||||
|
||||
### 📖 Development Guides
|
||||
|
||||
In the repository root:
|
||||
|
||||
- **[CLAUDE.md](CLAUDE.md)** - Build instructions, testing guidelines, common tasks
|
||||
- **[ROADMAP.md](ROADMAP.md)** - Strategic vision and development phases
|
||||
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
|
||||
|
||||
## Build Requirements
|
||||
## Requirements
|
||||
|
||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||
- CMake 3.14+
|
||||
- Python 3.12+
|
||||
- SFML 2.6
|
||||
- SFML 2.5+
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output directory: zip + ship
|
||||
│ ├─ (*)assets/ # (copied location of assets)
|
||||
│ ├─ (*)scripts/ # (copied location of src/scripts)
|
||||
│ └─ lib/ # SFML, TCOD libraries, Python + standard library / modules
|
||||
├── deps/ # Python, SFML, and libtcod imports can be tossed in here to build
|
||||
│ └─ platform/ # windows, linux subdirectories for OS-specific cpython config
|
||||
├── docs/ # generated HTML, markdown docs
|
||||
│ └─ stubs/ # .pyi files for editor integration
|
||||
├── modules/ # git submodules, to build all of McRogueFace's dependencies from source
|
||||
├── src/ # C++ engine source
|
||||
│ └─ scripts/ # Python game scripts (copied during build)
|
||||
├── scripts/ # Python game scripts
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output directory
|
||||
└── tests/ # Automated test suite
|
||||
└── tools/ # For the McRogueFace ecosystem: docs generation
|
||||
```
|
||||
|
||||
If you are building McRogueFace to implement game logic or scene configuration in C++, you'll have to compile the project.
|
||||
|
||||
If you are writing a game in Python using McRogueFace, you only need to rename and zip/distribute the `build` directory.
|
||||
|
||||
## Philosophy
|
||||
|
||||
- **C++ every frame, Python every tick**: All rendering data is handled in C++. Structure your UI and program animations in Python, and they are rendered without Python. All game logic can be written in Python.
|
||||
- **No Compiling Required; Zip And Ship**: Implement your game objects with Python, zip up McRogueFace with your "game.py" to ship
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod
|
||||
- **Hands-Off Testing**: 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
|
||||
|
||||
## 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.
|
||||
|
||||
### Issue Tracking
|
||||
|
||||
The project uses [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for task tracking and bug reports. Issues are organized with labels:
|
||||
|
||||
- **System labels** (grid, animation, python-binding, etc.) - identify which codebase area
|
||||
- **Priority labels** (tier1-active, tier2-foundation, tier3-future) - development timeline
|
||||
- **Type labels** (Major Feature, Minor Feature, Bugfix, etc.) - effort and scope
|
||||
|
||||
See the [Issue Roadmap](https://gamedev.ffwf.net/gitea/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
|
||||
McRogueFace is under active development. Check the [ROADMAP.md](ROADMAP.md) for current priorities and open issues.
|
||||
|
||||
## License
|
||||
|
||||
|
|
@ -177,6 +80,6 @@ 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
|
||||
- Developed for 7-Day Roguelike Challenge 2025
|
||||
- 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
|
||||
- Inspired by David Churchill's COMP4300 game engine lectures
|
||||
531
ROADMAP.md
|
|
@ -1,223 +1,362 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
## Project Status
|
||||
## Project Status: 🎉 ALPHA 0.1 RELEASE! 🎉
|
||||
|
||||
**Current State**: Active development - C++ game engine with Python scripting
|
||||
**Latest Release**: Alpha 0.1
|
||||
**Issue Tracking**: See [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues) for current tasks and bugs
|
||||
**Current State**: Alpha release achieved! All critical blockers resolved!
|
||||
**Latest Update**: Moved RenderTexture (#6) to Beta - Alpha is READY! (2025-07-05)
|
||||
**Branch**: interpreter_mode (ready for alpha release merge)
|
||||
**Open Issues**: ~46 remaining (non-blocking quality-of-life improvements)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Strategic Vision
|
||||
## Recent Achievements
|
||||
|
||||
### Engine Philosophy
|
||||
### 2025-07-05: ALPHA 0.1 ACHIEVED! 🎊🍾
|
||||
**All Alpha Blockers Resolved!**
|
||||
- Z-order rendering with performance optimization (Issue #63)
|
||||
- Python Sequence Protocol for collections (Issue #69)
|
||||
- Comprehensive Animation System (Issue #59)
|
||||
- Moved RenderTexture to Beta (not needed for Alpha)
|
||||
- **McRogueFace is ready for Alpha release!**
|
||||
|
||||
### 2025-07-05: Z-order Rendering Complete! 🎉
|
||||
**Issue #63 Resolved**: Consistent z-order rendering with performance optimization
|
||||
- Dirty flag pattern prevents unnecessary per-frame sorting
|
||||
- Lazy sorting for both Scene elements and Frame children
|
||||
- Frame children now respect z_index (fixed inconsistency)
|
||||
- Automatic dirty marking on z_index changes and collection modifications
|
||||
- Performance: O(1) check for static scenes vs O(n log n) every frame
|
||||
|
||||
### 2025-07-05: Python Sequence Protocol Complete! 🎉
|
||||
**Issue #69 Resolved**: Full sequence protocol implementation for collections
|
||||
- Complete __setitem__, __delitem__, __contains__ support
|
||||
- Slice operations with extended slice support (step != 1)
|
||||
- Concatenation (+) and in-place concatenation (+=) with validation
|
||||
- Negative indexing throughout, index() and count() methods
|
||||
- Type safety: UICollection (Frame/Caption/Sprite/Grid), EntityCollection (Entity only)
|
||||
- Default value support: None for texture/font parameters uses engine defaults
|
||||
|
||||
### 2025-07-05: Animation System Complete! 🎉
|
||||
**Issue #59 Resolved**: Comprehensive animation system with 30+ easing functions
|
||||
- Property-based animations for all UI classes (Frame, Caption, Sprite, Grid, Entity)
|
||||
- Individual color component animation (r/g/b/a)
|
||||
- Sprite sequence animation and text typewriter effects
|
||||
- Pure C++ execution without Python callbacks
|
||||
- Delta animation support for relative values
|
||||
|
||||
### 2025-01-03: Major Stability Update
|
||||
**Major Cleanup**: Removed deprecated registerPyAction system (-180 lines)
|
||||
**Bug Fixes**: 12 critical issues including Grid segfault, Issue #78 (middle click), Entity setters
|
||||
**New Features**: Entity.index() (#73), EntityCollection.extend() (#27), Sprite validation (#33)
|
||||
**Test Coverage**: Comprehensive test suite with timer callback pattern established
|
||||
|
||||
---
|
||||
|
||||
## 🔧 CURRENT WORK: Python Interpreter Mode & Automation API
|
||||
|
||||
### Branch: interpreter_mode
|
||||
**Status**: Actively implementing Python interpreter emulation features
|
||||
|
||||
#### Completed Features:
|
||||
- [x] **--exec flag implementation** - Execute multiple scripts before main program
|
||||
- Scripts execute in order and share Python interpreter state
|
||||
- Proper sys.argv handling for main script execution
|
||||
- Compatible with -i (interactive), -c (command), and -m (module) modes
|
||||
|
||||
- [x] **PyAutoGUI-compatible Automation API** - Full automation testing capability
|
||||
- Screenshot capture: `automation.screenshot(filename)`
|
||||
- Mouse control: `click()`, `moveTo()`, `dragTo()`, `scroll()`
|
||||
- Keyboard input: `typewrite()`, `hotkey()`, `keyDown()`, `keyUp()`
|
||||
- Event injection into SFML render loop
|
||||
- **Enables**: Automated UI testing, demo recording/playback, accessibility testing
|
||||
|
||||
#### Architectural Decisions:
|
||||
1. **Single-threaded design maintained** - All Python runs in main thread between frames
|
||||
2. **Honor system for scripts** - Scripts must return control to C++ render loop
|
||||
3. **Shared Python state** - All --exec scripts share the same interpreter
|
||||
4. **No threading complexity** - Chose simplicity over parallelism (see THREADING_FOOTGUNS.md)
|
||||
5. **Animation system in pure C++** - All interpolation happens in C++ for performance
|
||||
6. **Property-based animation** - Unified interface for all UI element properties
|
||||
|
||||
#### Key Files Created:
|
||||
- `src/McRFPy_Automation.h/cpp` - Complete automation API implementation
|
||||
- `EXEC_FLAG_DOCUMENTATION.md` - Usage guide and examples
|
||||
- `AUTOMATION_ARCHITECTURE_REPORT.md` - Design analysis and alternatives
|
||||
- Multiple example scripts demonstrating automation patterns
|
||||
|
||||
#### Addresses:
|
||||
- **#32** - Executable behave like `python` command (90% complete - all major Python interpreter flags implemented)
|
||||
|
||||
#### Test Suite Results (2025-07-03):
|
||||
Created comprehensive test suite with 13 tests covering all Python-exposed methods:
|
||||
|
||||
**✅ Fixed Issues:**
|
||||
- Fixed `--exec` Python interactive prompt bug (was entering REPL instead of game loop)
|
||||
- Resolved screenshot transparency issue (must use timer callbacks for rendered content)
|
||||
- Updated CLAUDE.md with testing guidelines and patterns
|
||||
|
||||
**❌ Critical Bugs Found:**
|
||||
1. **SEGFAULT**: Grid class crashes on instantiation (blocks all Grid functionality)
|
||||
2. **#78 CONFIRMED**: Middle mouse click sends 'C' keyboard event
|
||||
3. **Entity property setters**: "new style getargs format" error
|
||||
4. **Sprite texture setter**: Returns "error return without exception set"
|
||||
5. **keypressScene()**: Segfaults on non-callable arguments
|
||||
|
||||
**📋 Missing Features Confirmed:**
|
||||
- #73: Entity.index() method
|
||||
- #27: EntityCollection.extend() method
|
||||
- #41: UICollection.find(name) method
|
||||
- #38: Frame 'children' constructor parameter
|
||||
- #33: Sprite index validation
|
||||
|
||||
---
|
||||
|
||||
## 🚀 NEXT PHASE: Beta Features & Polish
|
||||
|
||||
### Alpha Complete! Moving to Beta Priorities:
|
||||
1. ~~**#69** - Python Sequence Protocol for collections~~ - *Completed! (2025-07-05)*
|
||||
2. ~~**#63** - Z-order rendering for UIDrawables~~ - *Completed! (2025-07-05)*
|
||||
3. ~~**#59** - Animation system~~ - *Completed! (2025-07-05)*
|
||||
4. **#6** - RenderTexture concept - *Extensive Overhaul*
|
||||
5. ~~**#47** - New README.md for Alpha release~~ - *Completed*
|
||||
- [x] **#78** - Middle Mouse Click sends "C" keyboard event - *Fixed*
|
||||
- [x] **#77** - Fix error message copy/paste bug - *Fixed*
|
||||
- [x] **#74** - Add missing `Grid.grid_y` property - *Fixed*
|
||||
- [ ] **#37** - Fix Windows build module import from "scripts" directory - *Isolated Fix*
|
||||
- [x] **Entity Property Setters** - Fix "new style getargs format" error - *Fixed*
|
||||
- [x] **Sprite Texture Setter** - Fix "error return without exception set" - *Fixed*
|
||||
- [x] **keypressScene() Validation** - Add proper error handling - *Fixed*
|
||||
|
||||
### 🔄 Complete Iterator System
|
||||
**Status**: Core iterators complete (#72 closed), Grid point iterators still pending
|
||||
|
||||
- [ ] **Grid Point Iterator Implementation** - Complete the remaining grid iteration work
|
||||
- [x] **#73** - Add `entity.index()` method for collection removal - *Fixed*
|
||||
- [x] **#69** ⚠️ **Alpha Blocker** - Refactor all collections to use Python Sequence Protocol - *Completed! (2025-07-05)*
|
||||
|
||||
**Dependencies**: Grid point iterators → #73 entity.index() → #69 Sequence Protocol overhaul
|
||||
|
||||
---
|
||||
|
||||
## ✅ ALPHA 0.1 RELEASE ACHIEVED! (All Blockers Complete)
|
||||
|
||||
### ✅ All Alpha Requirements Complete!
|
||||
- [x] **#69** - Collections use Python Sequence Protocol - *Completed! (2025-07-05)*
|
||||
- [x] **#63** - Z-order rendering for UIDrawables - *Completed! (2025-07-05)*
|
||||
- [x] **#59** - Animation system for arbitrary UIDrawable fields - *Completed! (2025-07-05)*
|
||||
- [x] **#47** - New README.md for Alpha release - *Completed*
|
||||
- [x] **#3** - Remove deprecated `McRFPy_API::player_input` - *Completed*
|
||||
- [x] **#2** - Remove `registerPyAction` system - *Completed*
|
||||
|
||||
### 📋 Moved to Beta:
|
||||
- [ ] **#6** - RenderTexture concept - *Moved to Beta (not needed for Alpha)*
|
||||
|
||||
---
|
||||
|
||||
## 🗂 ISSUE TRIAGE BY SYSTEM (78 Total Issues)
|
||||
|
||||
### 🎮 Core Engine Systems
|
||||
|
||||
#### Iterator/Collection System (2 issues)
|
||||
- [x] **#73** - Entity index() method for removal - *Fixed*
|
||||
- [x] **#69** ⚠️ **Alpha Blocker** - Sequence Protocol refactor - *Completed! (2025-07-05)*
|
||||
|
||||
#### Python/C++ Integration (7 issues)
|
||||
- [ ] **#76** - UIEntity derived type preservation in collections - *Multiple Integrations*
|
||||
- [ ] **#71** - Drawable base class hierarchy - *Extensive Overhaul*
|
||||
- [ ] **#70** - PyPI wheel distribution - *Extensive Overhaul*
|
||||
- [~] **#32** - Executable behave like `python` command - *Extensive Overhaul* *(90% Complete: -h, -V, -c, -m, -i, script execution, sys.argv, --exec all implemented. Only stdin (-) support missing)*
|
||||
- [ ] **#35** - TCOD as built-in module - *Extensive Overhaul*
|
||||
- [ ] **#14** - Expose SFML as built-in module - *Extensive Overhaul*
|
||||
- [ ] **#46** - Subinterpreter threading tests - *Multiple Integrations*
|
||||
|
||||
#### UI/Rendering System (12 issues)
|
||||
- [ ] **#63** ⚠️ **Alpha Blocker** - Z-order for UIDrawables - *Multiple Integrations*
|
||||
- [x] **#59** ⚠️ **Alpha Blocker** - Animation system - *Completed! (2025-07-05)*
|
||||
- [ ] **#6** ⚠️ **Alpha Blocker** - RenderTexture for all UIDrawables - *Extensive Overhaul*
|
||||
- [ ] **#10** - UIDrawable visibility/AABB system - *Extensive Overhaul*
|
||||
- [ ] **#8** - UIGrid RenderTexture viewport sizing - *Multiple Integrations*
|
||||
- [ ] **#9** - UIGrid RenderTexture resize handling - *Multiple Integrations*
|
||||
- [ ] **#52** - UIGrid skip out-of-bounds entities - *Isolated Fix*
|
||||
- [ ] **#50** - UIGrid background color field - *Isolated Fix*
|
||||
- [ ] **#19** - Sprite get/set texture methods - *Multiple Integrations*
|
||||
- [ ] **#17** - Move UISprite position into sf::Sprite - *Isolated Fix*
|
||||
- [x] **#33** - Sprite index validation against texture range - *Fixed*
|
||||
|
||||
#### Grid/Entity System (6 issues)
|
||||
- [ ] **#30** - Entity/Grid association management (.die() method) - *Extensive Overhaul*
|
||||
- [ ] **#16** - Grid strict mode for entity knowledge/visibility - *Extensive Overhaul*
|
||||
- [ ] **#67** - Grid stitching for infinite worlds - *Extensive Overhaul*
|
||||
- [ ] **#15** - UIGridPointState cleanup and standardization - *Multiple Integrations*
|
||||
- [ ] **#20** - UIGrid get_grid_size standardization - *Multiple Integrations*
|
||||
- [ ] **#12** - GridPoint/GridPointState forbid direct init - *Isolated Fix*
|
||||
|
||||
#### Scene/Window Management (5 issues)
|
||||
- [ ] **#61** - Scene object encapsulating key callbacks - *Extensive Overhaul*
|
||||
- [ ] **#34** - Window object for resolution/scaling - *Extensive Overhaul*
|
||||
- [ ] **#62** - Multiple windows support - *Extensive Overhaul*
|
||||
- [ ] **#49** - Window resolution & viewport controls - *Multiple Integrations*
|
||||
- [ ] **#1** - Scene resize event handling - *Isolated Fix*
|
||||
|
||||
### 🔧 Quality of Life Features
|
||||
|
||||
#### UI Enhancement Features (8 issues)
|
||||
- [ ] **#39** - Name field on UIDrawables - *Multiple Integrations*
|
||||
- [ ] **#40** - `only_one` arg for unique naming - *Multiple Integrations*
|
||||
- [ ] **#41** - `.find(name)` method for collections - *Multiple Integrations*
|
||||
- [ ] **#38** - `children` arg for Frame initialization - *Isolated Fix*
|
||||
- [ ] **#42** - Click callback arg for UIDrawable init - *Isolated Fix*
|
||||
- [x] **#27** - UIEntityCollection.extend() method - *Fixed*
|
||||
- [ ] **#28** - UICollectionIter for scene ui iteration - *Isolated Fix*
|
||||
- [ ] **#26** - UIEntityCollectionIter implementation - *Isolated Fix*
|
||||
|
||||
### 🧹 Refactoring & Cleanup
|
||||
|
||||
#### Code Cleanup (7 issues)
|
||||
- [x] **#3** ⚠️ **Alpha Blocker** - Remove `McRFPy_API::player_input` - *Completed*
|
||||
- [x] **#2** ⚠️ **Alpha Blocker** - Review `registerPyAction` necessity - *Completed*
|
||||
- [ ] **#7** - Remove unsafe no-argument constructors - *Multiple Integrations*
|
||||
- [ ] **#21** - PyUIGrid dealloc cleanup - *Isolated Fix*
|
||||
- [ ] **#75** - REPL thread separation from SFML window - *Multiple Integrations*
|
||||
|
||||
### 📚 Demo & Documentation
|
||||
|
||||
#### Documentation (2 issues)
|
||||
- [ ] **#47** ⚠️ **Alpha Blocker** - Alpha release README.md - *Isolated Fix*
|
||||
- [ ] **#48** - Dependency compilation documentation - *Isolated Fix*
|
||||
|
||||
#### Demo Projects (6 issues)
|
||||
- [ ] **#54** - Jupyter notebook integration demo - *Multiple Integrations*
|
||||
- [ ] **#55** - Hunt the Wumpus AI demo - *Multiple Integrations*
|
||||
- [ ] **#53** - Web interface input demo - *Multiple Integrations* *(New automation API could help)*
|
||||
- [ ] **#45** - Accessibility mode demos - *Multiple Integrations* *(New automation API could help test)*
|
||||
- [ ] **#36** - Dear ImGui integration tests - *Extensive Overhaul*
|
||||
- [ ] **#65** - Python Explorer scene (replaces uitest) - *Extensive Overhaul*
|
||||
|
||||
---
|
||||
|
||||
## 🎯 RECOMMENDED TRIAGE SEQUENCE
|
||||
|
||||
### Phase 1: Foundation Stabilization (1-2 weeks)
|
||||
```
|
||||
✅ COMPLETE AS OF 2025-01-03:
|
||||
1. ✅ Fix Grid Segfault - Grid now supports None/null textures
|
||||
2. ✅ Fix #78 Middle Mouse Click bug - Event type checking added
|
||||
3. ✅ Fix Entity/Sprite property setters - PyVector conversion fixed
|
||||
4. ✅ Fix #77 - Error message copy/paste bug fixed
|
||||
5. ✅ Fix #74 - Grid.grid_y property added
|
||||
6. ✅ Fix keypressScene() validation - Now rejects non-callable
|
||||
7. ✅ Fix Sprite texture setter - No longer returns error without exception
|
||||
8. ✅ Fix PyVector x/y properties - Were returning None
|
||||
|
||||
REMAINING IN PHASE 1:
|
||||
9. ✅ Fix #73 - Entity.index() method for removal
|
||||
10. ✅ Fix #27 - EntityCollection.extend() method
|
||||
11. ✅ Fix #33 - Sprite index validation
|
||||
12. Alpha Blockers (#3, #2) - Remove deprecated methods
|
||||
```
|
||||
|
||||
### Phase 2: Alpha Release Preparation (4-6 weeks)
|
||||
```
|
||||
1. Collections Sequence Protocol (#69) - Major refactor, alpha blocker
|
||||
2. Z-order rendering (#63) - Essential UI improvement, alpha blocker
|
||||
3. RenderTexture overhaul (#6) - Core rendering improvement, alpha blocker
|
||||
4. ✅ Animation system (#59) - COMPLETE! 30+ easing functions, all UI properties
|
||||
5. ✅ Documentation (#47) - README.md complete, #48 dependency docs remaining
|
||||
```
|
||||
|
||||
### Phase 3: Engine Architecture (6-8 weeks)
|
||||
```
|
||||
1. Drawable base class (#71) - Clean up inheritance patterns
|
||||
2. Entity/Grid associations (#30) - Proper lifecycle management
|
||||
3. Window object (#34) - Scene/window architecture
|
||||
4. UIDrawable visibility (#10) - Rendering optimization
|
||||
```
|
||||
|
||||
### Phase 4: Advanced Features (8-12 weeks)
|
||||
```
|
||||
1. Grid strict mode (#16) - Entity knowledge/visibility system
|
||||
2. SFML/TCOD integration (#14, #35) - Expose native libraries
|
||||
3. Scene object refactor (#61) - Better input handling
|
||||
4. Name-based finding (#39, #40, #41) - UI element management
|
||||
5. Demo projects (#54, #55, #36) - Showcase capabilities
|
||||
```
|
||||
|
||||
### Ongoing/Low Priority
|
||||
```
|
||||
- PyPI distribution (#70) - Community access
|
||||
- Multiple windows (#62) - Advanced use cases
|
||||
- Grid stitching (#67) - Infinite world support
|
||||
- Accessibility (#45) - Important but not blocking
|
||||
- Subinterpreter tests (#46) - Performance research
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 DIFFICULTY ASSESSMENT SUMMARY
|
||||
|
||||
**Isolated Fixes (24 issues)**: Single file/function changes
|
||||
- Bugfixes: #77, #74, #37, #78
|
||||
- Simple features: #73, #52, #50, #33, #17, #38, #42, #27, #28, #26, #12, #1
|
||||
- Cleanup: #3, #2, #21, #47, #48
|
||||
|
||||
**Multiple Integrations (28 issues)**: Cross-system changes
|
||||
- UI/Rendering: #63, #8, #9, #19, #39, #40, #41
|
||||
- Grid/Entity: #15, #20, #76, #46, #49, #75
|
||||
- Features: #54, #55, #53, #45, #7
|
||||
|
||||
**Extensive Overhauls (26 issues)**: Major architectural changes
|
||||
- Core Systems: #69, #59, #6, #10, #30, #16, #67, #61, #34, #62
|
||||
- Integration: #71, #70, #32, #35, #14
|
||||
- Advanced: #36, #65
|
||||
|
||||
---
|
||||
|
||||
## 🎮 STRATEGIC DIRECTION
|
||||
|
||||
### Engine Philosophy Maintained
|
||||
- **C++ First**: Performance-critical code stays in C++
|
||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
||||
- **Python Close Behind**: Rich scripting without frame-rate impact
|
||||
- **Game-Ready**: Each improvement should benefit actual game development
|
||||
|
||||
### Architecture Goals
|
||||
|
||||
1. **Clean Inheritance**: Drawable → UI components, proper type preservation
|
||||
2. **Collection Consistency**: Uniform iteration, indexing, and search patterns
|
||||
3. **Resource Management**: RAII everywhere, proper lifecycle handling
|
||||
4. **Multi-Platform**: Windows/Linux feature parity maintained
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Architecture Decisions
|
||||
|
||||
### Three-Layer Grid Architecture
|
||||
Following successful roguelike patterns (Caves of Qud, Cogmind, DCSS):
|
||||
|
||||
1. **Visual Layer** (UIGridPoint) - Sprites, colors, animations
|
||||
2. **World State Layer** (TCODMap) - Walkability, transparency, physics
|
||||
3. **Entity Perspective Layer** (UIGridPointState) - Per-entity FOV, knowledge
|
||||
|
||||
### Performance Architecture
|
||||
Critical for large maps (1000x1000):
|
||||
|
||||
- **Spatial Hashing** for entity queries (not quadtrees!)
|
||||
- **Batch Operations** with context managers (10-100x speedup)
|
||||
- **Memory Pooling** for entities and components
|
||||
- **Dirty Flag System** to avoid unnecessary updates
|
||||
- **Zero-Copy NumPy Integration** via buffer protocol
|
||||
|
||||
### Key Insight from Research
|
||||
"Minimizing Python/C++ boundary crossings matters more than individual function complexity"
|
||||
- Batch everything possible
|
||||
- Use context managers for logical operations
|
||||
- Expose arrays, not individual cells
|
||||
- Profile and optimize hot paths only
|
||||
### Success Metrics for Alpha 0.1
|
||||
- [ ] All Alpha Blocker issues resolved (5 of 7 complete: #69, #59, #47, #3, #2)
|
||||
- [ ] Grid point iteration complete and tested
|
||||
- [ ] Clean build on Windows and Linux
|
||||
- [ ] Documentation sufficient for external developers
|
||||
- [ ] At least one compelling demo (Wumpus or Jupyter integration)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Development Phases
|
||||
## 📚 REFERENCES & CONTEXT
|
||||
|
||||
For detailed task tracking and current priorities, see the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).
|
||||
**Issue Dependencies** (Key Chains):
|
||||
- Iterator System: Grid points → #73 → #69 (Alpha Blocker)
|
||||
- UI Hierarchy: #71 → #63 (Alpha Blocker)
|
||||
- Rendering: #6 (Alpha Blocker) → #8, #9 → #10
|
||||
- Entity System: #30 → #16 → #67
|
||||
- Window Management: #34 → #49, #61 → #62
|
||||
|
||||
### Phase 1: Foundation Stabilization ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #7 (Safe Constructors), #71 (Base Class), #87 (Visibility), #88 (Opacity)
|
||||
**Commit References**:
|
||||
- 167636c: Iterator improvements (UICollection/UIEntityCollection complete)
|
||||
- Recent work: 7DRL 2025 completion, RPATH updates, console improvements
|
||||
|
||||
### Phase 2: Constructor & API Polish ✅
|
||||
**Status**: Complete
|
||||
**Key Features**: Pythonic API, tuple support, standardized defaults
|
||||
|
||||
### Phase 3: Entity Lifecycle Management ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #30 (Entity.die()), #93 (Vector methods), #94 (Color helpers), #103 (Timer objects)
|
||||
|
||||
### Phase 4: Visibility & Performance ✅
|
||||
**Status**: Complete
|
||||
**Key Features**: AABB culling, name system, profiling tools
|
||||
|
||||
### Phase 5: Window/Scene Architecture ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #34 (Window object), #61 (Scene object), #1 (Resize events), #105 (Scene transitions)
|
||||
|
||||
### Phase 6: Rendering Revolution ✅
|
||||
**Status**: Complete
|
||||
**Key Issues**: #50 (Grid backgrounds), #6 (RenderTexture), #8 (Viewport rendering)
|
||||
|
||||
### Phase 7: Documentation & Distribution ✅
|
||||
**Status**: Complete (2025-10-30)
|
||||
**Key Issues**: #85 (Docstrings), #86 (Parameter docs), #108 (Type stubs), #97 (API docs)
|
||||
**Completed**: All classes and functions converted to MCRF_* macro system with automated HTML/Markdown/man page generation
|
||||
|
||||
See [current open issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues?state=open) for active work.
|
||||
**Architecture Files**:
|
||||
- Iterator patterns: src/UICollection.cpp, src/UIGrid.cpp
|
||||
- Python integration: src/McRFPy_API.cpp, src/PyObjectUtils.h
|
||||
- Game implementation: src/scripts/ (Crypt of Sokoban complete game)
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Future Vision: Pure Python Extension Architecture
|
||||
*Last Updated: 2025-07-05*
|
||||
*Total Open Issues: 62* (from original 78)
|
||||
*Alpha Status: 🎉 COMPLETE! All blockers resolved!*
|
||||
*Achievement Unlocked: Alpha 0.1 Release Ready*
|
||||
*Next Phase: Beta features including RenderTexture (#6), advanced UI patterns, and platform polish*
|
||||
|
||||
### Concept: McRogueFace as a Traditional Python Package
|
||||
**Status**: Long-term vision
|
||||
**Complexity**: Major architectural overhaul
|
||||
|
||||
Instead of being a C++ application that embeds Python, McRogueFace could be redesigned as a pure Python extension module that can be installed via `pip install mcrogueface`.
|
||||
|
||||
### Technical Approach
|
||||
|
||||
1. **Separate Core Engine from Python Embedding**
|
||||
- Extract SFML rendering, audio, and input into C++ extension modules
|
||||
- Remove embedded CPython interpreter
|
||||
- Use Python's C API to expose functionality
|
||||
|
||||
2. **Module Structure**
|
||||
```
|
||||
mcrfpy/
|
||||
├── __init__.py # Pure Python coordinator
|
||||
├── _core.so # C++ rendering/game loop extension
|
||||
├── _sfml.so # SFML bindings
|
||||
├── _audio.so # Audio system bindings
|
||||
└── engine.py # Python game engine logic
|
||||
```
|
||||
|
||||
3. **Inverted Control Flow**
|
||||
- Python drives the main loop instead of C++
|
||||
- C++ extensions handle performance-critical operations
|
||||
- Python manages game logic, scenes, and entity systems
|
||||
|
||||
### Benefits
|
||||
|
||||
- **Standard Python Packaging**: `pip install mcrogueface`
|
||||
- **Virtual Environment Support**: Works with venv, conda, poetry
|
||||
- **Better IDE Integration**: Standard Python development workflow
|
||||
- **Easier Testing**: Use pytest, standard Python testing tools
|
||||
- **Cross-Python Compatibility**: Support multiple Python versions
|
||||
- **Modular Architecture**: Users can import only what they need
|
||||
|
||||
### Challenges
|
||||
|
||||
- **Major Refactoring**: Complete restructure of codebase
|
||||
- **Performance Considerations**: Python-driven main loop overhead
|
||||
- **Build Complexity**: Multiple extension modules to compile
|
||||
- **Platform Support**: Need wheels for many platform/Python combinations
|
||||
- **API Stability**: Would need careful design to maintain compatibility
|
||||
|
||||
### Example Usage (Future Vision)
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
from mcrfpy import Scene, Frame, Sprite, Grid
|
||||
|
||||
# Create game directly in Python
|
||||
game = mcrfpy.Game(width=1024, height=768)
|
||||
|
||||
# Define scenes using Python classes
|
||||
class MainMenu(Scene):
|
||||
def on_enter(self):
|
||||
self.ui.append(Frame(100, 100, 200, 50))
|
||||
self.ui.append(Sprite("logo.png", x=400, y=100))
|
||||
|
||||
def on_keypress(self, key, pressed):
|
||||
if key == "ENTER" and pressed:
|
||||
self.game.set_scene("game")
|
||||
|
||||
# Run the game
|
||||
game.add_scene("menu", MainMenu())
|
||||
game.run()
|
||||
```
|
||||
|
||||
This architecture would make McRogueFace a first-class Python citizen, following standard Python packaging conventions while maintaining high performance through C++ extensions.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Major Feature Areas
|
||||
|
||||
For current status and detailed tasks, see the corresponding Gitea issue labels:
|
||||
|
||||
### Core Systems
|
||||
- **UI/Rendering System**: Issues tagged `[Major Feature]` related to rendering
|
||||
- **Grid/Entity System**: Pathfinding, FOV, entity management
|
||||
- **Animation System**: Property animation, easing functions, callbacks
|
||||
- **Scene/Window Management**: Scene lifecycle, transitions, viewport
|
||||
|
||||
### Performance Optimization
|
||||
- **#115**: SpatialHash for 10,000+ entities
|
||||
- **#116**: Dirty flag system
|
||||
- **#113**: Batch operations for NumPy-style access
|
||||
- **#117**: Memory pool for entities
|
||||
|
||||
### Advanced Features
|
||||
- **#118**: Scene as Drawable (scenes can be drawn/animated)
|
||||
- **#122**: Parent-Child UI System
|
||||
- **#123**: Grid Subgrid System (256x256 chunks)
|
||||
- **#124**: Grid Point Animation
|
||||
- **#106**: Shader support
|
||||
- **#107**: Particle system
|
||||
|
||||
### Documentation
|
||||
- **#92**: Inline C++ documentation system
|
||||
- **#91**: Python type stub files (.pyi)
|
||||
- **#97**: Automated API documentation extraction
|
||||
- **#126**: Generate perfectly consistent Python interface
|
||||
|
||||
---
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
- **Issue Tracker**: [Gitea Issues](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues)
|
||||
- **Source Code**: [Gitea Repository](https://gamedev.ffwf.net/gitea/john/McRogueFace)
|
||||
- **Documentation**: See `CLAUDE.md` for build instructions and development guide
|
||||
- **Tutorial**: See `roguelike_tutorial/` for implementation examples
|
||||
- **Workflow**: See "Gitea-First Workflow" section in `CLAUDE.md` for issue management best practices
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Development Workflow
|
||||
|
||||
**Gitea is the Single Source of Truth** for this project. Before starting any work:
|
||||
|
||||
1. **Check Gitea Issues** for existing tasks, bugs, or related work
|
||||
2. **Create granular issues** for new features or problems
|
||||
3. **Update issues** when work affects other systems
|
||||
4. **Document discoveries** - if something is undocumented or misleading, create a task to fix it
|
||||
5. **Cross-reference commits** with issue numbers (e.g., "Fixes #104")
|
||||
|
||||
See the "Gitea-First Workflow" section in `CLAUDE.md` for detailed guidelines on efficient development practices using the Gitea MCP tools.
|
||||
|
||||
---
|
||||
|
||||
*For current priorities, task tracking, and bug reports, please use the [Gitea issue tracker](https://gamedev.ffwf.net/gitea/john/McRogueFace/issues).*
|
||||
|
|
|
|||
BIN
assets/48px_ui_icons-KenneyNL.png
Normal file
|
After Width: | Height: | Size: 181 KiB |
BIN
assets/Sprite-0001.ase
Normal file
BIN
assets/Sprite-0001.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
assets/alives_other.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
assets/boom.wav
Normal file
BIN
assets/custom_player.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
assets/gamescale_buildings.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/gamescale_decor.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/kenney_TD_MR_IP.png
Normal file
|
After Width: | Height: | Size: 674 KiB |
BIN
assets/sfx/splat1.ogg
Normal file
BIN
assets/sfx/splat2.ogg
Normal file
BIN
assets/sfx/splat3.ogg
Normal file
BIN
assets/sfx/splat4.ogg
Normal file
BIN
assets/sfx/splat5.ogg
Normal file
BIN
assets/sfx/splat6.ogg
Normal file
BIN
assets/sfx/splat7.ogg
Normal file
BIN
assets/sfx/splat8.ogg
Normal file
BIN
assets/sfx/splat9.ogg
Normal file
BIN
assets/temp_logo.png
Normal file
|
After Width: | Height: | Size: 3 MiB |
BIN
assets/terrain.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
assets/terrain_alpha.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
assets/test_portraits.ase
Normal file
BIN
assets/test_portraits.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
127
automation_example.py
Normal file
|
|
@ -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!")
|
||||
336
automation_exec_examples.py
Normal file
|
|
@ -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")
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script for McRogueFace
|
||||
REM Run this over SSH without Visual Studio GUI
|
||||
|
||||
echo Building McRogueFace for Windows...
|
||||
|
||||
REM Clean previous build
|
||||
if exist build_win rmdir /s /q build_win
|
||||
mkdir build_win
|
||||
cd build_win
|
||||
|
||||
REM Generate Visual Studio project files with CMake
|
||||
REM Use -G to specify generator, -A for architecture
|
||||
REM Visual Studio 2022 = "Visual Studio 17 2022"
|
||||
REM Visual Studio 2019 = "Visual Studio 16 2019"
|
||||
cmake -G "Visual Studio 17 2022" -A x64 ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using MSBuild (comes with Visual Studio)
|
||||
REM You can also use cmake --build . --config Release
|
||||
msbuild McRogueFace.sln /p:Configuration=Release /p:Platform=x64 /m
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build completed successfully!
|
||||
echo Executable location: build_win\Release\mcrogueface.exe
|
||||
|
||||
REM Alternative: Using cmake to build (works with any generator)
|
||||
REM cmake --build . --config Release --parallel
|
||||
|
||||
cd ..
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
@echo off
|
||||
REM Windows build script using cmake --build (generator-agnostic)
|
||||
REM This version works with any CMake generator
|
||||
|
||||
echo Building McRogueFace for Windows using CMake...
|
||||
|
||||
REM Set build directory
|
||||
set BUILD_DIR=build_win
|
||||
set CONFIG=Release
|
||||
|
||||
REM Clean previous build
|
||||
if exist %BUILD_DIR% rmdir /s /q %BUILD_DIR%
|
||||
mkdir %BUILD_DIR%
|
||||
cd %BUILD_DIR%
|
||||
|
||||
REM Configure with CMake
|
||||
REM You can change the generator here if needed:
|
||||
REM -G "Visual Studio 17 2022" (VS 2022)
|
||||
REM -G "Visual Studio 16 2019" (VS 2019)
|
||||
REM -G "MinGW Makefiles" (MinGW)
|
||||
REM -G "Ninja" (Ninja build system)
|
||||
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% ..
|
||||
if errorlevel 1 (
|
||||
echo CMake configuration failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build using cmake (works with any generator)
|
||||
cmake --build . --config %CONFIG% --parallel
|
||||
if errorlevel 1 (
|
||||
echo Build failed!
|
||||
cd ..
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Build completed successfully!
|
||||
echo Executable: %BUILD_DIR%\%CONFIG%\mcrogueface.exe
|
||||
echo.
|
||||
|
||||
cd ..
|
||||
33
clean.sh
Executable file
|
|
@ -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}"
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
# CMake toolchain file for cross-compiling to Windows using MinGW-w64
|
||||
# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64-x86_64.cmake ..
|
||||
|
||||
set(CMAKE_SYSTEM_NAME Windows)
|
||||
set(CMAKE_SYSTEM_PROCESSOR x86_64)
|
||||
|
||||
# Specify the cross-compiler (use posix variant for std::mutex support)
|
||||
set(CMAKE_C_COMPILER x86_64-w64-mingw32-gcc-posix)
|
||||
set(CMAKE_CXX_COMPILER x86_64-w64-mingw32-g++-posix)
|
||||
set(CMAKE_RC_COMPILER x86_64-w64-mingw32-windres)
|
||||
|
||||
# Target environment location
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/x86_64-w64-mingw32)
|
||||
|
||||
# Add MinGW system include directories for Windows headers
|
||||
include_directories(SYSTEM /usr/x86_64-w64-mingw32/include)
|
||||
|
||||
# Adjust search behavior
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
|
||||
# Static linking of libgcc and libstdc++ to avoid runtime dependency issues
|
||||
# Enable auto-import for Python DLL data symbols
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "-static-libgcc -static-libstdc++ -Wl,--enable-auto-import")
|
||||
|
||||
# Windows-specific defines
|
||||
add_definitions(-DWIN32 -D_WIN32 -D_WINDOWS)
|
||||
add_definitions(-DMINGW_HAS_SECURE_API)
|
||||
|
||||
# Disable console window for GUI applications (optional, can be overridden)
|
||||
# set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -mwindows")
|
||||
157
css_colors.txt
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
aqua #00FFFF
|
||||
black #000000
|
||||
blue #0000FF
|
||||
fuchsia #FF00FF
|
||||
gray #808080
|
||||
green #008000
|
||||
lime #00FF00
|
||||
maroon #800000
|
||||
navy #000080
|
||||
olive #808000
|
||||
purple #800080
|
||||
red #FF0000
|
||||
silver #C0C0C0
|
||||
teal #008080
|
||||
white #FFFFFF
|
||||
yellow #FFFF00
|
||||
aliceblue #F0F8FF
|
||||
antiquewhite #FAEBD7
|
||||
aqua #00FFFF
|
||||
aquamarine #7FFFD4
|
||||
azure #F0FFFF
|
||||
beige #F5F5DC
|
||||
bisque #FFE4C4
|
||||
black #000000
|
||||
blanchedalmond #FFEBCD
|
||||
blue #0000FF
|
||||
blueviolet #8A2BE2
|
||||
brown #A52A2A
|
||||
burlywood #DEB887
|
||||
cadetblue #5F9EA0
|
||||
chartreuse #7FFF00
|
||||
chocolate #D2691E
|
||||
coral #FF7F50
|
||||
cornflowerblue #6495ED
|
||||
cornsilk #FFF8DC
|
||||
crimson #DC143C
|
||||
cyan #00FFFF
|
||||
darkblue #00008B
|
||||
darkcyan #008B8B
|
||||
darkgoldenrod #B8860B
|
||||
darkgray #A9A9A9
|
||||
darkgreen #006400
|
||||
darkkhaki #BDB76B
|
||||
darkmagenta #8B008B
|
||||
darkolivegreen #556B2F
|
||||
darkorange #FF8C00
|
||||
darkorchid #9932CC
|
||||
darkred #8B0000
|
||||
darksalmon #E9967A
|
||||
darkseagreen #8FBC8F
|
||||
darkslateblue #483D8B
|
||||
darkslategray #2F4F4F
|
||||
darkturquoise #00CED1
|
||||
darkviolet #9400D3
|
||||
deeppink #FF1493
|
||||
deepskyblue #00BFFF
|
||||
dimgray #696969
|
||||
dodgerblue #1E90FF
|
||||
firebrick #B22222
|
||||
floralwhite #FFFAF0
|
||||
forestgreen #228B22
|
||||
fuchsia #FF00FF
|
||||
gainsboro #DCDCDC
|
||||
ghostwhite #F8F8FF
|
||||
gold #FFD700
|
||||
goldenrod #DAA520
|
||||
gray #7F7F7F
|
||||
green #008000
|
||||
greenyellow #ADFF2F
|
||||
honeydew #F0FFF0
|
||||
hotpink #FF69B4
|
||||
indianred #CD5C5C
|
||||
indigo #4B0082
|
||||
ivory #FFFFF0
|
||||
khaki #F0E68C
|
||||
lavender #E6E6FA
|
||||
lavenderblush #FFF0F5
|
||||
lawngreen #7CFC00
|
||||
lemonchiffon #FFFACD
|
||||
lightblue #ADD8E6
|
||||
lightcoral #F08080
|
||||
lightcyan #E0FFFF
|
||||
lightgoldenrodyellow #FAFAD2
|
||||
lightgreen #90EE90
|
||||
lightgrey #D3D3D3
|
||||
lightpink #FFB6C1
|
||||
lightsalmon #FFA07A
|
||||
lightseagreen #20B2AA
|
||||
lightskyblue #87CEFA
|
||||
lightslategray #778899
|
||||
lightsteelblue #B0C4DE
|
||||
lightyellow #FFFFE0
|
||||
lime #00FF00
|
||||
limegreen #32CD32
|
||||
linen #FAF0E6
|
||||
magenta #FF00FF
|
||||
maroon #800000
|
||||
mediumaquamarine #66CDAA
|
||||
mediumblue #0000CD
|
||||
mediumorchid #BA55D3
|
||||
mediumpurple #9370DB
|
||||
mediumseagreen #3CB371
|
||||
mediumslateblue #7B68EE
|
||||
mediumspringgreen #00FA9A
|
||||
mediumturquoise #48D1CC
|
||||
mediumvioletred #C71585
|
||||
midnightblue #191970
|
||||
mintcream #F5FFFA
|
||||
mistyrose #FFE4E1
|
||||
moccasin #FFE4B5
|
||||
navajowhite #FFDEAD
|
||||
navy #000080
|
||||
navyblue #9FAFDF
|
||||
oldlace #FDF5E6
|
||||
olive #808000
|
||||
olivedrab #6B8E23
|
||||
orange #FFA500
|
||||
orangered #FF4500
|
||||
orchid #DA70D6
|
||||
palegoldenrod #EEE8AA
|
||||
palegreen #98FB98
|
||||
paleturquoise #AFEEEE
|
||||
palevioletred #DB7093
|
||||
papayawhip #FFEFD5
|
||||
peachpuff #FFDAB9
|
||||
peru #CD853F
|
||||
pink #FFC0CB
|
||||
plum #DDA0DD
|
||||
powderblue #B0E0E6
|
||||
purple #800080
|
||||
red #FF0000
|
||||
rosybrown #BC8F8F
|
||||
royalblue #4169E1
|
||||
saddlebrown #8B4513
|
||||
salmon #FA8072
|
||||
sandybrown #FA8072
|
||||
seagreen #2E8B57
|
||||
seashell #FFF5EE
|
||||
sienna #A0522D
|
||||
silver #C0C0C0
|
||||
skyblue #87CEEB
|
||||
slateblue #6A5ACD
|
||||
slategray #708090
|
||||
snow #FFFAFA
|
||||
springgreen #00FF7F
|
||||
steelblue #4682B4
|
||||
tan #D2B48C
|
||||
teal #008080
|
||||
thistle #D8BFD8
|
||||
tomato #FF6347
|
||||
turquoise #40E0D0
|
||||
violet #EE82EE
|
||||
wheat #F5DEB3
|
||||
white #FFFFFF
|
||||
whitesmoke #F5F5F5
|
||||
yellow #FFFF00
|
||||
yellowgreen #9ACD32
|
||||
BIN
debug_immediate.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
debug_multi_0.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
debug_multi_1.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
debug_multi_2.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
8
deps/platform/windows/platform.h
vendored
|
|
@ -1,12 +1,12 @@
|
|||
#ifndef __PLATFORM
|
||||
#define __PLATFORM
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
||||
#include <windows.h>
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 0
|
||||
#include <Windows.h>
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring exec_path = buffer;
|
||||
size_t path_index = exec_path.find_last_of(L"\\/");
|
||||
return exec_path.substr(0, path_index);
|
||||
|
|
@ -15,7 +15,7 @@ std::wstring executable_path()
|
|||
std::wstring executable_filename()
|
||||
{
|
||||
wchar_t buffer[MAX_PATH];
|
||||
GetModuleFileNameW(NULL, buffer, MAX_PATH); // Use explicit Unicode version
|
||||
GetModuleFileName(NULL, buffer, MAX_PATH);
|
||||
std::wstring exec_path = buffer;
|
||||
return exec_path;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
"""McRogueFace - Animated Movement (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
if new_x != current_x:
|
||||
anim = mcrfpy.Animation("x", float(new_x), duration, "easeInOut", callback=done)
|
||||
else:
|
||||
anim = mcrfpy.Animation("y", float(new_y), duration, "easeInOut", callback=done)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
"""McRogueFace - Animated Movement (basic_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_animated_movement
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_animated_movement_basic_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
current_anim = mcrfpy.Animation("x", 100.0, 0.5, "linear")
|
||||
current_anim.start(entity)
|
||||
# Later: current_anim = None # Let it complete or create new one
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import random
|
||||
|
||||
def wander(enemy, grid):
|
||||
"""Move randomly to an adjacent walkable tile."""
|
||||
ex, ey = int(enemy.x), int(enemy.y)
|
||||
|
||||
# Get valid adjacent tiles
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
random.shuffle(directions)
|
||||
|
||||
for dx, dy in directions:
|
||||
new_x, new_y = ex + dx, ey + dy
|
||||
|
||||
if is_walkable(grid, new_x, new_y) and not is_occupied(new_x, new_y):
|
||||
enemy.x = new_x
|
||||
enemy.y = new_y
|
||||
return
|
||||
|
||||
# No valid moves - stay in place
|
||||
|
||||
def is_walkable(grid, x, y):
|
||||
"""Check if a tile can be walked on."""
|
||||
grid_w, grid_h = grid.grid_size
|
||||
if x < 0 or x >= grid_w or y < 0 or y >= grid_h:
|
||||
return False
|
||||
return grid.at(x, y).walkable
|
||||
|
||||
def is_occupied(x, y, entities=None):
|
||||
"""Check if a tile is occupied by another entity."""
|
||||
if entities is None:
|
||||
return False
|
||||
|
||||
for entity in entities:
|
||||
if int(entity.x) == x and int(entity.y) == y:
|
||||
return True
|
||||
return False
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Filter to cardinal directions only
|
||||
path = [p for p in path if abs(p[0] - ex) + abs(p[1] - ey) == 1]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""McRogueFace - Basic Enemy AI (multi_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_enemy_ai
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_enemy_ai_multi_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def alert_nearby(x, y, radius, enemies):
|
||||
for enemy in enemies:
|
||||
dist = abs(enemy.entity.x - x) + abs(enemy.entity.y - y)
|
||||
if dist <= radius and hasattr(enemy.ai, 'alert'):
|
||||
enemy.ai.alert = True
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class CombatLog:
|
||||
"""Scrolling combat message log."""
|
||||
|
||||
def __init__(self, x, y, width, height, max_messages=10):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.max_messages = max_messages
|
||||
self.messages = []
|
||||
self.captions = []
|
||||
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Background
|
||||
self.frame = mcrfpy.Frame(x, y, width, height)
|
||||
self.frame.fill_color = mcrfpy.Color(0, 0, 0, 180)
|
||||
ui.append(self.frame)
|
||||
|
||||
def add_message(self, text, color=None):
|
||||
"""Add a message to the log."""
|
||||
if color is None:
|
||||
color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self.messages.append((text, color))
|
||||
|
||||
# Keep only recent messages
|
||||
if len(self.messages) > self.max_messages:
|
||||
self.messages.pop(0)
|
||||
|
||||
self._refresh_display()
|
||||
|
||||
def _refresh_display(self):
|
||||
"""Redraw all messages."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Remove old captions
|
||||
for caption in self.captions:
|
||||
try:
|
||||
ui.remove(caption)
|
||||
except:
|
||||
pass
|
||||
self.captions.clear()
|
||||
|
||||
# Create new captions
|
||||
line_height = 18
|
||||
for i, (text, color) in enumerate(self.messages):
|
||||
caption = mcrfpy.Caption(text, self.x + 5, self.y + 5 + i * line_height)
|
||||
caption.fill_color = color
|
||||
ui.append(caption)
|
||||
self.captions.append(caption)
|
||||
|
||||
def log_attack(self, attacker_name, defender_name, damage, killed=False, critical=False):
|
||||
"""Log an attack event."""
|
||||
if critical:
|
||||
text = f"{attacker_name} CRITS {defender_name} for {damage}!"
|
||||
color = mcrfpy.Color(255, 255, 0)
|
||||
else:
|
||||
text = f"{attacker_name} hits {defender_name} for {damage}."
|
||||
color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self.add_message(text, color)
|
||||
|
||||
if killed:
|
||||
self.add_message(f"{defender_name} is defeated!", mcrfpy.Color(255, 100, 100))
|
||||
|
||||
|
||||
# Global combat log
|
||||
combat_log = None
|
||||
|
||||
def init_combat_log():
|
||||
global combat_log
|
||||
combat_log = CombatLog(10, 500, 400, 200)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def die_with_animation(entity):
|
||||
# Play death animation
|
||||
anim = mcrfpy.Animation("opacity", 0.0, 0.5, "linear")
|
||||
anim.start(entity)
|
||||
# Remove after animation
|
||||
mcrfpy.setTimer("remove", lambda dt: remove_entity(entity), 500)
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
"""McRogueFace - Melee Combat System (complete_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_melee
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_melee_complete_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
@dataclass
|
||||
class AdvancedFighter(Fighter):
|
||||
fire_resist: float = 0.0
|
||||
ice_resist: float = 0.0
|
||||
physical_resist: float = 0.0
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class StackableEffect(StatusEffect):
|
||||
"""Effect that stacks intensity."""
|
||||
|
||||
def __init__(self, name, duration, intensity=1, max_stacks=5, **kwargs):
|
||||
super().__init__(name, duration, **kwargs)
|
||||
self.intensity = intensity
|
||||
self.max_stacks = max_stacks
|
||||
self.stacks = 1
|
||||
|
||||
def add_stack(self):
|
||||
"""Add another stack."""
|
||||
if self.stacks < self.max_stacks:
|
||||
self.stacks += 1
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class StackingEffectManager(EffectManager):
|
||||
"""Effect manager with stacking support."""
|
||||
|
||||
def add_effect(self, effect):
|
||||
if isinstance(effect, StackableEffect):
|
||||
# Check for existing stacks
|
||||
for existing in self.effects:
|
||||
if existing.name == effect.name:
|
||||
if existing.add_stack():
|
||||
# Refresh duration
|
||||
existing.duration = max(existing.duration, effect.duration)
|
||||
return
|
||||
else:
|
||||
return # Max stacks
|
||||
|
||||
# Default behavior
|
||||
super().add_effect(effect)
|
||||
|
||||
|
||||
# Stacking poison example
|
||||
def create_stacking_poison(base_damage=1, duration=5):
|
||||
def on_tick(target):
|
||||
# Find the poison effect to get stack count
|
||||
effect = target.effects.get_effect("poison")
|
||||
if effect:
|
||||
damage = base_damage * effect.stacks
|
||||
target.hp -= damage
|
||||
print(f"{target.name} takes {damage} poison damage! ({effect.stacks} stacks)")
|
||||
|
||||
return StackableEffect("poison", duration, on_tick=on_tick, max_stacks=5)
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic_2)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_2.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def apply_effect(self, effect):
|
||||
if effect.name in self.immunities:
|
||||
print(f"{self.name} is immune to {effect.name}!")
|
||||
return
|
||||
if effect.name in self.resistances:
|
||||
effect.duration //= 2 # Half duration
|
||||
self.effects.add_effect(effect)
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
"""McRogueFace - Status Effects (basic_3)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_status_effects
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_status_effects_basic_3.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def serialize_effects(effect_manager):
|
||||
return [{"name": e.name, "duration": e.duration}
|
||||
for e in effect_manager.effects]
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
"""McRogueFace - Turn-Based Game Loop (combat_turn_system)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/combat_turn_system
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/combat/combat_turn_system.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def create_turn_order_ui(turn_manager, x=800, y=50):
|
||||
"""Create a visual turn order display."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Background frame
|
||||
frame = mcrfpy.Frame(x, y, 200, 300)
|
||||
frame.fill_color = mcrfpy.Color(30, 30, 30, 200)
|
||||
frame.outline = 2
|
||||
frame.outline_color = mcrfpy.Color(100, 100, 100)
|
||||
ui.append(frame)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("Turn Order", x + 10, y + 10)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
return frame
|
||||
|
||||
def update_turn_order_display(frame, turn_manager, x=800, y=50):
|
||||
"""Update the turn order display."""
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
# Clear old entries (keep frame and title)
|
||||
# In practice, store references to caption objects and update them
|
||||
|
||||
for i, actor_data in enumerate(turn_manager.actors):
|
||||
actor = actor_data["actor"]
|
||||
is_current = (i == turn_manager.current)
|
||||
|
||||
# Actor name/type
|
||||
name = getattr(actor, 'name', f"Actor {i}")
|
||||
color = mcrfpy.Color(255, 255, 0) if is_current else mcrfpy.Color(200, 200, 200)
|
||||
|
||||
caption = mcrfpy.Caption(name, x + 10, y + 40 + i * 25)
|
||||
caption.fill_color = color
|
||||
ui.append(caption)
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
"""McRogueFace - Color Pulse Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class PulsingCell:
|
||||
"""A cell that continuously pulses until stopped."""
|
||||
|
||||
def __init__(self, grid, x, y, color, period=1.0, max_alpha=180):
|
||||
"""
|
||||
Args:
|
||||
grid: Grid with color layer
|
||||
x, y: Cell position
|
||||
color: RGB tuple
|
||||
period: Time for one complete pulse cycle
|
||||
max_alpha: Maximum alpha value (0-255)
|
||||
"""
|
||||
self.grid = grid
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.color = color
|
||||
self.period = period
|
||||
self.max_alpha = max_alpha
|
||||
self.is_pulsing = False
|
||||
self.pulse_id = 0
|
||||
self.cell = None
|
||||
|
||||
self._setup_layer()
|
||||
|
||||
def _setup_layer(self):
|
||||
"""Ensure color layer exists and get cell reference."""
|
||||
color_layer = None
|
||||
for layer in self.grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
self.grid.add_layer("color")
|
||||
color_layer = self.grid.layers[-1]
|
||||
|
||||
self.cell = color_layer.at(self.x, self.y)
|
||||
if self.cell:
|
||||
self.cell.color = mcrfpy.Color(self.color[0], self.color[1],
|
||||
self.color[2], 0)
|
||||
|
||||
def start(self):
|
||||
"""Start continuous pulsing."""
|
||||
if self.is_pulsing or not self.cell:
|
||||
return
|
||||
|
||||
self.is_pulsing = True
|
||||
self.pulse_id += 1
|
||||
self._pulse_up()
|
||||
|
||||
def _pulse_up(self):
|
||||
"""Animate alpha increasing."""
|
||||
if not self.is_pulsing:
|
||||
return
|
||||
|
||||
current_id = self.pulse_id
|
||||
half_period = self.period / 2
|
||||
|
||||
anim = mcrfpy.Animation("a", float(self.max_alpha), half_period, "easeInOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def next_phase(timer_name):
|
||||
if self.is_pulsing and self.pulse_id == current_id:
|
||||
self._pulse_down()
|
||||
|
||||
mcrfpy.Timer(f"pulse_up_{id(self)}_{current_id}",
|
||||
next_phase, int(half_period * 1000), once=True)
|
||||
|
||||
def _pulse_down(self):
|
||||
"""Animate alpha decreasing."""
|
||||
if not self.is_pulsing:
|
||||
return
|
||||
|
||||
current_id = self.pulse_id
|
||||
half_period = self.period / 2
|
||||
|
||||
anim = mcrfpy.Animation("a", 0.0, half_period, "easeInOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def next_phase(timer_name):
|
||||
if self.is_pulsing and self.pulse_id == current_id:
|
||||
self._pulse_up()
|
||||
|
||||
mcrfpy.Timer(f"pulse_down_{id(self)}_{current_id}",
|
||||
next_phase, int(half_period * 1000), once=True)
|
||||
|
||||
def stop(self):
|
||||
"""Stop pulsing and fade out."""
|
||||
self.is_pulsing = False
|
||||
if self.cell:
|
||||
anim = mcrfpy.Animation("a", 0.0, 0.2, "easeOut")
|
||||
anim.start(self.cell.color)
|
||||
|
||||
def set_color(self, color):
|
||||
"""Change pulse color."""
|
||||
self.color = color
|
||||
if self.cell:
|
||||
current_alpha = self.cell.color.a
|
||||
self.cell.color = mcrfpy.Color(color[0], color[1], color[2], current_alpha)
|
||||
|
||||
|
||||
# Usage
|
||||
objective_pulse = PulsingCell(grid, 10, 10, (0, 255, 100), period=1.5)
|
||||
objective_pulse.start()
|
||||
|
||||
# Later, when objective is reached:
|
||||
objective_pulse.stop()
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
"""McRogueFace - Color Pulse Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_color_pulse
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_color_pulse_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def ripple_effect(grid, center_x, center_y, color, max_radius=5, duration=1.0):
|
||||
"""
|
||||
Create an expanding ripple effect.
|
||||
|
||||
Args:
|
||||
grid: Grid with color layer
|
||||
center_x, center_y: Ripple origin
|
||||
color: RGB tuple
|
||||
max_radius: Maximum ripple size
|
||||
duration: Total animation time
|
||||
"""
|
||||
# Get color layer
|
||||
color_layer = None
|
||||
for layer in grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
grid.add_layer("color")
|
||||
color_layer = grid.layers[-1]
|
||||
|
||||
step_duration = duration / max_radius
|
||||
|
||||
for radius in range(max_radius + 1):
|
||||
# Get cells at this radius (ring, not filled)
|
||||
ring_cells = []
|
||||
for dy in range(-radius, radius + 1):
|
||||
for dx in range(-radius, radius + 1):
|
||||
dist_sq = dx * dx + dy * dy
|
||||
# Include cells approximately on the ring edge
|
||||
if radius * radius - radius <= dist_sq <= radius * radius + radius:
|
||||
cell = color_layer.at(center_x + dx, center_y + dy)
|
||||
if cell:
|
||||
ring_cells.append(cell)
|
||||
|
||||
# Schedule this ring to animate
|
||||
def animate_ring(timer_name, cells=ring_cells, c=color):
|
||||
for cell in cells:
|
||||
cell.color = mcrfpy.Color(c[0], c[1], c[2], 200)
|
||||
# Fade out
|
||||
anim = mcrfpy.Animation("a", 0.0, step_duration * 2, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
delay = int(radius * step_duration * 1000)
|
||||
mcrfpy.Timer(f"ripple_{radius}", animate_ring, delay, once=True)
|
||||
|
||||
|
||||
# Usage
|
||||
ripple_effect(grid, 10, 10, (100, 200, 255), max_radius=6, duration=0.8)
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Add a color layer to your grid (do this once during setup)
|
||||
grid.add_layer("color")
|
||||
color_layer = grid.layers[-1] # Get the color layer
|
||||
|
||||
def flash_cell(grid, x, y, color, duration=0.3):
|
||||
"""Flash a grid cell with a color overlay."""
|
||||
# Get the color layer (assumes it's the last layer added)
|
||||
color_layer = None
|
||||
for layer in grid.layers:
|
||||
if isinstance(layer, mcrfpy.ColorLayer):
|
||||
color_layer = layer
|
||||
break
|
||||
|
||||
if not color_layer:
|
||||
return
|
||||
|
||||
# Set cell to flash color
|
||||
cell = color_layer.at(x, y)
|
||||
cell.color = mcrfpy.Color(color[0], color[1], color[2], 200)
|
||||
|
||||
# Animate alpha back to 0
|
||||
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
def damage_at_position(grid, x, y, duration=0.3):
|
||||
"""Flash red at a grid position when damage occurs."""
|
||||
flash_cell(grid, x, y, (255, 0, 0), duration)
|
||||
|
||||
# Usage when entity takes damage
|
||||
damage_at_position(grid, int(enemy.x), int(enemy.y))
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class DamageEffects:
|
||||
"""Manages visual damage feedback effects."""
|
||||
|
||||
# Color presets
|
||||
DAMAGE_RED = (255, 50, 50)
|
||||
HEAL_GREEN = (50, 255, 50)
|
||||
POISON_PURPLE = (150, 50, 200)
|
||||
FIRE_ORANGE = (255, 150, 50)
|
||||
ICE_BLUE = (100, 200, 255)
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.color_layer = None
|
||||
self._setup_color_layer()
|
||||
|
||||
def _setup_color_layer(self):
|
||||
"""Ensure grid has a color layer for effects."""
|
||||
self.grid.add_layer("color")
|
||||
self.color_layer = self.grid.layers[-1]
|
||||
|
||||
def flash_entity(self, entity, color, duration=0.3):
|
||||
"""Flash an entity with a color tint."""
|
||||
# Flash at entity's grid position
|
||||
x, y = int(entity.x), int(entity.y)
|
||||
self.flash_cell(x, y, color, duration)
|
||||
|
||||
def flash_cell(self, x, y, color, duration=0.3):
|
||||
"""Flash a specific grid cell."""
|
||||
if not self.color_layer:
|
||||
return
|
||||
|
||||
cell = self.color_layer.at(x, y)
|
||||
if cell:
|
||||
cell.color = mcrfpy.Color(color[0], color[1], color[2], 180)
|
||||
|
||||
# Fade out
|
||||
anim = mcrfpy.Animation("a", 0.0, duration, "easeOut")
|
||||
anim.start(cell.color)
|
||||
|
||||
def damage(self, entity, amount, duration=0.3):
|
||||
"""Standard damage flash."""
|
||||
self.flash_entity(entity, self.DAMAGE_RED, duration)
|
||||
|
||||
def heal(self, entity, amount, duration=0.4):
|
||||
"""Healing effect - green flash."""
|
||||
self.flash_entity(entity, self.HEAL_GREEN, duration)
|
||||
|
||||
def poison(self, entity, duration=0.5):
|
||||
"""Poison damage - purple flash."""
|
||||
self.flash_entity(entity, self.POISON_PURPLE, duration)
|
||||
|
||||
def fire(self, entity, duration=0.3):
|
||||
"""Fire damage - orange flash."""
|
||||
self.flash_entity(entity, self.FIRE_ORANGE, duration)
|
||||
|
||||
def ice(self, entity, duration=0.4):
|
||||
"""Ice damage - blue flash."""
|
||||
self.flash_entity(entity, self.ICE_BLUE, duration)
|
||||
|
||||
def area_damage(self, center_x, center_y, radius, color, duration=0.4):
|
||||
"""Flash all cells in a radius."""
|
||||
for dy in range(-radius, radius + 1):
|
||||
for dx in range(-radius, radius + 1):
|
||||
if dx * dx + dy * dy <= radius * radius:
|
||||
self.flash_cell(center_x + dx, center_y + dy, color, duration)
|
||||
|
||||
# Setup
|
||||
effects = DamageEffects(grid)
|
||||
|
||||
# Usage examples
|
||||
effects.damage(player, 10) # Red flash
|
||||
effects.heal(player, 5) # Green flash
|
||||
effects.poison(enemy) # Purple flash
|
||||
effects.area_damage(5, 5, 3, effects.FIRE_ORANGE) # Area effect
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
"""McRogueFace - Damage Flash Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_damage_flash
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_damage_flash_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def multi_flash(grid, x, y, color, flashes=3, flash_duration=0.1):
|
||||
"""Flash a cell multiple times for emphasis."""
|
||||
delay = 0
|
||||
|
||||
for i in range(flashes):
|
||||
# Schedule each flash with increasing delay
|
||||
def do_flash(timer_name, fx=x, fy=y, fc=color, fd=flash_duration):
|
||||
flash_cell(grid, fx, fy, fc, fd)
|
||||
|
||||
mcrfpy.Timer(f"flash_{x}_{y}_{i}", do_flash, int(delay * 1000), once=True)
|
||||
delay += flash_duration * 1.5 # Gap between flashes
|
||||
|
||||
# Usage for critical hit
|
||||
multi_flash(grid, int(enemy.x), int(enemy.y), (255, 255, 0), flashes=3)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
"""McRogueFace - Floating Damage Numbers (effects_floating_text)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_floating_text
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_floating_text.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class StackedFloatingText:
|
||||
"""Prevents overlapping text by stacking vertically."""
|
||||
|
||||
def __init__(self, scene_name, grid=None):
|
||||
self.manager = FloatingTextManager(scene_name, grid)
|
||||
self.position_stack = {} # Track recent spawns per position
|
||||
|
||||
def spawn_stacked(self, x, y, text, color, **kwargs):
|
||||
"""Spawn with automatic vertical stacking."""
|
||||
key = (int(x), int(y))
|
||||
|
||||
# Calculate offset based on recent spawns at this position
|
||||
offset = self.position_stack.get(key, 0)
|
||||
actual_y = y - (offset * 20) # 20 pixels between stacked texts
|
||||
|
||||
self.manager.spawn(x, actual_y, text, color, **kwargs)
|
||||
|
||||
# Increment stack counter
|
||||
self.position_stack[key] = offset + 1
|
||||
|
||||
# Reset stack after delay
|
||||
def reset_stack(timer_name, k=key):
|
||||
if k in self.position_stack:
|
||||
self.position_stack[k] = max(0, self.position_stack[k] - 1)
|
||||
|
||||
mcrfpy.Timer(f"stack_reset_{x}_{y}_{offset}", reset_stack, 300, once=True)
|
||||
|
||||
# Usage
|
||||
stacked = StackedFloatingText("game", grid)
|
||||
# Rapid hits will stack vertically instead of overlapping
|
||||
stacked.spawn_stacked(5, 5, "-10", (255, 0, 0), is_grid_pos=True)
|
||||
stacked.spawn_stacked(5, 5, "-8", (255, 0, 0), is_grid_pos=True)
|
||||
stacked.spawn_stacked(5, 5, "-12", (255, 0, 0), is_grid_pos=True)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
"""McRogueFace - Path Animation (Multi-Step Movement) (effects_path_animation)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_path_animation
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_path_animation.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class CameraFollowingPath:
|
||||
"""Path animator that also moves the camera."""
|
||||
|
||||
def __init__(self, entity, grid, path, step_duration=0.2):
|
||||
self.entity = entity
|
||||
self.grid = grid
|
||||
self.path = path
|
||||
self.step_duration = step_duration
|
||||
self.index = 0
|
||||
self.on_complete = None
|
||||
|
||||
def start(self):
|
||||
self.index = 0
|
||||
self._next()
|
||||
|
||||
def _next(self):
|
||||
if self.index >= len(self.path):
|
||||
if self.on_complete:
|
||||
self.on_complete(self)
|
||||
return
|
||||
|
||||
x, y = self.path[self.index]
|
||||
|
||||
def done(anim, target):
|
||||
self.index += 1
|
||||
self._next()
|
||||
|
||||
# Animate entity
|
||||
if self.entity.x != x:
|
||||
anim = mcrfpy.Animation("x", float(x), self.step_duration,
|
||||
"easeInOut", callback=done)
|
||||
anim.start(self.entity)
|
||||
elif self.entity.y != y:
|
||||
anim = mcrfpy.Animation("y", float(y), self.step_duration,
|
||||
"easeInOut", callback=done)
|
||||
anim.start(self.entity)
|
||||
else:
|
||||
done(None, None)
|
||||
return
|
||||
|
||||
# Animate camera to follow
|
||||
cam_x = mcrfpy.Animation("center_x", (x + 0.5) * 16,
|
||||
self.step_duration, "easeInOut")
|
||||
cam_y = mcrfpy.Animation("center_y", (y + 0.5) * 16,
|
||||
self.step_duration, "easeInOut")
|
||||
cam_x.start(self.grid)
|
||||
cam_y.start(self.grid)
|
||||
|
||||
|
||||
# Usage
|
||||
path = [(5, 5), (5, 10), (10, 10)]
|
||||
mover = CameraFollowingPath(player, grid, path)
|
||||
mover.on_complete = lambda m: print("Journey complete!")
|
||||
mover.start()
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
"""McRogueFace - Scene Transition Effects (effects_scene_transitions)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_scene_transitions
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_scene_transitions.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class TransitionManager:
|
||||
"""Manages scene transitions with multiple effect types."""
|
||||
|
||||
def __init__(self, screen_width=1024, screen_height=768):
|
||||
self.width = screen_width
|
||||
self.height = screen_height
|
||||
self.is_transitioning = False
|
||||
|
||||
def go_to(self, scene_name, effect="fade", duration=0.5, **kwargs):
|
||||
"""
|
||||
Transition to a scene with the specified effect.
|
||||
|
||||
Args:
|
||||
scene_name: Target scene
|
||||
effect: "fade", "flash", "wipe", "instant"
|
||||
duration: Transition duration
|
||||
**kwargs: Effect-specific options (color, direction)
|
||||
"""
|
||||
if self.is_transitioning:
|
||||
return
|
||||
|
||||
self.is_transitioning = True
|
||||
|
||||
if effect == "instant":
|
||||
mcrfpy.setScene(scene_name)
|
||||
self.is_transitioning = False
|
||||
|
||||
elif effect == "fade":
|
||||
color = kwargs.get("color", (0, 0, 0))
|
||||
self._fade(scene_name, duration, color)
|
||||
|
||||
elif effect == "flash":
|
||||
color = kwargs.get("color", (255, 255, 255))
|
||||
self._flash(scene_name, duration, color)
|
||||
|
||||
elif effect == "wipe":
|
||||
direction = kwargs.get("direction", "right")
|
||||
color = kwargs.get("color", (0, 0, 0))
|
||||
self._wipe(scene_name, duration, direction, color)
|
||||
|
||||
def _fade(self, scene, duration, color):
|
||||
half = duration / 2
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("opacity", 1.0, half, "easeIn")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("opacity", 0.0, half, "easeOut")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("fade_done", cleanup, int(half * 1000) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("fade_switch", phase2, int(half * 1000), once=True)
|
||||
|
||||
def _flash(self, scene, duration, color):
|
||||
quarter = duration / 4
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 0)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("opacity", 1.0, quarter, "easeOut")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("opacity", 0.0, duration / 2, "easeIn")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("flash_done", cleanup, int(duration * 500) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("flash_switch", phase2, int(quarter * 2000), once=True)
|
||||
|
||||
def _wipe(self, scene, duration, direction, color):
|
||||
# Simplified wipe - right direction only for brevity
|
||||
half = duration / 2
|
||||
ui = mcrfpy.sceneUI(mcrfpy.currentScene())
|
||||
|
||||
overlay = mcrfpy.Frame(0, 0, 0, self.height)
|
||||
overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
overlay.z_index = 9999
|
||||
ui.append(overlay)
|
||||
|
||||
anim = mcrfpy.Animation("w", float(self.width), half, "easeInOut")
|
||||
anim.start(overlay)
|
||||
|
||||
def phase2(timer_name):
|
||||
mcrfpy.setScene(scene)
|
||||
new_ui = mcrfpy.sceneUI(scene)
|
||||
|
||||
new_overlay = mcrfpy.Frame(0, 0, self.width, self.height)
|
||||
new_overlay.fill_color = mcrfpy.Color(color[0], color[1], color[2], 255)
|
||||
new_overlay.z_index = 9999
|
||||
new_ui.append(new_overlay)
|
||||
|
||||
anim2 = mcrfpy.Animation("x", float(self.width), half, "easeInOut")
|
||||
anim2.start(new_overlay)
|
||||
|
||||
def cleanup(timer_name):
|
||||
for i, elem in enumerate(new_ui):
|
||||
if elem is new_overlay:
|
||||
new_ui.remove(i)
|
||||
break
|
||||
self.is_transitioning = False
|
||||
|
||||
mcrfpy.Timer("wipe_done", cleanup, int(half * 1000) + 50, once=True)
|
||||
|
||||
mcrfpy.Timer("wipe_switch", phase2, int(half * 1000), once=True)
|
||||
|
||||
|
||||
# Usage
|
||||
transitions = TransitionManager()
|
||||
|
||||
# Various transition styles
|
||||
transitions.go_to("game", effect="fade", duration=0.5)
|
||||
transitions.go_to("menu", effect="flash", color=(255, 255, 255), duration=0.4)
|
||||
transitions.go_to("next_level", effect="wipe", direction="right", duration=0.6)
|
||||
transitions.go_to("options", effect="instant")
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"""McRogueFace - Screen Shake Effect (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
def screen_shake(frame, intensity=5, duration=0.2):
|
||||
"""
|
||||
Shake a frame/container by animating its position.
|
||||
|
||||
Args:
|
||||
frame: The UI Frame to shake (often a container for all game elements)
|
||||
intensity: Maximum pixel offset
|
||||
duration: Total shake duration in seconds
|
||||
"""
|
||||
original_x = frame.x
|
||||
original_y = frame.y
|
||||
|
||||
# Quick shake to offset position
|
||||
shake_x = mcrfpy.Animation("x", float(original_x + intensity), duration / 4, "easeOut")
|
||||
shake_x.start(frame)
|
||||
|
||||
# Schedule return to center
|
||||
def return_to_center(timer_name):
|
||||
anim = mcrfpy.Animation("x", float(original_x), duration / 2, "easeInOut")
|
||||
anim.start(frame)
|
||||
|
||||
mcrfpy.Timer("shake_return", return_to_center, int(duration * 250), once=True)
|
||||
|
||||
# Usage - wrap your game content in a Frame
|
||||
game_container = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
# ... add game elements to game_container.children ...
|
||||
screen_shake(game_container, intensity=8, duration=0.3)
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
"""McRogueFace - Screen Shake Effect (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/effects_screen_shake
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/effects/effects_screen_shake_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import math
|
||||
|
||||
def directional_shake(shaker, direction_x, direction_y, intensity=10, duration=0.2):
|
||||
"""
|
||||
Shake in a specific direction (e.g., direction of impact).
|
||||
|
||||
Args:
|
||||
shaker: ScreenShakeManager instance
|
||||
direction_x, direction_y: Direction vector (will be normalized)
|
||||
intensity: Shake strength
|
||||
duration: Shake duration
|
||||
"""
|
||||
# Normalize direction
|
||||
length = math.sqrt(direction_x * direction_x + direction_y * direction_y)
|
||||
if length == 0:
|
||||
return
|
||||
|
||||
dir_x = direction_x / length
|
||||
dir_y = direction_y / length
|
||||
|
||||
# Shake in the direction, then opposite, then back
|
||||
shaker._animate_position(
|
||||
shaker.original_x + dir_x * intensity,
|
||||
shaker.original_y + dir_y * intensity,
|
||||
duration / 3
|
||||
)
|
||||
|
||||
def reverse(timer_name):
|
||||
shaker._animate_position(
|
||||
shaker.original_x - dir_x * intensity * 0.5,
|
||||
shaker.original_y - dir_y * intensity * 0.5,
|
||||
duration / 3
|
||||
)
|
||||
|
||||
def reset(timer_name):
|
||||
shaker._animate_position(
|
||||
shaker.original_x,
|
||||
shaker.original_y,
|
||||
duration / 3
|
||||
)
|
||||
shaker.is_shaking = False
|
||||
|
||||
mcrfpy.Timer("dir_shake_rev", reverse, int(duration * 333), once=True)
|
||||
mcrfpy.Timer("dir_shake_reset", reset, int(duration * 666), once=True)
|
||||
|
||||
# Usage: shake away from impact direction
|
||||
hit_from_x, hit_from_y = -1, 0 # Hit from the left
|
||||
directional_shake(shaker, hit_from_x, hit_from_y, intensity=12)
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (animated)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_animated.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class TargetingSystem:
|
||||
"""Handle ability targeting with visual feedback."""
|
||||
|
||||
def __init__(self, grid, player):
|
||||
self.grid = grid
|
||||
self.player = player
|
||||
self.highlights = HighlightManager(grid)
|
||||
self.current_ability = None
|
||||
self.valid_targets = set()
|
||||
|
||||
def start_targeting(self, ability):
|
||||
"""Begin targeting for an ability."""
|
||||
self.current_ability = ability
|
||||
px, py = self.player.pos
|
||||
|
||||
# Get valid targets based on ability
|
||||
if ability.target_type == 'self':
|
||||
self.valid_targets = {(px, py)}
|
||||
elif ability.target_type == 'adjacent':
|
||||
self.valid_targets = get_adjacent(px, py)
|
||||
elif ability.target_type == 'ranged':
|
||||
self.valid_targets = get_radius_range(px, py, ability.range)
|
||||
elif ability.target_type == 'line':
|
||||
self.valid_targets = get_line_range(px, py, ability.range)
|
||||
|
||||
# Filter to visible tiles only
|
||||
self.valid_targets = {
|
||||
(x, y) for x, y in self.valid_targets
|
||||
if grid.is_in_fov(x, y)
|
||||
}
|
||||
|
||||
# Show valid targets
|
||||
self.highlights.add('attack', self.valid_targets)
|
||||
|
||||
def update_hover(self, x, y):
|
||||
"""Update when cursor moves."""
|
||||
if not self.current_ability:
|
||||
return
|
||||
|
||||
# Clear previous AoE preview
|
||||
self.highlights.remove('danger')
|
||||
|
||||
if (x, y) in self.valid_targets:
|
||||
# Valid target - highlight it
|
||||
self.highlights.add('select', [(x, y)])
|
||||
|
||||
# Show AoE if applicable
|
||||
if self.current_ability.aoe_radius > 0:
|
||||
aoe = get_radius_range(x, y, self.current_ability.aoe_radius, True)
|
||||
self.highlights.add('danger', aoe)
|
||||
else:
|
||||
self.highlights.remove('select')
|
||||
|
||||
def confirm_target(self, x, y):
|
||||
"""Confirm target selection."""
|
||||
if (x, y) in self.valid_targets:
|
||||
self.cancel_targeting()
|
||||
return (x, y)
|
||||
return None
|
||||
|
||||
def cancel_targeting(self):
|
||||
"""Cancel targeting mode."""
|
||||
self.current_ability = None
|
||||
self.valid_targets = set()
|
||||
self.highlights.clear()
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def get_line_range(start_x, start_y, max_range):
|
||||
"""Get cells in cardinal directions (ranged attack)."""
|
||||
cells = set()
|
||||
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
for dist in range(1, max_range + 1):
|
||||
x = start_x + dx * dist
|
||||
y = start_y + dy * dist
|
||||
|
||||
# Stop if wall blocks line of sight
|
||||
if not grid.at(x, y).transparent:
|
||||
break
|
||||
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
||||
def get_radius_range(center_x, center_y, radius, include_center=False):
|
||||
"""Get cells within a radius (spell area)."""
|
||||
cells = set()
|
||||
|
||||
for x in range(center_x - radius, center_x + radius + 1):
|
||||
for y in range(center_y - radius, center_y + radius + 1):
|
||||
# Euclidean distance
|
||||
dist = ((x - center_x) ** 2 + (y - center_y) ** 2) ** 0.5
|
||||
if dist <= radius:
|
||||
if include_center or (x, y) != (center_x, center_y):
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
||||
def get_cone_range(origin_x, origin_y, direction, length, spread):
|
||||
"""Get cells in a cone (breath attack)."""
|
||||
import math
|
||||
cells = set()
|
||||
|
||||
# Direction angles (in radians)
|
||||
angles = {
|
||||
'n': -math.pi / 2,
|
||||
's': math.pi / 2,
|
||||
'e': 0,
|
||||
'w': math.pi,
|
||||
'ne': -math.pi / 4,
|
||||
'nw': -3 * math.pi / 4,
|
||||
'se': math.pi / 4,
|
||||
'sw': 3 * math.pi / 4
|
||||
}
|
||||
|
||||
base_angle = angles.get(direction, 0)
|
||||
half_spread = math.radians(spread / 2)
|
||||
|
||||
for x in range(origin_x - length, origin_x + length + 1):
|
||||
for y in range(origin_y - length, origin_y + length + 1):
|
||||
dx = x - origin_x
|
||||
dy = y - origin_y
|
||||
dist = (dx * dx + dy * dy) ** 0.5
|
||||
|
||||
if dist > 0 and dist <= length:
|
||||
angle = math.atan2(dy, dx)
|
||||
angle_diff = abs((angle - base_angle + math.pi) % (2 * math.pi) - math.pi)
|
||||
|
||||
if angle_diff <= half_spread:
|
||||
cells.add((x, y))
|
||||
|
||||
return cells
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
"""McRogueFace - Cell Highlighting (Targeting) (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_cell_highlighting
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_cell_highlighting_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def show_path_preview(start, end):
|
||||
"""Highlight the path between two points."""
|
||||
path = find_path(start, end) # Your pathfinding function
|
||||
|
||||
if path:
|
||||
highlights.add('path', path)
|
||||
|
||||
# Highlight destination specially
|
||||
highlights.add('select', [end])
|
||||
|
||||
def hide_path_preview():
|
||||
"""Clear path display."""
|
||||
highlights.remove('path')
|
||||
highlights.remove('select')
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"""McRogueFace - Dijkstra Distance Maps (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def ai_flee(entity, threat_x, threat_y):
|
||||
"""Move entity away from threat using Dijkstra map."""
|
||||
grid.compute_dijkstra(threat_x, threat_y)
|
||||
|
||||
ex, ey = entity.pos
|
||||
current_dist = grid.get_dijkstra_distance(ex, ey)
|
||||
|
||||
# Find neighbor with highest distance
|
||||
best_move = None
|
||||
best_dist = current_dist
|
||||
|
||||
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
|
||||
nx, ny = ex + dx, ey + dy
|
||||
|
||||
if grid.at(nx, ny).walkable:
|
||||
dist = grid.get_dijkstra_distance(nx, ny)
|
||||
if dist > best_dist:
|
||||
best_dist = dist
|
||||
best_move = (nx, ny)
|
||||
|
||||
if best_move:
|
||||
entity.pos = best_move
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
"""McRogueFace - Dijkstra Distance Maps (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dijkstra
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dijkstra_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Cache Dijkstra maps when possible
|
||||
class CachedDijkstra:
|
||||
"""Cache Dijkstra computations."""
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.cache = {}
|
||||
self.cache_valid = False
|
||||
|
||||
def invalidate(self):
|
||||
"""Call when map changes."""
|
||||
self.cache = {}
|
||||
self.cache_valid = False
|
||||
|
||||
def get_distance(self, from_x, from_y, to_x, to_y):
|
||||
"""Get cached distance or compute."""
|
||||
key = (to_x, to_y) # Cache by destination
|
||||
|
||||
if key not in self.cache:
|
||||
self.grid.compute_dijkstra(to_x, to_y)
|
||||
# Store all distances from this computation
|
||||
self.cache[key] = self._snapshot_distances()
|
||||
|
||||
return self.cache[key].get((from_x, from_y), float('inf'))
|
||||
|
||||
def _snapshot_distances(self):
|
||||
"""Capture current distance values."""
|
||||
grid_w, grid_h = self.grid.grid_size
|
||||
distances = {}
|
||||
for x in range(grid_w):
|
||||
for y in range(grid_h):
|
||||
dist = self.grid.get_dijkstra_distance(x, y)
|
||||
if dist != float('inf'):
|
||||
distances[(x, y)] = dist
|
||||
return distances
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
"""McRogueFace - Room and Corridor Generator (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class BSPNode:
|
||||
"""Node in a BSP tree for dungeon generation."""
|
||||
|
||||
MIN_SIZE = 6
|
||||
|
||||
def __init__(self, x, y, w, h):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.left = None
|
||||
self.right = None
|
||||
self.room = None
|
||||
|
||||
def split(self):
|
||||
"""Recursively split this node."""
|
||||
if self.left or self.right:
|
||||
return False
|
||||
|
||||
# Choose split direction
|
||||
if self.w > self.h and self.w / self.h >= 1.25:
|
||||
horizontal = False
|
||||
elif self.h > self.w and self.h / self.w >= 1.25:
|
||||
horizontal = True
|
||||
else:
|
||||
horizontal = random.random() < 0.5
|
||||
|
||||
max_size = (self.h if horizontal else self.w) - self.MIN_SIZE
|
||||
if max_size <= self.MIN_SIZE:
|
||||
return False
|
||||
|
||||
split = random.randint(self.MIN_SIZE, max_size)
|
||||
|
||||
if horizontal:
|
||||
self.left = BSPNode(self.x, self.y, self.w, split)
|
||||
self.right = BSPNode(self.x, self.y + split, self.w, self.h - split)
|
||||
else:
|
||||
self.left = BSPNode(self.x, self.y, split, self.h)
|
||||
self.right = BSPNode(self.x + split, self.y, self.w - split, self.h)
|
||||
|
||||
return True
|
||||
|
||||
def create_rooms(self, grid):
|
||||
"""Create rooms in leaf nodes and connect siblings."""
|
||||
if self.left or self.right:
|
||||
if self.left:
|
||||
self.left.create_rooms(grid)
|
||||
if self.right:
|
||||
self.right.create_rooms(grid)
|
||||
|
||||
# Connect children
|
||||
if self.left and self.right:
|
||||
left_room = self.left.get_room()
|
||||
right_room = self.right.get_room()
|
||||
if left_room and right_room:
|
||||
connect_points(grid, left_room.center, right_room.center)
|
||||
else:
|
||||
# Leaf node - create room
|
||||
w = random.randint(3, self.w - 2)
|
||||
h = random.randint(3, self.h - 2)
|
||||
x = self.x + random.randint(1, self.w - w - 1)
|
||||
y = self.y + random.randint(1, self.h - h - 1)
|
||||
self.room = Room(x, y, w, h)
|
||||
carve_room(grid, self.room)
|
||||
|
||||
def get_room(self):
|
||||
"""Get a room from this node or its children."""
|
||||
if self.room:
|
||||
return self.room
|
||||
|
||||
left_room = self.left.get_room() if self.left else None
|
||||
right_room = self.right.get_room() if self.right else None
|
||||
|
||||
if left_room and right_room:
|
||||
return random.choice([left_room, right_room])
|
||||
return left_room or right_room
|
||||
|
||||
|
||||
def generate_bsp_dungeon(grid, iterations=4):
|
||||
"""Generate a BSP-based dungeon."""
|
||||
grid_w, grid_h = grid.grid_size
|
||||
|
||||
# Fill with walls
|
||||
for x in range(grid_w):
|
||||
for y in range(grid_h):
|
||||
point = grid.at(x, y)
|
||||
point.tilesprite = TILE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
# Build BSP tree
|
||||
root = BSPNode(0, 0, grid_w, grid_h)
|
||||
nodes = [root]
|
||||
|
||||
for _ in range(iterations):
|
||||
new_nodes = []
|
||||
for node in nodes:
|
||||
if node.split():
|
||||
new_nodes.extend([node.left, node.right])
|
||||
nodes = new_nodes or nodes
|
||||
|
||||
# Create rooms and corridors
|
||||
root.create_rooms(grid)
|
||||
|
||||
# Collect all rooms
|
||||
rooms = []
|
||||
def collect_rooms(node):
|
||||
if node.room:
|
||||
rooms.append(node.room)
|
||||
if node.left:
|
||||
collect_rooms(node.left)
|
||||
if node.right:
|
||||
collect_rooms(node.right)
|
||||
|
||||
collect_rooms(root)
|
||||
return rooms
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""McRogueFace - Room and Corridor Generator (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_dungeon_generator
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_dungeon_generator_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
import random
|
||||
|
||||
# Tile indices (adjust for your tileset)
|
||||
TILE_FLOOR = 0
|
||||
TILE_WALL = 1
|
||||
TILE_DOOR = 2
|
||||
TILE_STAIRS_DOWN = 3
|
||||
TILE_STAIRS_UP = 4
|
||||
|
||||
class DungeonGenerator:
|
||||
"""Procedural dungeon generator with rooms and corridors."""
|
||||
|
||||
def __init__(self, grid, seed=None):
|
||||
self.grid = grid
|
||||
self.grid_w, self.grid_h = grid.grid_size
|
||||
self.rooms = []
|
||||
|
||||
if seed is not None:
|
||||
random.seed(seed)
|
||||
|
||||
def generate(self, room_count=8, min_room=4, max_room=10):
|
||||
"""Generate a complete dungeon level."""
|
||||
self.rooms = []
|
||||
|
||||
# Fill with walls
|
||||
self._fill_walls()
|
||||
|
||||
# Place rooms
|
||||
attempts = 0
|
||||
max_attempts = room_count * 10
|
||||
|
||||
while len(self.rooms) < room_count and attempts < max_attempts:
|
||||
attempts += 1
|
||||
|
||||
# Random room size
|
||||
w = random.randint(min_room, max_room)
|
||||
h = random.randint(min_room, max_room)
|
||||
|
||||
# Random position (leaving border)
|
||||
x = random.randint(1, self.grid_w - w - 2)
|
||||
y = random.randint(1, self.grid_h - h - 2)
|
||||
|
||||
room = Room(x, y, w, h)
|
||||
|
||||
# Check overlap
|
||||
if not any(room.intersects(r) for r in self.rooms):
|
||||
self._carve_room(room)
|
||||
|
||||
# Connect to previous room
|
||||
if self.rooms:
|
||||
self._dig_corridor(self.rooms[-1].center, room.center)
|
||||
|
||||
self.rooms.append(room)
|
||||
|
||||
# Place stairs
|
||||
if len(self.rooms) >= 2:
|
||||
self._place_stairs()
|
||||
|
||||
return self.rooms
|
||||
|
||||
def _fill_walls(self):
|
||||
"""Fill the entire grid with wall tiles."""
|
||||
for x in range(self.grid_w):
|
||||
for y in range(self.grid_h):
|
||||
point = self.grid.at(x, y)
|
||||
point.tilesprite = TILE_WALL
|
||||
point.walkable = False
|
||||
point.transparent = False
|
||||
|
||||
def _carve_room(self, room):
|
||||
"""Carve out a room, making it walkable."""
|
||||
for x in range(room.x, room.x + room.width):
|
||||
for y in range(room.y, room.y + room.height):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _set_floor(self, x, y):
|
||||
"""Set a single tile as floor."""
|
||||
if 0 <= x < self.grid_w and 0 <= y < self.grid_h:
|
||||
point = self.grid.at(x, y)
|
||||
point.tilesprite = TILE_FLOOR
|
||||
point.walkable = True
|
||||
point.transparent = True
|
||||
|
||||
def _dig_corridor(self, start, end):
|
||||
"""Dig an L-shaped corridor between two points."""
|
||||
x1, y1 = start
|
||||
x2, y2 = end
|
||||
|
||||
# Randomly choose horizontal-first or vertical-first
|
||||
if random.random() < 0.5:
|
||||
# Horizontal then vertical
|
||||
self._dig_horizontal(x1, x2, y1)
|
||||
self._dig_vertical(y1, y2, x2)
|
||||
else:
|
||||
# Vertical then horizontal
|
||||
self._dig_vertical(y1, y2, x1)
|
||||
self._dig_horizontal(x1, x2, y2)
|
||||
|
||||
def _dig_horizontal(self, x1, x2, y):
|
||||
"""Dig a horizontal tunnel."""
|
||||
for x in range(min(x1, x2), max(x1, x2) + 1):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _dig_vertical(self, y1, y2, x):
|
||||
"""Dig a vertical tunnel."""
|
||||
for y in range(min(y1, y2), max(y1, y2) + 1):
|
||||
self._set_floor(x, y)
|
||||
|
||||
def _place_stairs(self):
|
||||
"""Place stairs in first and last rooms."""
|
||||
# Stairs up in first room
|
||||
start_room = self.rooms[0]
|
||||
sx, sy = start_room.center
|
||||
point = self.grid.at(sx, sy)
|
||||
point.tilesprite = TILE_STAIRS_UP
|
||||
|
||||
# Stairs down in last room
|
||||
end_room = self.rooms[-1]
|
||||
ex, ey = end_room.center
|
||||
point = self.grid.at(ex, ey)
|
||||
point.tilesprite = TILE_STAIRS_DOWN
|
||||
|
||||
return (sx, sy), (ex, ey)
|
||||
|
||||
def get_spawn_point(self):
|
||||
"""Get a good spawn point for the player."""
|
||||
if self.rooms:
|
||||
return self.rooms[0].center
|
||||
return (self.grid_w // 2, self.grid_h // 2)
|
||||
|
||||
def get_random_floor(self):
|
||||
"""Get a random walkable floor tile."""
|
||||
floors = []
|
||||
for x in range(self.grid_w):
|
||||
for y in range(self.grid_h):
|
||||
if self.grid.at(x, y).walkable:
|
||||
floors.append((x, y))
|
||||
return random.choice(floors) if floors else None
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
"""McRogueFace - Basic Fog of War (grid_fog_of_war)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_fog_of_war
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_fog_of_war.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
# Shadowcasting (default) - fast and produces nice results
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.SHADOW)
|
||||
|
||||
# Recursive shadowcasting - slightly different corner behavior
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.RECURSIVE_SHADOW)
|
||||
|
||||
# Diamond - simple but produces diamond-shaped FOV
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.DIAMOND)
|
||||
|
||||
# Permissive - sees more tiles, good for tactical games
|
||||
grid.compute_fov(x, y, 10, mcrfpy.FOV.PERMISSIVE)
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
"""McRogueFace - Multi-Layer Tiles (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class EffectLayer:
|
||||
"""Manage visual effects with color overlays."""
|
||||
|
||||
def __init__(self, grid, z_index=2):
|
||||
self.grid = grid
|
||||
self.layer = grid.add_layer("color", z_index=z_index)
|
||||
self.effects = {} # (x, y) -> effect_data
|
||||
|
||||
def add_effect(self, x, y, effect_type, duration=None, **kwargs):
|
||||
"""Add a visual effect."""
|
||||
self.effects[(x, y)] = {
|
||||
'type': effect_type,
|
||||
'duration': duration,
|
||||
'time': 0,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
def remove_effect(self, x, y):
|
||||
"""Remove an effect."""
|
||||
if (x, y) in self.effects:
|
||||
del self.effects[(x, y)]
|
||||
self.layer.set(x, y, mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
def update(self, dt):
|
||||
"""Update all effects."""
|
||||
import math
|
||||
|
||||
to_remove = []
|
||||
|
||||
for (x, y), effect in self.effects.items():
|
||||
effect['time'] += dt
|
||||
|
||||
# Check expiration
|
||||
if effect['duration'] and effect['time'] >= effect['duration']:
|
||||
to_remove.append((x, y))
|
||||
continue
|
||||
|
||||
# Calculate color based on effect type
|
||||
color = self._calculate_color(effect)
|
||||
self.layer.set(x, y, color)
|
||||
|
||||
for pos in to_remove:
|
||||
self.remove_effect(*pos)
|
||||
|
||||
def _calculate_color(self, effect):
|
||||
"""Get color for an effect at current time."""
|
||||
import math
|
||||
|
||||
t = effect['time']
|
||||
effect_type = effect['type']
|
||||
|
||||
if effect_type == 'fire':
|
||||
# Flickering orange/red
|
||||
flicker = 0.7 + 0.3 * math.sin(t * 10)
|
||||
return mcrfpy.Color(
|
||||
255,
|
||||
int(100 + 50 * math.sin(t * 8)),
|
||||
0,
|
||||
int(180 * flicker)
|
||||
)
|
||||
|
||||
elif effect_type == 'poison':
|
||||
# Pulsing green
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 3)
|
||||
return mcrfpy.Color(0, 200, 0, int(100 * pulse))
|
||||
|
||||
elif effect_type == 'ice':
|
||||
# Static blue with shimmer
|
||||
shimmer = 0.8 + 0.2 * math.sin(t * 5)
|
||||
return mcrfpy.Color(100, 150, 255, int(120 * shimmer))
|
||||
|
||||
elif effect_type == 'blood':
|
||||
# Fading red
|
||||
duration = effect.get('duration', 5)
|
||||
fade = 1 - (t / duration) if duration else 1
|
||||
return mcrfpy.Color(150, 0, 0, int(150 * fade))
|
||||
|
||||
elif effect_type == 'highlight':
|
||||
# Pulsing highlight
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 4)
|
||||
base = effect.get('color', mcrfpy.Color(255, 255, 0, 100))
|
||||
return mcrfpy.Color(base.r, base.g, base.b, int(base.a * pulse))
|
||||
|
||||
return mcrfpy.Color(128, 128, 128, 50)
|
||||
|
||||
|
||||
# Usage
|
||||
effects = EffectLayer(grid)
|
||||
|
||||
# Add fire effect (permanent)
|
||||
effects.add_effect(5, 5, 'fire')
|
||||
|
||||
# Add blood stain (fades over 10 seconds)
|
||||
effects.add_effect(10, 10, 'blood', duration=10)
|
||||
|
||||
# Add poison cloud
|
||||
for x in range(8, 12):
|
||||
for y in range(8, 12):
|
||||
effects.add_effect(x, y, 'poison', duration=5)
|
||||
|
||||
# Update in game loop
|
||||
def game_update(runtime):
|
||||
effects.update(0.016) # 60 FPS
|
||||
|
||||
mcrfpy.setTimer("effects", game_update, 16)
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
"""McRogueFace - Multi-Layer Tiles (complete)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/grid_multi_layer
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/grid/grid_multi_layer_complete.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
class OptimizedLayers:
|
||||
"""Performance-optimized layer management."""
|
||||
|
||||
def __init__(self, grid):
|
||||
self.grid = grid
|
||||
self.dirty_effects = set() # Only update changed cells
|
||||
self.batch_updates = []
|
||||
|
||||
def mark_dirty(self, x, y):
|
||||
"""Mark a cell as needing update."""
|
||||
self.dirty_effects.add((x, y))
|
||||
|
||||
def batch_set(self, layer, cells_and_values):
|
||||
"""Queue batch updates."""
|
||||
self.batch_updates.append((layer, cells_and_values))
|
||||
|
||||
def flush(self):
|
||||
"""Apply all queued updates."""
|
||||
for layer, updates in self.batch_updates:
|
||||
for x, y, value in updates:
|
||||
layer.set(x, y, value)
|
||||
self.batch_updates = []
|
||||
|
||||
def update_dirty_only(self, effect_layer, effect_calculator):
|
||||
"""Only update cells marked dirty."""
|
||||
for x, y in self.dirty_effects:
|
||||
color = effect_calculator(x, y)
|
||||
effect_layer.set(x, y, color)
|
||||
self.dirty_effects.clear()
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (animated)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_animated.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class AnimatedHealthBar:
|
||||
"""Health bar with smooth fill animation."""
|
||||
|
||||
def __init__(self, x, y, w, h, current, maximum):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.current = current
|
||||
self.display_current = current # What's visually shown
|
||||
self.maximum = maximum
|
||||
self.timer_name = f"hp_anim_{id(self)}"
|
||||
|
||||
# Background
|
||||
self.background = mcrfpy.Frame(x, y, w, h)
|
||||
self.background.fill_color = mcrfpy.Color(40, 40, 40)
|
||||
self.background.outline = 2
|
||||
self.background.outline_color = mcrfpy.Color(60, 60, 60)
|
||||
|
||||
# Damage preview (shows recent damage in different color)
|
||||
self.damage_fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
|
||||
self.damage_fill.fill_color = mcrfpy.Color(180, 50, 50)
|
||||
self.damage_fill.outline = 0
|
||||
|
||||
# Main fill
|
||||
self.fill = mcrfpy.Frame(x + 2, y + 2, w - 4, h - 4)
|
||||
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
|
||||
self.fill.outline = 0
|
||||
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self):
|
||||
"""Update the visual fill based on display_current."""
|
||||
ratio = max(0, min(1, self.display_current / self.maximum))
|
||||
self.fill.w = (self.w - 4) * ratio
|
||||
|
||||
# Color based on ratio
|
||||
if ratio > 0.6:
|
||||
self.fill.fill_color = mcrfpy.Color(50, 200, 50)
|
||||
elif ratio > 0.3:
|
||||
self.fill.fill_color = mcrfpy.Color(230, 180, 30)
|
||||
else:
|
||||
self.fill.fill_color = mcrfpy.Color(200, 50, 50)
|
||||
|
||||
def set_health(self, new_current, animate=True):
|
||||
"""
|
||||
Set health with optional animation.
|
||||
|
||||
Args:
|
||||
new_current: New health value
|
||||
animate: Whether to animate the transition
|
||||
"""
|
||||
old_current = self.current
|
||||
self.current = max(0, min(self.maximum, new_current))
|
||||
|
||||
if not animate:
|
||||
self.display_current = self.current
|
||||
self._update_display()
|
||||
return
|
||||
|
||||
# Show damage preview immediately
|
||||
if self.current < old_current:
|
||||
damage_ratio = self.current / self.maximum
|
||||
self.damage_fill.w = (self.w - 4) * (old_current / self.maximum)
|
||||
|
||||
# Animate the fill
|
||||
self._start_animation()
|
||||
|
||||
def _start_animation(self):
|
||||
"""Start animating toward target health."""
|
||||
mcrfpy.delTimer(self.timer_name)
|
||||
|
||||
def animate_step(dt):
|
||||
# Lerp toward target
|
||||
diff = self.current - self.display_current
|
||||
if abs(diff) < 0.5:
|
||||
self.display_current = self.current
|
||||
mcrfpy.delTimer(self.timer_name)
|
||||
# Also update damage preview
|
||||
self.damage_fill.w = self.fill.w
|
||||
else:
|
||||
# Move 10% of the way each frame
|
||||
self.display_current += diff * 0.1
|
||||
|
||||
self._update_display()
|
||||
|
||||
mcrfpy.setTimer(self.timer_name, animate_step, 16)
|
||||
|
||||
def damage(self, amount):
|
||||
"""Apply damage with animation."""
|
||||
self.set_health(self.current - amount, animate=True)
|
||||
|
||||
def heal(self, amount):
|
||||
"""Apply healing with animation."""
|
||||
self.set_health(self.current + amount, animate=True)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all frames to scene."""
|
||||
ui.append(self.background)
|
||||
ui.append(self.damage_fill)
|
||||
ui.append(self.fill)
|
||||
|
||||
|
||||
# Usage
|
||||
hp_bar = AnimatedHealthBar(50, 50, 300, 30, current=100, maximum=100)
|
||||
hp_bar.add_to_scene(ui)
|
||||
|
||||
# Damage will animate smoothly
|
||||
hp_bar.damage(40)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Player health bar at top
|
||||
player_hp = EnhancedHealthBar(10, 10, 300, 30, 100, 100)
|
||||
player_hp.add_to_scene(ui)
|
||||
|
||||
# Enemy health bar
|
||||
enemy_hp = EnhancedHealthBar(400, 10, 200, 20, 50, 50)
|
||||
enemy_hp.add_to_scene(ui)
|
||||
|
||||
# Simulate combat
|
||||
def combat_tick(dt):
|
||||
import random
|
||||
if random.random() < 0.3:
|
||||
player_hp.damage(random.randint(5, 15))
|
||||
if random.random() < 0.4:
|
||||
enemy_hp.damage(random.randint(3, 8))
|
||||
|
||||
mcrfpy.setTimer("combat", combat_tick, 1000)
|
||||
|
||||
# Keyboard controls for testing
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
if key == "H":
|
||||
player_hp.heal(20)
|
||||
elif key == "D":
|
||||
player_hp.damage(10)
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class EnhancedHealthBar:
|
||||
"""Health bar with text display, color transitions, and animations."""
|
||||
|
||||
def __init__(self, x, y, w, h, current, maximum, show_text=True):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.current = current
|
||||
self.maximum = maximum
|
||||
self.show_text = show_text
|
||||
|
||||
# Color thresholds (ratio -> color)
|
||||
self.colors = {
|
||||
0.6: mcrfpy.Color(50, 205, 50), # Green when > 60%
|
||||
0.3: mcrfpy.Color(255, 165, 0), # Orange when > 30%
|
||||
0.0: mcrfpy.Color(220, 20, 20), # Red when <= 30%
|
||||
}
|
||||
|
||||
# Background frame with dark fill
|
||||
self.background = mcrfpy.Frame(x, y, w, h)
|
||||
self.background.fill_color = mcrfpy.Color(30, 30, 30)
|
||||
self.background.outline = 2
|
||||
self.background.outline_color = mcrfpy.Color(100, 100, 100)
|
||||
|
||||
# Fill frame (nested inside background conceptually)
|
||||
padding = 2
|
||||
self.fill = mcrfpy.Frame(
|
||||
x + padding,
|
||||
y + padding,
|
||||
w - padding * 2,
|
||||
h - padding * 2
|
||||
)
|
||||
self.fill.outline = 0
|
||||
|
||||
# Text label
|
||||
self.label = None
|
||||
if show_text:
|
||||
self.label = mcrfpy.Caption(
|
||||
"",
|
||||
mcrfpy.default_font,
|
||||
x + w / 2 - 20,
|
||||
y + h / 2 - 8
|
||||
)
|
||||
self.label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
self.label.outline = 1
|
||||
self.label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
|
||||
self._update()
|
||||
|
||||
def _get_color_for_ratio(self, ratio):
|
||||
"""Get the appropriate color based on health ratio."""
|
||||
for threshold, color in sorted(self.colors.items(), reverse=True):
|
||||
if ratio > threshold:
|
||||
return color
|
||||
# Return the lowest threshold color if ratio is 0 or below
|
||||
return self.colors[0.0]
|
||||
|
||||
def _update(self):
|
||||
"""Update fill width, color, and text."""
|
||||
ratio = max(0, min(1, self.current / self.maximum))
|
||||
|
||||
# Update fill width (accounting for padding)
|
||||
padding = 2
|
||||
self.fill.w = (self.w - padding * 2) * ratio
|
||||
|
||||
# Update color based on ratio
|
||||
self.fill.fill_color = self._get_color_for_ratio(ratio)
|
||||
|
||||
# Update text
|
||||
if self.label:
|
||||
self.label.text = f"{int(self.current)}/{int(self.maximum)}"
|
||||
# Center the text
|
||||
text_width = len(self.label.text) * 8 # Approximate
|
||||
self.label.x = self.x + (self.w - text_width) / 2
|
||||
|
||||
def set_health(self, current, maximum=None):
|
||||
"""Update health values."""
|
||||
self.current = max(0, current)
|
||||
if maximum is not None:
|
||||
self.maximum = maximum
|
||||
self._update()
|
||||
|
||||
def damage(self, amount):
|
||||
"""Apply damage (convenience method)."""
|
||||
self.set_health(self.current - amount)
|
||||
|
||||
def heal(self, amount):
|
||||
"""Apply healing (convenience method)."""
|
||||
self.set_health(min(self.maximum, self.current + amount))
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
"""Add all components to scene UI."""
|
||||
ui.append(self.background)
|
||||
ui.append(self.fill)
|
||||
if self.label:
|
||||
ui.append(self.label)
|
||||
|
||||
|
||||
# Usage
|
||||
mcrfpy.createScene("demo")
|
||||
mcrfpy.setScene("demo")
|
||||
ui = mcrfpy.sceneUI("demo")
|
||||
|
||||
# Create enhanced health bar
|
||||
hp = EnhancedHealthBar(50, 50, 250, 25, current=100, maximum=100)
|
||||
hp.add_to_scene(ui)
|
||||
|
||||
# Simulate damage
|
||||
hp.damage(30) # Now 70/100, shows green
|
||||
hp.damage(25) # Now 45/100, shows orange
|
||||
hp.damage(20) # Now 25/100, shows red
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
"""McRogueFace - Health Bar Widget (multi)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_health_bar
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_health_bar_multi.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class ResourceBar:
|
||||
"""Generic resource bar that can represent any stat."""
|
||||
|
||||
def __init__(self, x, y, w, h, current, maximum,
|
||||
fill_color, bg_color=None, label=""):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.w = w
|
||||
self.h = h
|
||||
self.current = current
|
||||
self.maximum = maximum
|
||||
self.label_text = label
|
||||
|
||||
if bg_color is None:
|
||||
bg_color = mcrfpy.Color(30, 30, 30)
|
||||
|
||||
# Background
|
||||
self.background = mcrfpy.Frame(x, y, w, h)
|
||||
self.background.fill_color = bg_color
|
||||
self.background.outline = 1
|
||||
self.background.outline_color = mcrfpy.Color(60, 60, 60)
|
||||
|
||||
# Fill
|
||||
self.fill = mcrfpy.Frame(x + 1, y + 1, w - 2, h - 2)
|
||||
self.fill.fill_color = fill_color
|
||||
self.fill.outline = 0
|
||||
|
||||
# Label (left side)
|
||||
self.label = mcrfpy.Caption(label, mcrfpy.default_font, x - 30, y + 2)
|
||||
self.label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
ratio = max(0, min(1, self.current / self.maximum))
|
||||
self.fill.w = (self.w - 2) * ratio
|
||||
|
||||
def set_value(self, current, maximum=None):
|
||||
self.current = max(0, current)
|
||||
if maximum:
|
||||
self.maximum = maximum
|
||||
self._update()
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
if self.label_text:
|
||||
ui.append(self.label)
|
||||
ui.append(self.background)
|
||||
ui.append(self.fill)
|
||||
|
||||
|
||||
class PlayerStats:
|
||||
"""Collection of resource bars for a player."""
|
||||
|
||||
def __init__(self, x, y):
|
||||
bar_width = 200
|
||||
bar_height = 18
|
||||
spacing = 25
|
||||
|
||||
self.hp = ResourceBar(
|
||||
x, y, bar_width, bar_height,
|
||||
current=100, maximum=100,
|
||||
fill_color=mcrfpy.Color(220, 50, 50),
|
||||
label="HP"
|
||||
)
|
||||
|
||||
self.mp = ResourceBar(
|
||||
x, y + spacing, bar_width, bar_height,
|
||||
current=50, maximum=50,
|
||||
fill_color=mcrfpy.Color(50, 100, 220),
|
||||
label="MP"
|
||||
)
|
||||
|
||||
self.stamina = ResourceBar(
|
||||
x, y + spacing * 2, bar_width, bar_height,
|
||||
current=80, maximum=80,
|
||||
fill_color=mcrfpy.Color(50, 180, 50),
|
||||
label="SP"
|
||||
)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
self.hp.add_to_scene(ui)
|
||||
self.mp.add_to_scene(ui)
|
||||
self.stamina.add_to_scene(ui)
|
||||
|
||||
|
||||
# Usage
|
||||
mcrfpy.createScene("stats_demo")
|
||||
mcrfpy.setScene("stats_demo")
|
||||
ui = mcrfpy.sceneUI("stats_demo")
|
||||
|
||||
stats = PlayerStats(80, 20)
|
||||
stats.add_to_scene(ui)
|
||||
|
||||
# Update individual stats
|
||||
stats.hp.set_value(75)
|
||||
stats.mp.set_value(30)
|
||||
stats.stamina.set_value(60)
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
"""McRogueFace - Selection Menu Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Setup
|
||||
mcrfpy.createScene("main_menu")
|
||||
mcrfpy.setScene("main_menu")
|
||||
ui = mcrfpy.sceneUI("main_menu")
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
bg.fill_color = mcrfpy.Color(20, 20, 35)
|
||||
ui.append(bg)
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption("DUNGEON QUEST", mcrfpy.default_font, 350, 100)
|
||||
title.fill_color = mcrfpy.Color(255, 200, 50)
|
||||
ui.append(title)
|
||||
|
||||
# Menu
|
||||
def start_game():
|
||||
print("Starting game...")
|
||||
|
||||
def show_options():
|
||||
print("Options...")
|
||||
|
||||
menu = Menu(
|
||||
362, 250,
|
||||
["New Game", "Continue", "Options", "Quit"],
|
||||
lambda i, opt: {
|
||||
0: start_game,
|
||||
1: lambda: print("Continue..."),
|
||||
2: show_options,
|
||||
3: mcrfpy.exit
|
||||
}.get(i, lambda: None)(),
|
||||
title="Main Menu"
|
||||
)
|
||||
menu.add_to_scene(ui)
|
||||
|
||||
# Input
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
menu.handle_key(key)
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
"""McRogueFace - Selection Menu Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_menu
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_menu_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
class MenuBar:
|
||||
"""Horizontal menu bar with dropdown submenus."""
|
||||
|
||||
def __init__(self, y=0, items=None):
|
||||
"""
|
||||
Create a menu bar.
|
||||
|
||||
Args:
|
||||
y: Y position (usually 0 for top)
|
||||
items: List of dicts with 'label' and 'options' keys
|
||||
"""
|
||||
self.y = y
|
||||
self.items = items or []
|
||||
self.selected_item = 0
|
||||
self.dropdown_open = False
|
||||
self.dropdown_selected = 0
|
||||
|
||||
self.item_width = 100
|
||||
self.height = 30
|
||||
|
||||
# Main bar frame
|
||||
self.bar = mcrfpy.Frame(0, y, 1024, self.height)
|
||||
self.bar.fill_color = mcrfpy.Color(50, 50, 70)
|
||||
self.bar.outline = 0
|
||||
|
||||
# Item captions
|
||||
self.item_captions = []
|
||||
for i, item in enumerate(items):
|
||||
cap = mcrfpy.Caption(
|
||||
item['label'],
|
||||
mcrfpy.default_font,
|
||||
10 + i * self.item_width,
|
||||
y + 7
|
||||
)
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.item_captions.append(cap)
|
||||
|
||||
# Dropdown panel (hidden initially)
|
||||
self.dropdown = None
|
||||
self.dropdown_captions = []
|
||||
|
||||
def _update_highlight(self):
|
||||
"""Update visual selection on bar."""
|
||||
for i, cap in enumerate(self.item_captions):
|
||||
if i == self.selected_item and self.dropdown_open:
|
||||
cap.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
else:
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
def _show_dropdown(self, ui):
|
||||
"""Show dropdown for selected item."""
|
||||
# Remove existing dropdown
|
||||
self._hide_dropdown(ui)
|
||||
|
||||
item = self.items[self.selected_item]
|
||||
options = item.get('options', [])
|
||||
|
||||
if not options:
|
||||
return
|
||||
|
||||
x = 5 + self.selected_item * self.item_width
|
||||
y = self.y + self.height
|
||||
width = 150
|
||||
height = len(options) * 25 + 10
|
||||
|
||||
self.dropdown = mcrfpy.Frame(x, y, width, height)
|
||||
self.dropdown.fill_color = mcrfpy.Color(40, 40, 60, 250)
|
||||
self.dropdown.outline = 1
|
||||
self.dropdown.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
ui.append(self.dropdown)
|
||||
|
||||
self.dropdown_captions = []
|
||||
for i, opt in enumerate(options):
|
||||
cap = mcrfpy.Caption(
|
||||
opt['label'],
|
||||
mcrfpy.default_font,
|
||||
x + 10,
|
||||
y + 5 + i * 25
|
||||
)
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
self.dropdown_captions.append(cap)
|
||||
ui.append(cap)
|
||||
|
||||
self.dropdown_selected = 0
|
||||
self._update_dropdown_highlight()
|
||||
|
||||
def _hide_dropdown(self, ui):
|
||||
"""Hide dropdown menu."""
|
||||
if self.dropdown:
|
||||
try:
|
||||
ui.remove(self.dropdown)
|
||||
except:
|
||||
pass
|
||||
self.dropdown = None
|
||||
|
||||
for cap in self.dropdown_captions:
|
||||
try:
|
||||
ui.remove(cap)
|
||||
except:
|
||||
pass
|
||||
self.dropdown_captions = []
|
||||
|
||||
def _update_dropdown_highlight(self):
|
||||
"""Update dropdown selection highlight."""
|
||||
for i, cap in enumerate(self.dropdown_captions):
|
||||
if i == self.dropdown_selected:
|
||||
cap.fill_color = mcrfpy.Color(255, 255, 100)
|
||||
else:
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
|
||||
def add_to_scene(self, ui):
|
||||
ui.append(self.bar)
|
||||
for cap in self.item_captions:
|
||||
ui.append(cap)
|
||||
|
||||
def handle_key(self, key, ui):
|
||||
"""Handle keyboard navigation."""
|
||||
if not self.dropdown_open:
|
||||
if key == "Left":
|
||||
self.selected_item = (self.selected_item - 1) % len(self.items)
|
||||
self._update_highlight()
|
||||
elif key == "Right":
|
||||
self.selected_item = (self.selected_item + 1) % len(self.items)
|
||||
self._update_highlight()
|
||||
elif key == "Return" or key == "Down":
|
||||
self.dropdown_open = True
|
||||
self._show_dropdown(ui)
|
||||
self._update_highlight()
|
||||
else:
|
||||
if key == "Up":
|
||||
options = self.items[self.selected_item].get('options', [])
|
||||
self.dropdown_selected = (self.dropdown_selected - 1) % len(options)
|
||||
self._update_dropdown_highlight()
|
||||
elif key == "Down":
|
||||
options = self.items[self.selected_item].get('options', [])
|
||||
self.dropdown_selected = (self.dropdown_selected + 1) % len(options)
|
||||
self._update_dropdown_highlight()
|
||||
elif key == "Return":
|
||||
opt = self.items[self.selected_item]['options'][self.dropdown_selected]
|
||||
if opt.get('action'):
|
||||
opt['action']()
|
||||
self.dropdown_open = False
|
||||
self._hide_dropdown(ui)
|
||||
self._update_highlight()
|
||||
elif key == "Escape":
|
||||
self.dropdown_open = False
|
||||
self._hide_dropdown(ui)
|
||||
self._update_highlight()
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
"""McRogueFace - Message Log Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Initialize
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Create log at bottom of screen
|
||||
log = EnhancedMessageLog(10, 500, 700, 250, line_height=20)
|
||||
ui.append(log.frame)
|
||||
|
||||
# Simulate game events
|
||||
def simulate_combat(dt):
|
||||
import random
|
||||
events = [
|
||||
("You swing your sword!", "combat"),
|
||||
("The orc dodges!", "combat"),
|
||||
("Critical hit!", "combat"),
|
||||
("You found a potion!", "loot"),
|
||||
]
|
||||
event = random.choice(events)
|
||||
log.add(event[0], event[1])
|
||||
|
||||
# Add messages every 2 seconds for demo
|
||||
mcrfpy.setTimer("combat_sim", simulate_combat, 2000)
|
||||
|
||||
# Keyboard controls
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
if key == "PageUp":
|
||||
log.scroll_up(3)
|
||||
elif key == "PageDown":
|
||||
log.scroll_down(3)
|
||||
elif key == "C":
|
||||
log.set_filter('combat')
|
||||
elif key == "L":
|
||||
log.set_filter('loot')
|
||||
elif key == "A":
|
||||
log.set_filter(None) # All
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||
|
||||
log.system("Press PageUp/PageDown to scroll")
|
||||
log.system("Press C for combat, L for loot, A for all")
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
"""McRogueFace - Message Log Widget (enhanced)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_message_log
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_message_log_enhanced.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
def handle_keys(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if key == "PageUp":
|
||||
log.scroll_up(5)
|
||||
elif key == "PageDown":
|
||||
log.scroll_down(5)
|
||||
|
||||
mcrfpy.keypressScene(handle_keys)
|
||||
|
||||
# Or with mouse scroll on the frame
|
||||
def on_log_scroll(x, y, button, action):
|
||||
# Note: You may need to implement scroll detection
|
||||
# based on your input system
|
||||
pass
|
||||
|
||||
log.frame.click = on_log_scroll
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
"""McRogueFace - Modal Dialog Widget (basic)
|
||||
|
||||
Documentation: https://mcrogueface.github.io/cookbook/ui_modal_dialog
|
||||
Repository: https://github.com/jmccardle/McRogueFace/blob/master/docs/cookbook/ui/ui_modal_dialog_basic.py
|
||||
|
||||
This code is extracted from the McRogueFace documentation and can be
|
||||
run directly with: ./mcrogueface path/to/this/file.py
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# Scene setup
|
||||
mcrfpy.createScene("game")
|
||||
mcrfpy.setScene("game")
|
||||
ui = mcrfpy.sceneUI("game")
|
||||
|
||||
# Game background
|
||||
bg = mcrfpy.Frame(0, 0, 1024, 768)
|
||||
bg.fill_color = mcrfpy.Color(25, 35, 45)
|
||||
ui.append(bg)
|
||||
|
||||
title = mcrfpy.Caption("My Game", mcrfpy.default_font, 450, 50)
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(title)
|
||||
|
||||
# Quit button
|
||||
quit_btn = mcrfpy.Frame(430, 400, 160, 50)
|
||||
quit_btn.fill_color = mcrfpy.Color(150, 50, 50)
|
||||
quit_btn.outline = 2
|
||||
quit_btn.outline_color = mcrfpy.Color(200, 100, 100)
|
||||
ui.append(quit_btn)
|
||||
|
||||
quit_label = mcrfpy.Caption("Quit Game", mcrfpy.default_font, 460, 415)
|
||||
quit_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
ui.append(quit_label)
|
||||
|
||||
# Confirmation dialog
|
||||
confirm_dialog = None
|
||||
|
||||
def show_quit_confirm():
|
||||
global confirm_dialog
|
||||
|
||||
def on_response(index, label):
|
||||
if label == "Yes":
|
||||
mcrfpy.exit()
|
||||
|
||||
confirm_dialog = EnhancedDialog(
|
||||
"Quit Game?",
|
||||
"Are you sure you want to quit?\nUnsaved progress will be lost.",
|
||||
["Yes", "No"],
|
||||
DialogStyle.WARNING,
|
||||
on_response
|
||||
)
|
||||
confirm_dialog.add_to_scene(ui)
|
||||
confirm_dialog.show()
|
||||
|
||||
quit_btn.click = lambda x, y, b, a: show_quit_confirm() if a == "start" else None
|
||||
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
if confirm_dialog and confirm_dialog.handle_key(key):
|
||||
return
|
||||
|
||||
if key == "Escape":
|
||||
show_quit_confirm()
|
||||
|
||||
mcrfpy.keypressScene(on_key)
|
||||