Compare commits
5 commits
master
...
alpha_stre
| Author | SHA1 | Date | |
|---|---|---|---|
| 99f301e3a0 | |||
| 2f2b488fb5 | |||
| 5a003a9aa5 | |||
| e5affaf317 | |||
| d03182d347 |
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)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential cmake git \
|
||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
||||
libgl-dev libopenal-dev
|
||||
|
||||
- name: Check for pre-built libraries
|
||||
run: |
|
||||
if [ ! -d "__lib" ]; then
|
||||
echo "::error::__lib/ directory not found. Pre-built libraries must be available on the runner."
|
||||
echo "See BUILD_FROM_SOURCE.md for instructions on building dependencies."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build (Release)
|
||||
run: make linux
|
||||
|
||||
- name: Run tests (Release)
|
||||
run: cd tests && python3 run_tests.py -v
|
||||
|
||||
debug-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential cmake git \
|
||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
||||
libgl-dev libopenal-dev
|
||||
|
||||
- name: Check for debug libraries
|
||||
run: |
|
||||
if [ ! -d "__lib_debug" ]; then
|
||||
echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and test (debug Python)
|
||||
run: make debug-test
|
||||
|
||||
asan-test:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential cmake git \
|
||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
||||
libgl-dev libopenal-dev
|
||||
|
||||
- name: Check for debug libraries
|
||||
run: |
|
||||
if [ ! -d "__lib_debug" ]; then
|
||||
echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and test (ASan + UBSan)
|
||||
run: make asan-test
|
||||
|
||||
valgrind-test:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
build-essential cmake git valgrind \
|
||||
zlib1g-dev libx11-dev libxrandr-dev libxcursor-dev \
|
||||
libfreetype-dev libudev-dev libvorbis-dev libflac-dev \
|
||||
libgl-dev libopenal-dev
|
||||
|
||||
- name: Check for debug libraries
|
||||
run: |
|
||||
if [ ! -d "__lib_debug" ]; then
|
||||
echo "::error::__lib_debug/ directory not found. Build debug Python first: tools/build_debug_python.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build and test (Valgrind memcheck)
|
||||
run: make valgrind-test
|
||||
timeout-minutes: 30
|
||||
70
.gitignore
vendored
|
|
@ -7,83 +7,23 @@ PCbuild
|
|||
.vs
|
||||
obj
|
||||
build
|
||||
/lib
|
||||
__pycache__
|
||||
|
||||
# unimportant files that won't pass clean dir check
|
||||
build*
|
||||
docs
|
||||
.claude
|
||||
my_games
|
||||
|
||||
# images are produced by many tests
|
||||
*.png
|
||||
|
||||
# generated snippet preview screenshots (#381); consumed/committed by the doc-site repo
|
||||
snippet-shots/
|
||||
|
||||
# WASM stdlib for Emscripten build
|
||||
!wasm_stdlib/
|
||||
lib
|
||||
obj
|
||||
|
||||
.cache/
|
||||
7DRL2025 Release/
|
||||
CMakeFiles/
|
||||
Makefile
|
||||
*.md
|
||||
*.zip
|
||||
__lib/
|
||||
__lib_debug/
|
||||
__lib_windows/
|
||||
build-windows/
|
||||
build_windows/
|
||||
_oldscripts/
|
||||
|
||||
# Audit tooling virtualenv (tools/audit_pymethoddef.py)
|
||||
.venv-audit/
|
||||
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/
|
||||
|
||||
# Fuzzing build artifacts and runtime data (build-fuzz matched by build* above)
|
||||
tests/fuzz/corpora/
|
||||
tests/fuzz/crashes/
|
||||
|
||||
# >>> generated-docs (untrack via tools/untrack_generated_docs.sh) >>>
|
||||
# Rendered/generated documentation is regenerated by the pre-commit hook and
|
||||
# shipped inside release artifacts -- it no longer lives in git. The compact
|
||||
# api/manifest.json is the exception: it STAYS committed (refreshed by the hook).
|
||||
# Personal-notes docs (plans, triage, research) are also untracked here.
|
||||
# tools/untrack_generated_docs.sh reads exactly this block to migrate the
|
||||
# currently-tracked copies out of the index.
|
||||
docs/API_REFERENCE_DYNAMIC.md
|
||||
docs/api_reference_dynamic.html
|
||||
docs/mcrfpy.3
|
||||
stubs/mcrfpy.pyi
|
||||
docs/generated/
|
||||
docs/API_REFERENCE_COMPLETE.md
|
||||
docs/api_reference_complete.html
|
||||
docs/VISION.md
|
||||
docs/GAMES.md
|
||||
docs/plan-*.md
|
||||
docs/api-audit-*.md
|
||||
docs/ISSUE_TRIAGE_*.md
|
||||
docs/GRID_ENTITY_OVERHAUL_ROADMAP.md
|
||||
docs/sprint-*.md
|
||||
docs/EMSCRIPTEN_RESEARCH.md
|
||||
docs/WASM_TROUBLESHOOTING.md
|
||||
docs/MCROGUFACE_LITE_PICOCALC_RESEARCH.md
|
||||
docs/3D_SYSTEM_GUIDE.md
|
||||
docs/PROCEDURAL_GENERATION_SPEC.md
|
||||
# <<< generated-docs <<<
|
||||
test_*
|
||||
|
|
|
|||
13
.gitmodules
vendored
|
|
@ -10,13 +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/RapidXML"]
|
||||
path = modules/RapidXML
|
||||
url = https://github.com/Fe-Bell/RapidXML
|
||||
[submodule "modules/json"]
|
||||
path = modules/json
|
||||
url = git@github.com:nlohmann/json.git
|
||||
[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/`.
|
||||
594
CMakeLists.txt
|
|
@ -8,527 +8,49 @@ project(McRogueFace)
|
|||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED True)
|
||||
|
||||
# Headless build option (no SFML, no graphics - for server/testing/Emscripten prep)
|
||||
option(MCRF_HEADLESS "Build without graphics dependencies (SFML, ImGui)" OFF)
|
||||
|
||||
# SDL2 backend option (SDL2 + OpenGL ES 2 - for Emscripten/WebGL, Android, cross-platform)
|
||||
option(MCRF_SDL2 "Build with SDL2+OpenGL ES 2 backend instead of SFML" OFF)
|
||||
|
||||
# Playground mode - minimal scripts for web playground (REPL-focused)
|
||||
option(MCRF_PLAYGROUND "Build with minimal playground scripts instead of full game" OFF)
|
||||
|
||||
# Demo mode - self-contained demo game for web showcase
|
||||
option(MCRF_DEMO "Build with demo scripts (web showcase)" OFF)
|
||||
|
||||
# Game shell mode - fullscreen canvas, no REPL chrome (for itch.io / standalone web games)
|
||||
option(MCRF_GAME_SHELL "Use minimal game-only HTML shell (no REPL)" OFF)
|
||||
|
||||
# Debug/sanitizer build options
|
||||
option(MCRF_SANITIZE_ADDRESS "Build with AddressSanitizer" OFF)
|
||||
option(MCRF_SANITIZE_UNDEFINED "Build with UBSan" OFF)
|
||||
option(MCRF_SANITIZE_THREAD "Build with ThreadSanitizer" OFF)
|
||||
option(MCRF_DEBUG_PYTHON "Link against debug CPython from __lib_debug/" OFF)
|
||||
option(MCRF_FREE_THREADED_PYTHON "Link against free-threaded CPython (python3.14t)" OFF)
|
||||
option(MCRF_WASM_DEBUG "Build WASM with DWARF debug info and source maps" OFF)
|
||||
option(MCRF_FUZZER "Build with libFuzzer coverage instrumentation for atheris" OFF)
|
||||
option(MCRF_PROFILE "Keep frame pointers for perf/Callgrind profiling (use with RelWithDebInfo)" OFF)
|
||||
|
||||
# Validate mutually exclusive sanitizers
|
||||
if(MCRF_SANITIZE_ADDRESS AND MCRF_SANITIZE_THREAD)
|
||||
message(FATAL_ERROR "ASan and TSan are mutually exclusive. Use one or the other.")
|
||||
endif()
|
||||
|
||||
# Validate debug Python library exists when requested
|
||||
if(MCRF_DEBUG_PYTHON)
|
||||
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0")
|
||||
message(FATAL_ERROR
|
||||
"__lib_debug/libpython3.14.so.1.0 not found.\n"
|
||||
"Build it first: tools/build_debug_python.sh")
|
||||
endif()
|
||||
message(STATUS "Using debug CPython from __lib_debug/")
|
||||
endif()
|
||||
|
||||
# Emscripten builds: use SDL2 if specified, otherwise fall back to headless
|
||||
if(EMSCRIPTEN)
|
||||
if(MCRF_SDL2)
|
||||
message(STATUS "Emscripten detected - using SDL2 backend")
|
||||
set(MCRF_HEADLESS OFF)
|
||||
else()
|
||||
set(MCRF_HEADLESS ON)
|
||||
message(STATUS "Emscripten detected - forcing HEADLESS mode (use -DMCRF_SDL2=ON for graphics)")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(MCRF_SDL2)
|
||||
message(STATUS "Building with SDL2 backend - SDL2+OpenGL ES 2")
|
||||
endif()
|
||||
|
||||
if(MCRF_PLAYGROUND)
|
||||
message(STATUS "Building in PLAYGROUND mode - minimal scripts for web REPL")
|
||||
endif()
|
||||
|
||||
if(MCRF_HEADLESS)
|
||||
message(STATUS "Building in HEADLESS mode - no SFML/ImGui dependencies")
|
||||
endif()
|
||||
|
||||
# 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}/src)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/tiled)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/ldtk)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/src/audio)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/RapidXML)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/modules/json/single_include)
|
||||
#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 vs Emscripten
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten build: use Python headers compiled for wasm32-emscripten
|
||||
# The pyconfig.h from cross-build has correct LONG_BIT and other settings
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
# Force-include wasm pyconfig.h BEFORE anything else to set correct platform defines
|
||||
add_compile_options(-include ${PYTHON_WASM_BUILD}/pyconfig.h)
|
||||
# Override LONG_BIT - Emscripten's limits.h incorrectly defines it as 64 for wasm32
|
||||
add_compile_definitions(LONG_BIT=32)
|
||||
# Include wasm build directory FIRST so its pyconfig.h is found by #include "pyconfig.h"
|
||||
include_directories(BEFORE ${PYTHON_WASM_BUILD})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/cpython/Include)
|
||||
message(STATUS "Using Emscripten Python from: ${PYTHON_WASM_BUILD}")
|
||||
elseif(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 (not needed in headless or SDL2 mode)
|
||||
# SDL2 builds will use ImGui with SDL2 backend later; for now, no ImGui
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
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
|
||||
)
|
||||
endif()
|
||||
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 (only if using SFML)
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||
# Add GLAD for OpenGL function loading (needed for 3D rendering on SFML)
|
||||
list(APPEND SOURCES "${CMAKE_SOURCE_DIR}/src/3d/glad.c")
|
||||
endif()
|
||||
|
||||
# Find OpenGL (required by ImGui-SFML) - not needed in headless mode
|
||||
# SDL2 builds handle OpenGL ES 2 differently (via SDL2 or Emscripten)
|
||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||
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()
|
||||
endif()
|
||||
|
||||
# Create a list of libraries to link against
|
||||
if(EMSCRIPTEN)
|
||||
# Emscripten build: link against WASM-compiled Python and libtcod
|
||||
set(PYTHON_WASM_BUILD "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/build/python")
|
||||
set(PYTHON_WASM_PREFIX "${CMAKE_SOURCE_DIR}/deps/cpython/cross-build/wasm32-emscripten/prefix")
|
||||
set(LIBTCOD_WASM_BUILD "${CMAKE_SOURCE_DIR}/modules/libtcod-headless/build-emscripten")
|
||||
# Collect HACL crypto object files (not included in libpython3.14.a)
|
||||
file(GLOB PYTHON_HACL_OBJECTS "${PYTHON_WASM_BUILD}/Modules/_hacl/*.o")
|
||||
set(LINK_LIBS
|
||||
${PYTHON_WASM_BUILD}/libpython3.14.a
|
||||
${PYTHON_HACL_OBJECTS}
|
||||
${PYTHON_WASM_BUILD}/Modules/expat/libexpat.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libmpdec.a
|
||||
${PYTHON_WASM_PREFIX}/lib/libffi.a
|
||||
${LIBTCOD_WASM_BUILD}/libtcod.a
|
||||
${LIBTCOD_WASM_BUILD}/_deps/lodepng-c-build/liblodepng-c.a
|
||||
${LIBTCOD_WASM_BUILD}/_deps/utf8proc-build/libutf8proc.a)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux) # Use Linux platform stubs for now
|
||||
# For SDL2 builds, add stb headers for image/font loading
|
||||
if(MCRF_SDL2)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/stb)
|
||||
endif()
|
||||
message(STATUS "Linking Emscripten Python: ${PYTHON_WASM_BUILD}/libpython3.14.a")
|
||||
message(STATUS "Linking Emscripten libtcod: ${LIBTCOD_WASM_BUILD}/libtcod.a")
|
||||
elseif(MCRF_SDL2)
|
||||
# SDL2 build (non-Emscripten): link against SDL2 and system libraries
|
||||
# Note: For desktop SDL2 builds in the future
|
||||
find_package(SDL2 REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
set(LINK_LIBS
|
||||
SDL2::SDL2
|
||||
OpenGL::GL
|
||||
tcod
|
||||
python3.14
|
||||
m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/stb) # stb_image.h, stb_truetype.h
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
message(STATUS "Building with SDL2 backend (desktop)")
|
||||
elseif(MCRF_HEADLESS)
|
||||
# Headless build: no SFML, no OpenGL
|
||||
if(WIN32 OR MCRF_CROSS_WINDOWS)
|
||||
set(LINK_LIBS
|
||||
libtcod
|
||||
python314)
|
||||
if(MCRF_CROSS_WINDOWS)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows/libtcod/lib)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_windows)
|
||||
else()
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/windows)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
else()
|
||||
# Unix/Linux headless build
|
||||
if(MCRF_FREE_THREADED_PYTHON)
|
||||
set(PYTHON_LIB python3.14t)
|
||||
elseif(MCRF_DEBUG_PYTHON)
|
||||
set(PYTHON_LIB python3.14d)
|
||||
else()
|
||||
set(PYTHON_LIB python3.14)
|
||||
endif()
|
||||
set(LINK_LIBS
|
||||
tcod
|
||||
${PYTHON_LIB}
|
||||
m dl util pthread)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
||||
endif()
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib)
|
||||
endif()
|
||||
elseif(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
|
||||
if(MCRF_FREE_THREADED_PYTHON)
|
||||
set(PYTHON_LIB python3.14t)
|
||||
elseif(MCRF_DEBUG_PYTHON)
|
||||
set(PYTHON_LIB python3.14d)
|
||||
else()
|
||||
set(PYTHON_LIB python3.14)
|
||||
endif()
|
||||
set(LINK_LIBS
|
||||
sfml-graphics
|
||||
sfml-window
|
||||
sfml-system
|
||||
sfml-audio
|
||||
tcod
|
||||
${PYTHON_LIB}
|
||||
m dl util pthread
|
||||
${OPENGL_LIBRARIES})
|
||||
include_directories(${CMAKE_SOURCE_DIR}/deps/platform/linux)
|
||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/__lib_debug)
|
||||
endif()
|
||||
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)
|
||||
# We ALWAYS need this because libtcod headers expect SDL3, not SDL2
|
||||
# Our SDL2 backend is separate from libtcod's SDL3 renderer
|
||||
target_compile_definitions(mcrogueface PRIVATE NO_SDL)
|
||||
|
||||
# Sanitizer instrumentation — applied to mcrogueface target only (not imported libs)
|
||||
if(MCRF_SANITIZE_ADDRESS)
|
||||
message(STATUS "AddressSanitizer enabled")
|
||||
target_compile_options(mcrogueface PRIVATE
|
||||
-fsanitize=address -fno-omit-frame-pointer -g -O1)
|
||||
target_link_options(mcrogueface PRIVATE
|
||||
-fsanitize=address)
|
||||
endif()
|
||||
|
||||
if(MCRF_SANITIZE_UNDEFINED)
|
||||
message(STATUS "UndefinedBehaviorSanitizer enabled")
|
||||
# -fno-sanitize=function is Clang-only; -fno-sanitize=vptr avoids CPython false positives
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(UBSAN_EXCLUSIONS -fno-sanitize=function,vptr)
|
||||
else()
|
||||
set(UBSAN_EXCLUSIONS -fno-sanitize=vptr)
|
||||
endif()
|
||||
target_compile_options(mcrogueface PRIVATE
|
||||
-fsanitize=undefined ${UBSAN_EXCLUSIONS} -g -O1)
|
||||
target_link_options(mcrogueface PRIVATE
|
||||
-fsanitize=undefined ${UBSAN_EXCLUSIONS})
|
||||
endif()
|
||||
|
||||
if(MCRF_SANITIZE_THREAD)
|
||||
message(STATUS "ThreadSanitizer enabled")
|
||||
target_compile_options(mcrogueface PRIVATE
|
||||
-fsanitize=thread -g -O1)
|
||||
target_link_options(mcrogueface PRIVATE
|
||||
-fsanitize=thread)
|
||||
endif()
|
||||
|
||||
# Profiling instrumentation (#345) — retain frame pointers so perf --call-graph fp
|
||||
# and Callgrind can unwind through optimized code. Optimization/-g come from the
|
||||
# RelWithDebInfo build type; this only adds the frame pointer. Binary is NOT stripped.
|
||||
if(MCRF_PROFILE)
|
||||
message(STATUS "Profiling build: frame pointers retained (perf/Callgrind)")
|
||||
target_compile_options(mcrogueface PRIVATE -fno-omit-frame-pointer)
|
||||
endif()
|
||||
|
||||
if(MCRF_FUZZER)
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
message(FATAL_ERROR "MCRF_FUZZER=ON requires Clang. Invoke with CC=clang-18 CXX=clang++-18.")
|
||||
endif()
|
||||
message(STATUS "Building mcrfpy_fuzz harness (libFuzzer + ASan + UBSan)")
|
||||
|
||||
set(MCRF_FUZZ_SOURCES ${SOURCES})
|
||||
list(REMOVE_ITEM MCRF_FUZZ_SOURCES ${CMAKE_SOURCE_DIR}/src/main.cpp)
|
||||
list(APPEND MCRF_FUZZ_SOURCES ${CMAKE_SOURCE_DIR}/tests/fuzz/fuzz_common.cpp)
|
||||
|
||||
add_executable(mcrfpy_fuzz ${MCRF_FUZZ_SOURCES})
|
||||
target_compile_definitions(mcrfpy_fuzz PRIVATE NO_SDL MCRF_FUZZ_HARNESS)
|
||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
||||
target_compile_definitions(mcrfpy_fuzz PRIVATE Py_DEBUG)
|
||||
endif()
|
||||
if(MCRF_FREE_THREADED_PYTHON)
|
||||
target_compile_definitions(mcrfpy_fuzz PRIVATE Py_GIL_DISABLED)
|
||||
endif()
|
||||
if(MCRF_HEADLESS)
|
||||
target_compile_definitions(mcrfpy_fuzz PRIVATE MCRF_HEADLESS)
|
||||
endif()
|
||||
if(MCRF_SDL2)
|
||||
target_compile_definitions(mcrfpy_fuzz PRIVATE MCRF_SDL2)
|
||||
endif()
|
||||
target_include_directories(mcrfpy_fuzz PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src
|
||||
${CMAKE_SOURCE_DIR}/tests/fuzz)
|
||||
target_compile_options(mcrfpy_fuzz PRIVATE
|
||||
-fsanitize=fuzzer-no-link,address,undefined
|
||||
-fno-sanitize=function,vptr
|
||||
-fno-omit-frame-pointer -g -O1)
|
||||
target_link_options(mcrfpy_fuzz PRIVATE
|
||||
-fsanitize=fuzzer,address,undefined
|
||||
-fno-sanitize=function,vptr)
|
||||
target_link_libraries(mcrfpy_fuzz ${LINK_LIBS})
|
||||
|
||||
# Copy Python runtime + assets next to mcrfpy_fuzz so the embedded
|
||||
# interpreter finds the stdlib and default_font/default_texture load.
|
||||
add_custom_command(TARGET mcrfpy_fuzz POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrfpy_fuzz>/lib
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/assets $<TARGET_FILE_DIR:mcrfpy_fuzz>/assets)
|
||||
if(MCRF_DEBUG_PYTHON)
|
||||
add_custom_command(TARGET mcrfpy_fuzz POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
||||
$<TARGET_FILE_DIR:mcrfpy_fuzz>/lib/libpython3.14.so.1.0
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
||||
$<TARGET_FILE_DIR:mcrfpy_fuzz>/lib/libpython3.14d.so.1.0
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||
libpython3.14d.so.1.0
|
||||
$<TARGET_FILE_DIR:mcrfpy_fuzz>/lib/libpython3.14d.so)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Enable Py_DEBUG when linking against debug CPython (matches pydebug ABI)
|
||||
if(MCRF_DEBUG_PYTHON OR MCRF_FREE_THREADED_PYTHON)
|
||||
target_compile_definitions(mcrogueface PRIVATE Py_DEBUG)
|
||||
endif()
|
||||
|
||||
# Enable Py_GIL_DISABLED for free-threaded CPython (no-GIL build)
|
||||
if(MCRF_FREE_THREADED_PYTHON)
|
||||
target_compile_definitions(mcrogueface PRIVATE Py_GIL_DISABLED)
|
||||
endif()
|
||||
|
||||
# Define MCRF_HEADLESS for headless builds (excludes SFML/ImGui code)
|
||||
if(MCRF_HEADLESS)
|
||||
target_compile_definitions(mcrogueface PRIVATE MCRF_HEADLESS)
|
||||
endif()
|
||||
|
||||
# Define MCRF_SDL2 for SDL2 builds (uses SDL2+OpenGL ES 2 instead of SFML)
|
||||
if(MCRF_SDL2)
|
||||
target_compile_definitions(mcrogueface PRIVATE MCRF_SDL2)
|
||||
endif()
|
||||
|
||||
# Asset/script directories for WASM preloading (game projects override these)
|
||||
set(MCRF_ASSETS_DIR "${CMAKE_SOURCE_DIR}/assets" CACHE PATH "Assets directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_DIR "${CMAKE_SOURCE_DIR}/src/scripts" CACHE PATH "Scripts directory for WASM preloading")
|
||||
set(MCRF_SCRIPTS_PLAYGROUND_DIR "${CMAKE_SOURCE_DIR}/src/scripts_playground" CACHE PATH "Playground scripts for WASM")
|
||||
set(MCRF_SCRIPTS_DEMO_DIR "${CMAKE_SOURCE_DIR}/src/scripts_demo" CACHE PATH "Demo scripts for WASM showcase")
|
||||
|
||||
# Emscripten-specific link options (use ports for zlib, bzip2, sqlite3)
|
||||
if(EMSCRIPTEN)
|
||||
# Base Emscripten options
|
||||
set(EMSCRIPTEN_LINK_OPTIONS
|
||||
-sUSE_ZLIB=1
|
||||
-sUSE_BZIP2=1
|
||||
-sUSE_SQLITE3=1
|
||||
-sALLOW_MEMORY_GROWTH=1
|
||||
-sSTACK_SIZE=2097152
|
||||
-sEXPORTED_RUNTIME_METHODS=ccall,cwrap,FS,IDBFS
|
||||
-sEXPORTED_FUNCTIONS=_main,_run_python_string,_run_python_string_with_output,_reset_python_environment,_notify_canvas_resize,_sync_storage
|
||||
-lidbfs.js
|
||||
-sASSERTIONS=2
|
||||
-sSTACK_OVERFLOW_CHECK=2
|
||||
-fexceptions
|
||||
-sNO_DISABLE_EXCEPTION_CATCHING
|
||||
# Disable features that require dynamic linking support
|
||||
-sERROR_ON_UNDEFINED_SYMBOLS=0
|
||||
-sALLOW_UNIMPLEMENTED_SYSCALLS=1
|
||||
# Preload Python stdlib into virtual filesystem at /lib/python3.14
|
||||
--preload-file=${CMAKE_SOURCE_DIR}/wasm_stdlib/lib@/lib
|
||||
# Preload game scripts into /scripts (playground, demo, or full game)
|
||||
--preload-file=$<IF:$<BOOL:${MCRF_PLAYGROUND}>,${MCRF_SCRIPTS_PLAYGROUND_DIR},$<IF:$<BOOL:${MCRF_DEMO}>,${MCRF_SCRIPTS_DEMO_DIR},${MCRF_SCRIPTS_DIR}>>@/scripts
|
||||
# Preload assets
|
||||
--preload-file=${MCRF_ASSETS_DIR}@/assets
|
||||
# Use custom HTML shell - game shell (fullscreen) or playground shell (REPL)
|
||||
--shell-file=${CMAKE_SOURCE_DIR}/src/$<IF:$<BOOL:${MCRF_GAME_SHELL}>,shell_game.html,shell.html>
|
||||
# Pre-JS to fix browser zoom causing undefined values in events
|
||||
--pre-js=${CMAKE_SOURCE_DIR}/src/emscripten_pre.js
|
||||
)
|
||||
|
||||
# Add SDL2 options if using SDL2 backend
|
||||
if(MCRF_SDL2)
|
||||
list(APPEND EMSCRIPTEN_LINK_OPTIONS
|
||||
-sUSE_SDL=2
|
||||
-sUSE_SDL_MIXER=2
|
||||
-sFULL_ES2=1
|
||||
-sMIN_WEBGL_VERSION=2
|
||||
-sMAX_WEBGL_VERSION=2
|
||||
-sUSE_FREETYPE=1
|
||||
)
|
||||
# SDL2, SDL2_mixer, and FreeType flags are also needed at compile time for headers
|
||||
target_compile_options(mcrogueface PRIVATE
|
||||
-sUSE_SDL=2
|
||||
-sUSE_SDL_MIXER=2
|
||||
-sUSE_FREETYPE=1
|
||||
)
|
||||
message(STATUS "Emscripten SDL2 options enabled: -sUSE_SDL=2 -sUSE_SDL_MIXER=2 -sFULL_ES2=1 -sUSE_FREETYPE=1")
|
||||
endif()
|
||||
|
||||
# WASM debug builds: DWARF symbols, source maps, symbol map for stack traces
|
||||
if(MCRF_WASM_DEBUG)
|
||||
list(APPEND EMSCRIPTEN_LINK_OPTIONS
|
||||
-g4
|
||||
-gsource-map
|
||||
--emit-symbol-map
|
||||
)
|
||||
target_compile_options(mcrogueface PRIVATE -g4)
|
||||
message(STATUS "Emscripten debug enabled: DWARF (-g4), source maps, symbol map")
|
||||
endif()
|
||||
|
||||
target_link_options(mcrogueface PRIVATE ${EMSCRIPTEN_LINK_OPTIONS})
|
||||
|
||||
# Output as HTML to use the shell file
|
||||
set_target_properties(mcrogueface PROPERTIES SUFFIX ".html")
|
||||
|
||||
# Set Python home for the embedded interpreter
|
||||
target_compile_definitions(mcrogueface PRIVATE
|
||||
MCRF_WASM_PYTHON_HOME="/lib/python3.14"
|
||||
)
|
||||
endif()
|
||||
|
||||
# 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})
|
||||
|
||||
|
|
@ -543,63 +65,11 @@ add_custom_command(TARGET mcrogueface POST_BUILD
|
|||
${CMAKE_SOURCE_DIR}/src/scripts $<TARGET_FILE_DIR:mcrogueface>/scripts)
|
||||
|
||||
# Copy Python standard library to build directory
|
||||
if(MCRF_DEBUG_PYTHON)
|
||||
# Copy all libs first (SFML, libtcod, Python stdlib), then overwrite with debug Python
|
||||
# The debug lib has SONAME libpython3.14d.so.1.0, so we need both names
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
||||
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14.so.1.0
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_SOURCE_DIR}/__lib_debug/libpython3.14.so.1.0
|
||||
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14d.so.1.0
|
||||
COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||
libpython3.14d.so.1.0
|
||||
$<TARGET_FILE_DIR:mcrogueface>/lib/libpython3.14d.so)
|
||||
else()
|
||||
add_custom_command(TARGET mcrogueface POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_SOURCE_DIR}/__lib $<TARGET_FILE_DIR:mcrogueface>/lib)
|
||||
endif()
|
||||
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"
|
||||
590
Makefile
|
|
@ -1,590 +0,0 @@
|
|||
# McRogueFace Build Makefile
|
||||
# Usage:
|
||||
# make - Build for Linux (default)
|
||||
# make windows - Cross-compile for Windows using MinGW (release)
|
||||
# make windows-debug - Cross-compile for Windows with console & debug symbols
|
||||
# make clean - Clean Linux build
|
||||
# make clean-windows - Clean Windows build
|
||||
# make run - Run the Linux build
|
||||
#
|
||||
# Profiling (see docs/profiling.md):
|
||||
# make profile - RelWithDebInfo + frame pointers build in build-profile/
|
||||
# make callgrind SCRIPT=tests/benchmarks/foo.py - Callgrind a headless benchmark
|
||||
#
|
||||
# WebAssembly / Emscripten:
|
||||
# make wasm - Build full game for web WITH the REPL console (dev/debug build)
|
||||
# make wasm-game - Build the clean full game for web (fullscreen, no REPL) -- shipped
|
||||
# make playground - Build minimal playground for web REPL
|
||||
# make serve - Serve the clean game (build-wasm-game) locally on port 8080
|
||||
# make serve-game - Serve the clean game (build-wasm-game) locally on port 8080
|
||||
# make serve-dev - Serve the wasm dev build with REPL console (build-emscripten)
|
||||
# make clean-wasm - Clean Emscripten builds
|
||||
#
|
||||
# Packaging:
|
||||
# make package-windows-light - Windows with minimal stdlib (~5 MB)
|
||||
# make package-windows-full - Windows with full stdlib (~15 MB)
|
||||
# make package-linux-light - Linux with minimal stdlib
|
||||
# make package-linux-full - Linux with full stdlib
|
||||
# make package-all - All platform/preset combinations
|
||||
#
|
||||
# Release:
|
||||
# make version-bump NEXT_VERSION=x.y.z-suffix
|
||||
# Tags HEAD with current version, builds all packages, bumps to NEXT_VERSION
|
||||
|
||||
.PHONY: all linux windows windows-debug clean clean-windows clean-dist run
|
||||
.PHONY: wasm wasm-game wasm-debug playground playground-debug serve serve-game serve-dev serve-playground clean-wasm
|
||||
.PHONY: package-windows-light package-windows-full package-linux-light package-linux-full package-all
|
||||
.PHONY: version-bump
|
||||
.PHONY: debug debug-test asan asan-test tsan tsan-test valgrind-test massif-test analyze clean-debug
|
||||
.PHONY: profile callgrind clean-profile
|
||||
.PHONY: install-hooks
|
||||
|
||||
# Number of parallel jobs for compilation
|
||||
JOBS := $(shell nproc 2>/dev/null || echo 4)
|
||||
|
||||
all: linux
|
||||
|
||||
linux:
|
||||
@echo "Building McRogueFace for Linux..."
|
||||
@mkdir -p build
|
||||
@cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
|
||||
@echo "Build complete! Run with: ./build/mcrogueface"
|
||||
|
||||
windows:
|
||||
@echo "Cross-compiling McRogueFace for Windows..."
|
||||
@mkdir -p build-windows
|
||||
@cd build-windows && cmake .. \
|
||||
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
|
||||
@echo "Windows build complete! Output: build-windows/mcrogueface.exe"
|
||||
|
||||
windows-debug:
|
||||
@echo "Cross-compiling McRogueFace for Windows (debug with console)..."
|
||||
@mkdir -p build-windows-debug
|
||||
@cd build-windows-debug && cmake .. \
|
||||
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_WINDOWS_CONSOLE=ON && make -j$(JOBS)
|
||||
@echo "Windows debug build complete! Output: build-windows-debug/mcrogueface.exe"
|
||||
@echo "Run from cmd.exe to see console output"
|
||||
|
||||
clean:
|
||||
@echo "Cleaning Linux build..."
|
||||
@rm -rf build
|
||||
|
||||
clean-windows:
|
||||
@echo "Cleaning Windows builds..."
|
||||
@rm -rf build-windows build-windows-debug
|
||||
|
||||
clean-dist:
|
||||
@echo "Cleaning distribution packages..."
|
||||
@rm -rf dist
|
||||
|
||||
clean-all: clean clean-windows clean-wasm clean-debug clean-profile clean-dist
|
||||
@echo "All builds and packages cleaned."
|
||||
|
||||
run: linux
|
||||
@cd build && ./mcrogueface
|
||||
|
||||
# Install git hooks by symlinking tools/hooks/* into .git/hooks/ (idempotent).
|
||||
install-hooks:
|
||||
@hooks_dir="$$(git rev-parse --git-path hooks)"; \
|
||||
mkdir -p "$$hooks_dir"; \
|
||||
for hook in tools/hooks/*; do \
|
||||
[ -e "$$hook" ] || continue; \
|
||||
name="$$(basename "$$hook")"; \
|
||||
ln -sf "$$(pwd)/$$hook" "$$hooks_dir/$$name"; \
|
||||
echo "Linked $$hooks_dir/$$name -> $$hook"; \
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Documentation / release
|
||||
#
|
||||
# Every piece of the docs pipeline already existed; nothing chained them, so the
|
||||
# site drifted 26 engine commits behind without anything noticing. These targets
|
||||
# are the chain.
|
||||
#
|
||||
# SITE_DIR is the mcrogueface.github.io checkout. Override if yours lives elsewhere:
|
||||
# make release-docs SITE_DIR=/path/to/mcrogueface.github.io
|
||||
# ---------------------------------------------------------------------------
|
||||
SITE_DIR ?= $(CURDIR)/../mcrogueface.github.io
|
||||
|
||||
# Two DIFFERENT refs, easily conflated:
|
||||
#
|
||||
# BASE_REF the PREVIOUS release. The API delta is measured against it, to answer
|
||||
# "what changed, and which pages does that oblige us to revisit?"
|
||||
# RELEASE_REF the tag being CUT. The site pins its source links to it, which is what
|
||||
# freezes this version into the site's history.
|
||||
#
|
||||
# When cutting a release, set both:
|
||||
# make release-docs BASE_REF=0.2.8 RELEASE_REF=0.2.9
|
||||
#
|
||||
# Note: BASE_REF must carry api/manifest.json. The manifest infrastructure landed in
|
||||
# 54624b3, so tags older than that (0.2.8 included) have no baseline to diff against --
|
||||
# api_delta will say so rather than inventing one.
|
||||
BASE_REF ?= $(shell git describe --tags --abbrev=0 2>/dev/null)
|
||||
RELEASE_REF ?= $(shell git describe --tags --always --abbrev=0 2>/dev/null)
|
||||
|
||||
# Regenerate everything derived from the compiled module: man page, API reference
|
||||
# (HTML + Markdown), type stubs, and the tracked api/manifest.json.
|
||||
docs: linux
|
||||
@./tools/generate_all_docs.sh
|
||||
@./build/mcrogueface --headless --exec ../tools/generate_api_manifest.py
|
||||
|
||||
# Re-run every docs snippet and stamp its `# mcrf:` header with what actually
|
||||
# happened (status from the run, verified from the engine, objects from the source).
|
||||
stamp-snippets: linux
|
||||
@python3 tools/stamp_snippets.py
|
||||
|
||||
# CI gate: fail if any snippet is broken or its stamp is stale. Writes nothing.
|
||||
check-snippets: linux
|
||||
@python3 tools/stamp_snippets.py --check
|
||||
|
||||
# Render a deterministic preview PNG for every snippet into snippet-shots/ (gitignored;
|
||||
# the images belong in the doc-site repo, which pulls them from here -- see #381). The
|
||||
# images are a visual-regression oracle: a changed PNG means behaviour changed.
|
||||
snippet-shots: linux
|
||||
@python3 tools/generate_snippet_shots.py
|
||||
|
||||
# What changed in the API since the last release, and which site pages that
|
||||
# obligates you to update (resolved via each page's `mcrf.objects` frontmatter).
|
||||
api-delta:
|
||||
@python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format md
|
||||
|
||||
# The full release documentation pass. Run this when cutting a tag: it refreshes
|
||||
# everything the engine derives, proves the published samples still run, rebuilds
|
||||
# the site's generated reference + snippet library against this engine, and prints
|
||||
# the checklist of hand-written pages the API change obligates you to revisit.
|
||||
#
|
||||
# Deliberately NOT automatic: it does not commit, and it does not touch the
|
||||
# hand-written pages. It tells you what it found and leaves the judgment to you.
|
||||
release-docs: docs stamp-snippets snippet-shots
|
||||
@echo ""
|
||||
@echo "=== Regenerating the site against this engine (SITE_DIR=$(SITE_DIR)) ==="
|
||||
@test -d "$(SITE_DIR)" || { echo "SITE_DIR not found: $(SITE_DIR)"; exit 1; }
|
||||
@cd "$(SITE_DIR)" && python3 tools/build_reference.py
|
||||
@cd "$(SITE_DIR)" && python3 tools/build_library.py --ref $(RELEASE_REF)
|
||||
@echo ""
|
||||
@echo "=== API delta $(BASE_REF) -> now, and the pages it obligates ==="
|
||||
@python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format md
|
||||
@echo ""
|
||||
@echo "Site pinned to RELEASE_REF=$(RELEASE_REF); delta measured from BASE_REF=$(BASE_REF)."
|
||||
@echo "Next: review the delta above, update the hand-written pages it names,"
|
||||
@echo " then commit both repos. For a Gitea checklist issue instead, run:"
|
||||
@echo " python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format gitea"
|
||||
|
||||
.PHONY: docs stamp-snippets check-snippets snippet-shots api-delta release-docs
|
||||
|
||||
# Debug and sanitizer targets
|
||||
debug:
|
||||
@echo "Building McRogueFace with debug Python (pydebug assertions)..."
|
||||
@mkdir -p build-debug
|
||||
@cd build-debug && cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_DEBUG_PYTHON=ON && make -j$(JOBS)
|
||||
@echo "Debug build complete! Output: build-debug/mcrogueface"
|
||||
|
||||
debug-test: debug
|
||||
@echo "Running test suite with debug Python..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-debug \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
python3 run_tests.py -v
|
||||
|
||||
# Profiling build (#345): RelWithDebInfo (-O2 -g) + frame pointers, self-contained
|
||||
# in build-profile/ (lib/assets/scripts copied like the normal build). Suited to
|
||||
# both Callgrind (deterministic, no perms) and perf (--call-graph fp).
|
||||
profile:
|
||||
@echo "Building McRogueFace for profiling (RelWithDebInfo + frame pointers)..."
|
||||
@mkdir -p build-profile
|
||||
@cd build-profile && cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DMCRF_PROFILE=ON && make -j$(JOBS)
|
||||
@echo "Profile build complete! Binary: build-profile/mcrogueface"
|
||||
|
||||
# Callgrind a headless benchmark script. Deterministic, exact instruction counts,
|
||||
# no special permissions. Usage: make callgrind SCRIPT=tests/benchmarks/foo.py
|
||||
# Output: callgrind.out (feed to callgrind_annotate).
|
||||
SCRIPT ?= tests/benchmarks/issue_331_property_read_bench.py
|
||||
callgrind: profile
|
||||
@echo "Running Callgrind on $(SCRIPT)..."
|
||||
@cd build-profile && valgrind --tool=callgrind \
|
||||
--callgrind-out-file=callgrind.out \
|
||||
./mcrogueface --headless --exec ../$(SCRIPT)
|
||||
@echo "Done. Annotate with: callgrind_annotate build-profile/callgrind.out | head -60"
|
||||
|
||||
clean-profile:
|
||||
@echo "Cleaning profile build..."
|
||||
@rm -rf build-profile
|
||||
|
||||
asan:
|
||||
@echo "Building McRogueFace with ASan + UBSan..."
|
||||
@mkdir -p build-asan
|
||||
@cd build-asan && cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_DEBUG_PYTHON=ON \
|
||||
-DMCRF_SANITIZE_ADDRESS=ON \
|
||||
-DMCRF_SANITIZE_UNDEFINED=ON && make -j$(JOBS)
|
||||
@echo "ASan build complete! Output: build-asan/mcrogueface"
|
||||
|
||||
asan-test: asan
|
||||
@echo "Running test suite under ASan + UBSan..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-asan \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
PYTHONMALLOC=malloc \
|
||||
ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:print_summary=1" \
|
||||
LSAN_OPTIONS="suppressions=$(CURDIR)/sanitizers/asan.supp" \
|
||||
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
|
||||
python3 run_tests.py -v --sanitizer
|
||||
|
||||
# Fuzzing targets (clang-18 + libFuzzer + ASan + UBSan).
|
||||
# Design: ONE instrumented executable `mcrfpy_fuzz` that embeds CPython,
|
||||
# registers the mcrfpy module, and dispatches each libFuzzer iteration to
|
||||
# a Python `fuzz_one_input(data)` function loaded from the script named by
|
||||
# the MCRF_FUZZ_TARGET env var. libFuzzer instruments the C++ engine code
|
||||
# where all the #258-#278 bugs live. No atheris dependency.
|
||||
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior audio_dsp import_parsers texture_factory shader_bindings
|
||||
FUZZ_SECONDS ?= 30
|
||||
|
||||
# Shared env for running the fuzz binary. PYTHONHOME points at the build-fuzz
|
||||
# copy of the bundled stdlib (post-build copied into build-fuzz/lib/).
|
||||
# ASAN_OPTIONS: leak detection disabled because libFuzzer intentionally holds
|
||||
# inputs for its corpus; abort_on_error ensures crashes are loud and repro-able.
|
||||
define FUZZ_ENV
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
PYTHONMALLOC=malloc \
|
||||
PYTHONHOME=../__lib/Python \
|
||||
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1" \
|
||||
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
|
||||
endef
|
||||
|
||||
fuzz-build:
|
||||
@echo "Building mcrfpy_fuzz with libFuzzer + ASan (clang-18)..."
|
||||
@mkdir -p build-fuzz
|
||||
@cd build-fuzz && CC=clang-18 CXX=clang++-18 cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_DEBUG_PYTHON=ON \
|
||||
-DMCRF_SANITIZE_ADDRESS=ON \
|
||||
-DMCRF_SANITIZE_UNDEFINED=ON \
|
||||
-DMCRF_FUZZER=ON \
|
||||
-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld && make -j$(JOBS) mcrfpy_fuzz
|
||||
@echo "Fuzz build complete! Output: build-fuzz/mcrfpy_fuzz"
|
||||
|
||||
fuzz: fuzz-build
|
||||
@for t in $(FUZZ_TARGETS); do \
|
||||
if [ ! -f tests/fuzz/fuzz_$$t.py ]; then \
|
||||
echo "SKIP: tests/fuzz/fuzz_$$t.py does not exist yet"; \
|
||||
continue; \
|
||||
fi; \
|
||||
echo "=== fuzzing $$t for $(FUZZ_SECONDS)s ==="; \
|
||||
mkdir -p tests/fuzz/corpora/$$t tests/fuzz/crashes; \
|
||||
( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$$t \
|
||||
./mcrfpy_fuzz \
|
||||
-max_total_time=$(FUZZ_SECONDS) \
|
||||
-artifact_prefix=../tests/fuzz/crashes/$$t- \
|
||||
../tests/fuzz/corpora/$$t ../tests/fuzz/seeds/$$t ) || exit 1; \
|
||||
done
|
||||
|
||||
fuzz-long: fuzz-build
|
||||
@test -n "$(TARGET)" || (echo "Usage: make fuzz-long TARGET=<name> SECONDS=<n>"; exit 1)
|
||||
@test -f tests/fuzz/fuzz_$(TARGET).py || (echo "No target: tests/fuzz/fuzz_$(TARGET).py"; exit 1)
|
||||
@mkdir -p tests/fuzz/corpora/$(TARGET) tests/fuzz/crashes
|
||||
@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
|
||||
./mcrfpy_fuzz \
|
||||
-max_total_time=$(or $(SECONDS),3600) \
|
||||
-artifact_prefix=../tests/fuzz/crashes/$(TARGET)- \
|
||||
../tests/fuzz/corpora/$(TARGET) ../tests/fuzz/seeds/$(TARGET) )
|
||||
|
||||
fuzz-repro:
|
||||
@test -n "$(TARGET)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
|
||||
@test -n "$(CRASH)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
|
||||
@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
|
||||
./mcrfpy_fuzz ../$(CRASH) )
|
||||
|
||||
clean-fuzz:
|
||||
@echo "Cleaning fuzz build and corpora..."
|
||||
@rm -rf build-fuzz tests/fuzz/corpora tests/fuzz/crashes
|
||||
|
||||
tsan:
|
||||
@echo "Building McRogueFace with TSan + free-threaded Python..."
|
||||
@echo "NOTE: Requires free-threaded debug Python built with:"
|
||||
@echo " tools/build_debug_python.sh --tsan"
|
||||
@mkdir -p build-tsan
|
||||
@cd build-tsan && cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_FREE_THREADED_PYTHON=ON \
|
||||
-DMCRF_SANITIZE_THREAD=ON && make -j$(JOBS)
|
||||
@echo "TSan build complete! Output: build-tsan/mcrogueface"
|
||||
|
||||
tsan-test: tsan
|
||||
@echo "Running test suite under TSan..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-tsan \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" \
|
||||
python3 run_tests.py -v --sanitizer
|
||||
|
||||
valgrind-test: debug
|
||||
@echo "Running test suite under Valgrind memcheck..."
|
||||
cd tests && MCRF_BUILD_DIR=../build-debug \
|
||||
MCRF_LIB_DIR=../__lib_debug \
|
||||
MCRF_TIMEOUT_MULTIPLIER=50 \
|
||||
PYTHONMALLOC=malloc \
|
||||
python3 run_tests.py -v --valgrind
|
||||
|
||||
massif-test: debug
|
||||
@echo "Running heap profiling under Valgrind Massif..."
|
||||
@mkdir -p build-debug
|
||||
cd build-debug && valgrind --tool=massif \
|
||||
--massif-out-file=massif.out \
|
||||
--pages-as-heap=no \
|
||||
--detailed-freq=10 \
|
||||
--max-snapshots=100 \
|
||||
./mcrogueface --headless --exec ../tests/benchmarks/stress_test_suite.py
|
||||
@echo "Massif output: build-debug/massif.out"
|
||||
@echo "View with: ms_print build-debug/massif.out"
|
||||
|
||||
analyze:
|
||||
@echo "Running cppcheck static analysis..."
|
||||
cppcheck --enable=warning,performance,portability \
|
||||
--suppress=missingIncludeSystem \
|
||||
--suppress=unusedFunction \
|
||||
--suppress=noExplicitConstructor \
|
||||
--suppress=missingOverride \
|
||||
--inline-suppr \
|
||||
-I src/ -I deps/ -I deps/cpython -I deps/Python \
|
||||
-I src/platform -I src/3d -I src/tiled -I src/ldtk -I src/audio \
|
||||
--std=c++20 \
|
||||
--quiet \
|
||||
src/ 2>&1
|
||||
@echo "Static analysis complete."
|
||||
|
||||
clean-debug:
|
||||
@echo "Cleaning debug/sanitizer builds..."
|
||||
@rm -rf build-debug build-asan build-tsan build-fuzz
|
||||
|
||||
# Packaging targets using tools/package.sh
|
||||
package-windows-light: windows
|
||||
@./tools/package.sh windows light
|
||||
|
||||
package-windows-full: windows
|
||||
@./tools/package.sh windows full
|
||||
|
||||
package-linux-light: linux
|
||||
@./tools/package.sh linux light
|
||||
|
||||
package-linux-full: linux
|
||||
@./tools/package.sh linux full
|
||||
|
||||
package-all: windows linux
|
||||
@./tools/package.sh all
|
||||
|
||||
# Legacy target for backwards compatibility
|
||||
package-windows: package-windows-full
|
||||
|
||||
# Emscripten / WebAssembly targets
|
||||
# Requires: source ~/emsdk/emsdk_env.sh (or wherever your emsdk is installed)
|
||||
#
|
||||
# For iterative development, configure once then rebuild:
|
||||
# source ~/emsdk/emsdk_env.sh && emmake make -C build-emscripten
|
||||
#
|
||||
wasm:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-emscripten/Makefile ]; then \
|
||||
echo "Configuring WebAssembly build (full game)..."; \
|
||||
mkdir -p build-emscripten; \
|
||||
cd build-emscripten && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace for WebAssembly..."
|
||||
@emmake make -C build-emscripten -j$(JOBS)
|
||||
@echo "WebAssembly dev build complete (game + REPL console)! Files in build-emscripten/"
|
||||
@echo "Run 'make serve-dev' to test locally. For the clean shipped game, use 'make wasm-game' + 'make serve'."
|
||||
|
||||
playground:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-playground/Makefile ]; then \
|
||||
echo "Configuring Playground build..."; \
|
||||
mkdir -p build-playground; \
|
||||
cd build-playground && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_PLAYGROUND=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace Playground for WebAssembly..."
|
||||
@emmake make -C build-playground -j$(JOBS)
|
||||
@echo "Playground build complete! Files in build-playground/"
|
||||
@echo "Run 'make serve-playground' to test locally"
|
||||
|
||||
serve:
|
||||
@echo "Serving the clean game build (build-wasm-game) at http://localhost:8080"
|
||||
@echo "(Run 'make wasm-game' first if it is not built. For the REPL dev build, use 'make serve-dev'.)"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-wasm-game && python3 -m http.server 8080
|
||||
|
||||
serve-dev:
|
||||
@echo "Serving the wasm DEV build with REPL console (build-emscripten) at http://localhost:8080"
|
||||
@echo "(Run 'make wasm' first if it is not built.)"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-emscripten && python3 -m http.server 8080
|
||||
|
||||
serve-playground:
|
||||
@echo "Serving Playground build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-playground && python3 -m http.server 8080
|
||||
|
||||
wasm-game:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-wasm-game/Makefile ]; then \
|
||||
echo "Configuring WebAssembly game build (fullscreen, no REPL)..."; \
|
||||
mkdir -p build-wasm-game; \
|
||||
cd build-wasm-game && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_GAME_SHELL=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace game for WebAssembly..."
|
||||
@emmake make -C build-wasm-game -j$(JOBS)
|
||||
@echo "Clean game build complete (no REPL console)! Files in build-wasm-game/"
|
||||
@echo "Run 'make serve' (or 'make serve-game') to test locally"
|
||||
|
||||
serve-game:
|
||||
@echo "Serving game build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-wasm-game && python3 -m http.server 8080
|
||||
|
||||
wasm-demo:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-wasm-demo/Makefile ]; then \
|
||||
echo "Configuring WebAssembly demo build..."; \
|
||||
mkdir -p build-wasm-demo; \
|
||||
cd build-wasm-demo && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_DEMO=ON \
|
||||
-DMCRF_GAME_SHELL=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace demo for WebAssembly..."
|
||||
@emmake make -C build-wasm-demo -j$(JOBS)
|
||||
@cp web/index.html build-wasm-demo/index.html
|
||||
@echo "Demo build complete! Files in build-wasm-demo/"
|
||||
@echo "Run 'make serve-demo' to test locally"
|
||||
|
||||
serve-demo:
|
||||
@echo "Serving demo build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-wasm-demo && python3 -m http.server 8080
|
||||
|
||||
clean-wasm:
|
||||
@echo "Cleaning Emscripten builds..."
|
||||
@rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo build-wasm-debug build-playground-debug
|
||||
|
||||
wasm-debug:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-wasm-debug/Makefile ]; then \
|
||||
echo "Configuring WebAssembly debug build (DWARF + source maps)..."; \
|
||||
mkdir -p build-wasm-debug; \
|
||||
cd build-wasm-debug && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_WASM_DEBUG=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace for WebAssembly (debug)..."
|
||||
@emmake make -C build-wasm-debug -j$(JOBS)
|
||||
@echo "Debug WASM build complete! Files in build-wasm-debug/"
|
||||
@echo "Debug artifacts: .wasm.map (source map), .symbols (symbol map)"
|
||||
@echo "Run 'make serve-wasm-debug' to test locally"
|
||||
|
||||
serve-wasm-debug:
|
||||
@echo "Serving debug WASM build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-wasm-debug && python3 -m http.server 8080
|
||||
|
||||
playground-debug:
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@if [ ! -f build-playground-debug/Makefile ]; then \
|
||||
echo "Configuring Playground debug build (DWARF + source maps)..."; \
|
||||
mkdir -p build-playground-debug; \
|
||||
cd build-playground-debug && emcmake cmake .. \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DMCRF_SDL2=ON \
|
||||
-DMCRF_PLAYGROUND=ON \
|
||||
-DMCRF_WASM_DEBUG=ON; \
|
||||
fi
|
||||
@echo "Building McRogueFace Playground for WebAssembly (debug)..."
|
||||
@emmake make -C build-playground-debug -j$(JOBS)
|
||||
@echo "Playground debug build complete! Files in build-playground-debug/"
|
||||
@echo "Run 'make serve-playground-debug' to test locally"
|
||||
|
||||
serve-playground-debug:
|
||||
@echo "Serving debug Playground build at http://localhost:8080"
|
||||
@echo "Press Ctrl+C to stop"
|
||||
@cd build-playground-debug && python3 -m http.server 8080
|
||||
|
||||
# Current version extracted from source
|
||||
CURRENT_VERSION := $(shell grep 'MCRFPY_VERSION' src/McRogueFaceVersion.h | sed 's/.*"\(.*\)"/\1/')
|
||||
|
||||
# Release workflow: tag current version, build all packages, bump to next version
|
||||
# Usage: make version-bump NEXT_VERSION=0.2.6-prerelease-7drl2026
|
||||
version-bump:
|
||||
ifndef NEXT_VERSION
|
||||
$(error Usage: make version-bump NEXT_VERSION=x.y.z-suffix)
|
||||
endif
|
||||
@if ! command -v emcmake >/dev/null 2>&1; then \
|
||||
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
# git status (clean working dir check), but ignore modules/, because building submodules dirties their subdirs
|
||||
@if [ -n "$$(git status --porcelain | grep -v modules)" ]; then \
|
||||
echo "Error: Working tree is not clean. Commit or stash changes first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "=== Releasing $(CURRENT_VERSION) ==="
|
||||
@# Idempotent tag: ok if it already points at HEAD (resuming partial run)
|
||||
@if git rev-parse "$(CURRENT_VERSION)" >/dev/null 2>&1; then \
|
||||
TAG_COMMIT=$$(git rev-parse "$(CURRENT_VERSION)^{}"); \
|
||||
HEAD_COMMIT=$$(git rev-parse HEAD); \
|
||||
if [ "$$TAG_COMMIT" != "$$HEAD_COMMIT" ]; then \
|
||||
echo "Error: Tag $(CURRENT_VERSION) already exists but points to a different commit."; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "Tag $(CURRENT_VERSION) already exists at HEAD (resuming)."; \
|
||||
else \
|
||||
git tag "$(CURRENT_VERSION)"; \
|
||||
fi
|
||||
$(MAKE) package-linux-full
|
||||
$(MAKE) package-windows-full
|
||||
$(MAKE) wasm-game
|
||||
@echo "Packaging WASM build (clean game, no REPL console)..."
|
||||
@mkdir -p dist
|
||||
cd build-wasm-game && zip -r ../dist/McRogueFace-$(CURRENT_VERSION)-WASM.zip \
|
||||
mcrogueface.html mcrogueface.js mcrogueface.wasm mcrogueface.data
|
||||
@echo ""
|
||||
@echo "Bumping version: $(CURRENT_VERSION) -> $(NEXT_VERSION)"
|
||||
@sed -i 's|MCRFPY_VERSION "$(CURRENT_VERSION)"|MCRFPY_VERSION "$(NEXT_VERSION)"|' src/McRogueFaceVersion.h
|
||||
@TAGGED_HASH=$$(git rev-parse --short HEAD); \
|
||||
git add src/McRogueFaceVersion.h && \
|
||||
git commit -m "Version bump: $(CURRENT_VERSION) ($$TAGGED_HASH) -> $(NEXT_VERSION)"
|
||||
@echo ""
|
||||
@echo "=== Release $(CURRENT_VERSION) complete ==="
|
||||
@echo "Tag: $(CURRENT_VERSION)"
|
||||
@echo "Next: $(NEXT_VERSION)"
|
||||
@echo "Packages:"
|
||||
@ls -lh dist/*$(CURRENT_VERSION)* 2>/dev/null
|
||||
222
README.md
|
|
@ -3,221 +3,79 @@
|
|||
|
||||
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"
|
||||
**Pre-Alpha Release Demo**: my 7DRL 2025 entry *"Crypt of Sokoban"* - a prototype with buttons, boulders, enemies, and items.
|
||||
|
||||
📖 **[Full Documentation & Tutorials](https://mcrogueface.github.io/)** - Quickstart guide, API reference, and cookbook
|
||||
## Tenets
|
||||
|
||||
- **Python & C++ Hand-in-Hand**: Create your game without ever recompiling. Your Python commands create C++ objects, and animations can occur without calling Python at all.
|
||||
- **Simple Yet Flexible UI System**: Sprites, Grids, Frames, and Captions with full animation support
|
||||
- **Entity-Component Architecture**: Implement your game objects with Python integration
|
||||
- **Built-in Roguelike Support**: Dungeon generation, pathfinding, and field-of-view via libtcod (demos still under construction)
|
||||
- **Automation API**: PyAutoGUI-inspired event generation framework. All McRogueFace interactions can be performed headlessly via script: for software testing or AI integration
|
||||
- **Interactive Development**: Python REPL integration for live game debugging. Use `mcrogueface` like a Python interpreter
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Download** the [latest release](https://github.com/jmccardle/McRogueFace/releases/latest):
|
||||
- **Windows**: `McRogueFace-*-Win.zip`
|
||||
- **Linux**: `McRogueFace-*-Linux.tar.bz2`
|
||||
```bash
|
||||
# Clone and build
|
||||
git clone <wherever you found this repo>
|
||||
cd McRogueFace
|
||||
make
|
||||
|
||||
Extract and run `mcrogueface` (or `mcrogueface.exe` on Windows) to see the demo game.
|
||||
# Run the example game
|
||||
cd build
|
||||
./mcrogueface
|
||||
```
|
||||
|
||||
### Your First Game
|
||||
|
||||
Create `scripts/game.py` (or edit the existing one):
|
||||
## Example: Creating a Simple Scene
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create and activate a scene
|
||||
scene = mcrfpy.Scene("game")
|
||||
scene.activate()
|
||||
# Create a new scene
|
||||
mcrfpy.createScene("intro")
|
||||
|
||||
# Load a sprite sheet
|
||||
texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
# Add a text caption
|
||||
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
|
||||
caption.size = 48
|
||||
caption.fill_color = (255, 255, 255)
|
||||
|
||||
# Create a tile grid
|
||||
grid = mcrfpy.Grid(grid_size=(20, 15), texture=texture, pos=(50, 50), size=(640, 480))
|
||||
grid.zoom = 2.0
|
||||
scene.children.append(grid)
|
||||
# Add to scene
|
||||
mcrfpy.sceneUI("intro").append(caption)
|
||||
|
||||
# Add a player entity
|
||||
player = mcrfpy.Entity(pos=(10, 7), texture=texture, sprite_index=84)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Handle keyboard input
|
||||
def on_key(key, state):
|
||||
if state != "start":
|
||||
return
|
||||
x, y = int(player.x), int(player.y)
|
||||
if key == "W": y -= 1
|
||||
elif key == "S": y += 1
|
||||
elif key == "A": x -= 1
|
||||
elif key == "D": x += 1
|
||||
player.x, player.y = x, y
|
||||
|
||||
scene.on_key = on_key
|
||||
```
|
||||
|
||||
Run `mcrogueface` and you have a movable character!
|
||||
|
||||
### Visual Framework
|
||||
|
||||
- **Sprite**: Single image or sprite from a shared sheet
|
||||
- **Caption**: Text rendering with fonts
|
||||
- **Frame**: Container rectangle for composing UIs
|
||||
- **Grid**: 2D tile array with zoom and camera control
|
||||
- **Entity**: Grid-based game object with sprite and pathfinding
|
||||
- **Animation**: Interpolate any property over time with easing
|
||||
|
||||
## 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: Main Menu with Buttons
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Create a scene
|
||||
scene = mcrfpy.Scene("menu")
|
||||
|
||||
# Add a background frame
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(1024, 768),
|
||||
fill_color=mcrfpy.Color(20, 20, 40))
|
||||
scene.children.append(bg)
|
||||
|
||||
# Add a title
|
||||
title = mcrfpy.Caption(pos=(312, 100), text="My Roguelike",
|
||||
fill_color=mcrfpy.Color(255, 255, 100))
|
||||
title.font_size = 48
|
||||
scene.children.append(title)
|
||||
|
||||
# Create a button
|
||||
button = mcrfpy.Frame(pos=(362, 300), size=(300, 80),
|
||||
fill_color=mcrfpy.Color(50, 150, 50))
|
||||
button_text = mcrfpy.Caption(pos=(90, 25), text="Start Game")
|
||||
button.children.append(button_text)
|
||||
|
||||
def on_click(x, y, btn):
|
||||
print("Game starting!")
|
||||
|
||||
button.on_click = on_click
|
||||
scene.children.append(button)
|
||||
|
||||
scene.activate()
|
||||
# Switch to the scene
|
||||
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://dev.ffwf.net/forgejo/john/McRogueFace/wiki)**
|
||||
|
||||
Key wiki pages:
|
||||
|
||||
- **[Home](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Home)** - Documentation hub with multiple entry points
|
||||
- **[Grid System](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Grid-System)** - Three-layer grid architecture
|
||||
- **[Python Binding System](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Python-Binding-System)** - C++/Python integration
|
||||
- **[Performance and Profiling](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Performance-and-Profiling)** - Optimization tools
|
||||
- **[Adding Python Bindings](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Adding-Python-Bindings)** - Step-by-step binding guide
|
||||
- **[Issue Roadmap](https://dev.ffwf.net/forgejo/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
|
||||
- **[docs/api-stability.md](docs/api-stability.md)** - 1.0 compatibility policy (value semantics, bulk-edit convention, subinterpreter exclusion)
|
||||
- **[docs/threading-model.md](docs/threading-model.md)** - Threading contract for off-main-thread access via `mcrfpy.lock()`
|
||||
- **[roguelike_tutorial/](roguelike_tutorial/)** - Complete roguelike tutorial implementations
|
||||
|
||||
> **Note:** Running mcrfpy in a Python subinterpreter is unsupported in 1.x; the module declares `m_size = -1` and will refuse or misbehave. See [#220](https://dev.ffwf.net/forgejo/john/McRogueFace/issues/220).
|
||||
|
||||
## Build Requirements
|
||||
## Requirements
|
||||
|
||||
- C++17 compiler (GCC 7+ or Clang 5+)
|
||||
- CMake 3.14+
|
||||
- Python 3.14 (embedded)
|
||||
- SFML 2.6
|
||||
- Python 3.12+
|
||||
- SFML 2.5+
|
||||
- Linux or Windows (macOS untested)
|
||||
|
||||
See [BUILD_FROM_SOURCE.md](BUILD_FROM_SOURCE.md) for detailed compilation instructions.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
McRogueFace/
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output: this is what you distribute
|
||||
│ ├── assets/ # (copied from assets/)
|
||||
│ ├── scripts/ # (copied from src/scripts/)
|
||||
│ └── lib/ # Python stdlib and extension modules
|
||||
├── docs/ # Generated HTML, markdown API docs
|
||||
├── src/ # C++ engine source
|
||||
│ └── scripts/ # Python game scripts
|
||||
├── stubs/ # .pyi type stubs for IDE integration
|
||||
├── tests/ # Automated test suite
|
||||
└── tools/ # Documentation generation scripts
|
||||
├── scripts/ # Python game scripts
|
||||
├── assets/ # Sprites, fonts, audio
|
||||
├── build/ # Build output directory
|
||||
└── tests/ # Automated test suite
|
||||
```
|
||||
|
||||
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 [Forgejo Issues](https://dev.ffwf.net/forgejo/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://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Issue-Roadmap) on the wiki for organized view of all open tasks.
|
||||
The project has a private roadmap and issue list. Reach out via email or social media if you have bugs or feature requests.
|
||||
|
||||
## License
|
||||
|
||||
|
|
@ -225,6 +83,6 @@ This project is licensed under the MIT License - see LICENSE file for details.
|
|||
|
||||
## Acknowledgments
|
||||
|
||||
- Developed for 7-Day Roguelike 2023, 2024, 2025, 2026 - here's to many more
|
||||
- Developed for 7-Day Roguelike 2023, 2024, 2025 - here's to many more
|
||||
- Built with [SFML](https://www.sfml-dev.org/), [libtcod](https://github.com/libtcod/libtcod), and Python
|
||||
- Inspired by David Churchill's COMP4300 game engine lectures
|
||||
|
|
|
|||
154
ROADMAP.md
|
|
@ -1,154 +0,0 @@
|
|||
# McRogueFace - Development Roadmap
|
||||
|
||||
**Version**: 0.2.8 | **Era**: McRogueFace (2D roguelikes) -- on the road to 1.0
|
||||
|
||||
For detailed architecture, philosophy, and decision framework, see the [Strategic Direction](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Strategic-Direction) wiki page. For per-issue tracking, see the [Issue Roadmap](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Issue-Roadmap).
|
||||
|
||||
---
|
||||
|
||||
## What Has Shipped
|
||||
|
||||
**Alpha 0.1** (2024) -- First complete release. Milestone: all datatypes behaving.
|
||||
|
||||
**0.2 series** (Jan-Mar 2026) -- Weekly updates to GitHub. Key additions:
|
||||
- 3D/Voxel pipeline (experimental): Viewport3D, Camera3D, Entity3D, VoxelGrid with greedy meshing and serialization
|
||||
- Procedural generation: HeightMap, BSP, NoiseSource, DiscreteMap
|
||||
- Tiled and LDtk import with Wang tile / AutoRule resolution
|
||||
- Emscripten/SDL2 backend for WebAssembly deployment
|
||||
- Animation callbacks, mouse event system, grid cell callbacks
|
||||
- Multi-layer grid system with chunk-based rendering and dirty-flag caching
|
||||
- Documentation macro system with auto-generated API docs, man pages, and type stubs
|
||||
- Windows cross-compilation, mobile-ish WASM support, SDL2_mixer audio
|
||||
- Behavior/Trigger turn manager: `grid.step()`, entity labels, `cell_pos`, Dijkstra-backed pathfinding (#295-#303)
|
||||
|
||||
**Proving grounds**: Crypt of Sokoban (7DRL 2025), then 7DRL 2026 -- both shipped on the same engine. The 2026 jam surfaced hotfix-worthy issues (SDL key scancodes, composite textures) that have since landed on master.
|
||||
|
||||
---
|
||||
|
||||
## Current Focus: API Freeze + Memory Safety Sweep
|
||||
|
||||
7DRL 2026 is behind us (Feb 28 -- Mar 8). The engine has two concurrent tracks to 1.0:
|
||||
|
||||
### Track 1: API Freeze
|
||||
The process is underway. Closed in this pass: camelCase module functions (#304), deprecated `sprite_number` (#305), legacy string enum comparisons (#306), `Color.__eq__`/`__ne__` (#307), `Grid.position` alias (#308). The freeze decisions are now locked behind a public API-surface snapshot regression test (#314), so accidental signature drift fails CI.
|
||||
|
||||
Remaining freeze work:
|
||||
1. Catalog every public Python class, method, and property -- audit against `stubs/mcrfpy.pyi` and generated docs (snapshot test now enforces the catalog)
|
||||
2. Identify any last naming/signature/default changes before committing
|
||||
3. Final breaking-change pass, bundled
|
||||
4. Document the stable API as the contract
|
||||
5. Experimental modules (3D/Voxel) stay out of the freeze with an `experimental` label
|
||||
|
||||
### Track 2: Fuzz-Driven Bug Sweep
|
||||
The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing (W1), native harness (W2/W3), then six targeted fuzzers under `tests/fuzz/`:
|
||||
- `fuzz_grid_entity` -- EntityCollection lifetime (W4, fixed #258-#263, #273, #274)
|
||||
- `fuzz_property_types` -- refcount / type confusion (W5, fixed #267, #268, #272)
|
||||
- `fuzz_anim_timer_scene` -- animation/timer/scene lifecycles (W6)
|
||||
- `fuzz_fov` -- compute_fov parameters (W8, fixed #310)
|
||||
- `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7)
|
||||
- `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311)
|
||||
|
||||
Coverage extension (#312) added four more: `fuzz_audio_dsp` (SoundBuffer DSP), `fuzz_import_parsers` (Tiled/LDtk file parsers), `fuzz_texture_factory` (byte ingestion), `fuzz_shader_bindings` (uniform-binding lifetime), plus Tier C surface folded into the existing targets. That run found five new bugs: #321 (HIGH -- ColorLayer.draw_fov bad-free), #322 (WangSet.terrain_enum error-pending abort), #323/#324/#325 (float→int UB in pitch_shift/hsl_shift/Vector) -- all five now fixed and merged, see Recently Shipped.
|
||||
|
||||
### Recently Shipped (April 2026)
|
||||
- **#294** -- `entity.perspective_map` replaces flat `vector<UIGridPointState>` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered.
|
||||
- **#315** -- Pathfinding API extended with built-in heuristics (Euclidean/Manhattan/Chebyshev/Diagonal/Zero), multi-root Dijkstra, FLEE primitives (invert + descent), and an interactive demo. EntityBehavior SEEK/FLEE refactored to a `PathProvider` strategy.
|
||||
- **Phase 5.2** -- six performance benchmark scripts under `tests/benchmarks/` covering grid.step(), FOV writeback cost, spatial hash vs. O(n), pathfinding with collision labels, multi-GridView render, and Dijkstra variants. Baselines under `tests/benchmarks/baseline/phase5_2/`.
|
||||
- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source.
|
||||
|
||||
### Recently Shipped (July 2026)
|
||||
- **Grid input revival + the GridData/GridView type split (#355, #357-#368, #370, #371)** -- merged to master as `36e74e0`; 15 issues closed. Grid input had been dead since the #252 GridView unification: cell callbacks never fired, grid children were unclickable, cell hover was stuck. The cause was structural -- `UIGrid` inherited *both* `UIDrawable` and `GridData`, so every map secretly carried a second camera and RenderTexture that nothing on screen corresponded to, and input, children, and dirty propagation all dead-ended in it. **Input fix (#355, #357, #358, #360, #362, #363, #365, #366)**: cell callbacks and hit-testing move to `UIGridView` -- input belongs to the *camera*, not the data, so two views over one map track hover independently; a `weak_ptr` view registry replaces the single `owning_view` (N views, one map); `find()`/`findAll()` descend into grids again; the ImGui scene explorer can expand them; hover-exit fires when the cursor leaves the window; `UICollection.repr` names Grids instead of `"UIDrawable"`. **Type split (#359, #361, #364, #370, #371)**: `UIGrid` is **deleted, not demoted**. `mcrfpy.GridData` is now public and standalone-constructible -- a map with no position, size, camera, or `render()`; only `UIGridView` is a `UIDrawable`. `mcrfpy.Grid` is an *alias bound to the same type object* as `GridView` (not a subclass -- `isinstance` works both ways, no MRO to explain). Overlay children belong to the view; entities belong to the map. Breaking change: `entity.grid` now returns a `GridData` (the setter still accepts a view). The split flushed out four latent bugs, each verified against unmodified master before being fixed: **#370** `Grid.center_camera()` and `Grid.size = ...` were silent **no-ops** (writing to the ghost camera nothing rendered); **#371** the shader uniform binder did type-confused pointer arithmetic, reading a `GridView` wrapper as a `_GridData` wrapper -- which "worked" only because both wrappers had identical layout and both C++ classes happened to put `UIDrawable` at offset 0; **#368** `markContentDirty()`'s walk up the parent chain was gated on a flag with exactly one `clearDirty()` call site in the whole engine, so the guard was permanently false and content invalidation propagated **nowhere** -- `Frame(cache_subtree=True)` silently froze its subtree's content (text never updated, colours never changed; only movement survived, being already on an unconditional path). The walk is now unconditional; entity movement, the hot path, is unaffected (164 ns/move). **#367**: hover state is cleared on focus loss. Suite 329/329. Riding along on the same branch: generated docs are **untracked** (regenerated by `tools/hooks/pre-commit`, installed via `make install-hooks`, and shipped inside release artifacts), with a compact `api/manifest.json` -- signatures + docstring hashes + carried-forward `since`/`modified` lifecycle metadata -- as the one committed exception, so API drift stays diffable by ref via `tools/api_delta.py REF1 REF2` without rebuilding at old commits.
|
||||
- **Grid render/memory sprint (#351, #334, #338, #332, #335; safety #353/#354)** -- a batch review closed dead/unbottlenecked issues (#117 entity pool, #145 TexturePool -- no demonstrated allocation gate; reopen after profiling) and shipped six grid-layer changes. **#351**: `UIGridView::render()` had *no* dirty check post-#252 and cleared+redrew every frame -- added a `GridData::content_generation` counter (bumped at every data/layer/entity mutation choke point) so an idle, non-perspective, childless view re-blits its cached texture instead of re-rendering; fixed a latent bug where Python entity `set_position`/`set_spritenumber` updated the spatial hash but never invalidated render (masked by the unconditional redraw). A follow-on regression (**6b99a1b**): the C++ turn manager `py_step` mutates entity cells directly, bypassing the setters -- it now calls `GridData::markCompositeDirty()` once per step (must qualify `GridData::`; `using UIDrawable::markCompositeDirty` shadows the data-layer override). **#334**: `DiscreteMap` exposes its `uint8_t` plane via the Python buffer protocol (`tp_as_buffer`, 2D zero-copy numpy/memoryview views). **#332**: `GridData` migrated from per-cell `UIGridPoint` structs (24 B for 2 B payload) + the whole chunk manager to two dense `std::vector<uint8_t>` planes (walkable/transparent) -- deleted `GridChunk`/`ChunkManager`/`CHUNK_THRESHOLD`, stripped `UIGridPoint` to a Python-accessor namespace, net **-257 lines**; pathfinding's `isCellWalkable`/`cellWalkable` were the critical non-obvious callers. **#335**: `ColorLayer`/`TileLayer` get zero-copy numpy views through a new `with layer.edit() as view:` context manager (implements the #328 convention; `__exit__` marks the layer dirty) -- `(h,w,4)` uint8 and `(h,w)` int32. **#338** (issue left open): safe subset only -- `rotationTexture` is now lazily allocated (`unique_ptr`) on Grid/GridView; deferred the `render_sprite` relocation as not worth the churn. Validation was delivered via **The Crucible** (**#354**, `tests/benchmarks/crucible.py`) -- a headless, deterministic, wall-clock A/B harness that safely replaces the windowed Gauntlet (which OOM-hard-locked the desktop during grid ramp). **#353** hardened the Gauntlet with `predict_bytes`/`max_load` refusal + an `RLIMIT_AS` backstop + RSS watchdog. Crucible A/B, current master vs the 0.2.8 dist artifact (both headless): grid_alloc **-58.9%**, layer_fill **-56.1%**, entity_churn **-43.0%**, grid_fill **-25.4%**, fov_storm/step_swarm/path_queries single-digit; **geomean 0.673 = ~32.7% faster overall**, peak RSS ~103 vs ~119 MB -- the big wins track #332 SoA + #329 indexing. Suite 314/314. Remaining in the thread: **#333** (lazy TCOD map rebuild, unblocked by #332), **#352** (perspective-grid early-out), and the deferred #338 relocation.
|
||||
- **Native profiling rig + hot-path perf batch (#345, #331, #342/#343/#344/#348)** -- a Callgrind/perf profiling workflow (`make profile` -> `build-profile/` RelWithDebInfo + frame pointers; `make callgrind SCRIPT=...`; `docs/profiling.md`) landed as **#345**, then drove five measured fixes. **#331**: hot property getters stop re-importing `mcrfpy` per call. **#348** (found *by* Callgrind, not by guessing): `UIGridView::get_grid` re-allocated a throwaway Grid wrapper + weakref every call at 0% cache hit -- now holds one persistent strong ref (`get_grid` inclusive **72.6M -> 2.1M Ir, -97%**). **#342**: scalar-float animation fast path bypasses two per-frame `std::variant` visits (`Animation::update` **-18%**, variant machinery **473M -> 99M Ir**) -- the profiler *disproved* the guessed strcmp-cascade and weak_ptr-lock hypotheses and found the real cost. **#344**: memoized Key/InputState enum members instead of rebuilding via `EnumMeta.__call__` per event (enum-ctor **19.18M -> 0.228M Ir, -99%**; wall-clock -31%). **#343**: skip the per-frame `GetAttrString("update")` for non-subclassed scenes (**4,559 -> 3 Ir/frame** on the real `doFrame` loop; measuring it required bypassing headless `step()`, which doesn't call `updatePythonScenes()` -- filed as **#350**). Harness total instructions **3,522.9M -> 3,211.5M (-8.8%)**; suite 307/307 with new regression tests `issue_34{2,4,8}_*`. Durable sprint doc with A/B numbers at `docs/sprint-perf-342-348.md`. Spun out **#349** (hybrid declarative scene serialization, Major Feature, tier2) from the finding that `automation.screenshot()` PNG encode is ~96% of a render-profile's instructions.
|
||||
- **Tier1 memory-model batch resolved** -- all five pre-1.0 freeze decisions from the 2026-07-02 review are closed on master. **#326**: Color/Vector are value types forever (copies at every property boundary; write-through proxies rejected); **#328**: `with layer.edit() as view:` is the single bulk-edit convention for future buffer/numpy APIs (conservative invalidation on `__exit__`; unblocks #335); **#327**: threading contract documented -- `docs/threading-model.md` + normative `mcrfpy.lock()` docstring ("off-main-thread access outside the lock is undefined"); **#330**: subinterpreters explicitly excluded from 1.x compatibility; **#329**: `grid.entities` re-backed by `std::vector` -- indexing is O(1) (5000-entity indexed sweep: 49.7 ms -> 0.5 ms), iterator is index-based with the same mutation-guard semantics, suite 304/304. #326/#328/#330 are recorded in the new `docs/api-stability.md` compatibility policy, enforced by the api-surface snapshot test.
|
||||
- **#340 The Gauntlet** -- interactive on-screen stress benchmark (`tests/benchmarks/gauntlet/`). Six trials (ENTITY SWARM, ANIMATION STORM, GRID TITAN, PATHFINDER RUSH, UI AVALANCHE, SIGHTLINE SIEGE) ramp load geometrically until p95 frame time breaks the 16.67 ms budget; live HUD with frame-time sparkline; menu + results screens diff against a committed JSON baseline with letter grades and a geometric-mean GAUNTLET SCORE. First real baseline captured windowed on 0.2.8 (e.g. 10,555 entities, 450 pathfinding queries/tick, 4,123 UI elements at budget). Caveat: desktop noise made callback-heavy trials vary run-to-run -- recapture on a quiet system for a canonical baseline. Found #341 (get_metrics draw_calls/render counters read 0).
|
||||
|
||||
### Recently Shipped (June 2026)
|
||||
- **0.2.8 release** -- Version bumped to `0.2.8` (dropped the in-development `-7DRL-2026` suffix) and cut for distribution: `dist/McRogueFace-0.2.8-{Linux-full.tar.gz,Windows-full.zip,WASM.zip}`. Shipped libtcod (Linux `.so` / Windows `.dll` / Emscripten `.a`) rebuilt from submodule 79abc66 so release binaries carry the #321 FOV fix; the Linux build is `SDL3=OFF` with vendored utf8proc to stay self-contained (only libz + system libs), matching prior releases. Also fixed a pre-existing `tools/package.sh` bug that affected 0.2.7 too: the Linux package shipped the bare `libtcod.so` instead of its DT_NEEDED SONAME `libtcod.so.2` (+ compat symlink, mirroring the SFML packaging), so the runtime loader couldn't resolve it on a clean machine -- an absolute dev RUNPATH had masked this locally. No Gitea issue tracks this release (declined). Local commit `cf844f4`, pushed to origin/master; the `0.2.8` tag exists locally but has not been pushed yet.
|
||||
- **#321** (HIGH) -- Fixed the `ColorLayer.draw_fov` heap-buffer-overflow by bumping `libtcod-headless` to 79abc66, pulling in upstream FOV overflow fixes (root cause: off-by-one in `view_array_insert` overflowing `active_views` in `fov_permissive2.c`; the "bad-free in ~GridData" of the issue title is the downstream symptom). Verified by A/B replay of the #312 `fuzz_fov` crash corpus under clang-18 ASan -- pre-fix (8835239) inputs abort with the overflow inside `GridData::computeFOV`, post-fix (79abc66) all clean plus a 45s/21,952-run fuzz smoke. (The gitignored shipped `__lib/libtcod.so` needed rebuilding from this commit for release binaries to carry the fix -- done for 0.2.8, see above.)
|
||||
- **#322-#325** (fuzz-surfaced safety batch) -- the four remaining #312 fuzz bugs fixed and regression-tested. **#322**: `WangSet.terrain_enum` (and the parallel `AutoRuleSet.terrain_enum`) ignored Python C-API return codes, so an invalid-UTF-8 import name left an exception pending and the next `PyObject_Call` tripped a `_PyErr_Occurred` assertion/abort; every C-API return is now checked and the real exception propagated. **#323/#324/#325**: NaN/inf (or out-of-long-range) floats reached unchecked float->integer casts -- `pitch_shift` factor (`AudioEffects.cpp:27`, nan->unsigned long), `hsl_shift` shifts (`PyTexture.cpp:458`, nan->unsigned char), and `Vector.int` components (`PyVector.cpp:610`, inf->long), all undefined behavior; each now validates finiteness/range at the binding boundary and raises ValueError/OverflowError. Verified by A/B replay of the #312 crash corpus under clang-18 UBSan/ASan: all four inputs abort pre-fix at the exact cited lines and run clean post-fix, with four new regression tests (`tests/regression/issue_322..325_*`).
|
||||
- **#312** -- Fuzz coverage extended to the remaining public API surface. Four new libFuzzer targets (`fuzz_audio_dsp`, `fuzz_import_parsers`, `fuzz_texture_factory`, `fuzz_shader_bindings`) cover the Tier A/B gaps (external file parsers, audio DSP math, raw-byte texture ingestion, shader uniform-binding lifetime); Tier C surface (Line/Circle/Arc, `Scene.children` collections, `find`/`find_all`/`bresenham`/`lock`, grid spatial queries, GridPoint dynamic attrs, `Grid.find_path`+AStarPath, ColorLayer perspective/`draw_fov`, layer `apply_*`) folded into the five existing targets. Each new target is signature-validated against the live API and seeded from real fixtures. The campaign immediately found **five new bugs** -- filed #321-#325 (no fixes this round; targets only). A pre-existing infra fix rode along: `tools/build_debug_libs.sh` flag-quoting bug that broke instrumented debug-lib rebuilds. The benchmark triplet is deliberately excluded from fuzzing (`end_benchmark()` writes a file per call).
|
||||
- **#320** -- `Caption` constructor positional signature now matches its frozen docstring. The docstring advertised `Caption(pos, font, text, ...)` (parallel to `Sprite`/`Entity`, whose 2nd positional is the resource), but the implementation laid its two positional slots out as `(pos, text)` with `font` keyword-only, so `Caption((x,y), None, "text")` raised `TypeError`. Fixed `UICaption::init` to `(pos, font, text)` positional-or-keyword. Audited zero live callers of the old `(pos, text)` 2-positional form. Also added the matching read-only `Caption.font` getter (the class docstring listed `font` as an attribute but no getter existed; it now reflects the supplied or engine-default font). Also rewrote two stale unit tests (`test_animation_raii`, `test_animation_property_locking`) that called the removed `mcrfpy.Animation(...)` constructor to use `drawable.animate(...)` -- preserving the suite's only `conflict_mode` (#120) coverage and the weak-target RAII checks. Suite now 297/297.
|
||||
- **#317 / #318 / #319** -- The three code-level bugs surfaced by the #314 docstring-accuracy verify pass, fixed together. #317: `automation.scroll()` dropped the x of its position argument (the scroll delta now has its own `injectMouseEvent` parameter, so the real x/y is forwarded). #318: `GridView.texture` always returned `None` (a TODO stub) -- it now returns a `Texture` wrapper (and since `mcrfpy.Grid`/`mcrfpy.GridView` are one type post-#252, both names benefit). #319: `Entity.visible_entities(radius=None)` raised `TypeError` (the `i` format code rejects `None`) -- radius is now parsed as an object so `None`/omitted/`-1` mean "grid default". Regression tests for each; api-surface snapshot re-baselined and docs/stubs regenerated.
|
||||
- **#316** -- Sparse (windowed) perspective writeback in `UIEntity::updateVisibility`. The demote+promote passes are now clipped to an AABB sized to `fov_radius` (with a `prev_fov` window cache so a moving entity leaves no trailing "ghost vision"), replacing two full-`W*H` walks per entity. The Phase 5.2 benchmark's flat ~25-36 ms/entity writeback overhead on a 1000x1000 grid collapses to single-digit microseconds (384x-6577x on the cheap algorithms; lost in timing noise on the rest). Adversarial verify caught a regression the happy-path test missed -- externally-assigned maps (the documented `from_bytes` load/resume path) need a one-shot full demote (`perspective_full_demote_pending`) since `prev_fov` only bounds engine-promoted cells; fixed and locked with a 7-section regression test.
|
||||
- **#313** -- `UIEntity::grid` migrated from `shared_ptr<UIGrid>` to `shared_ptr<GridData>` (post-#252 refactor cleanup), adding a new public `entity.texture` read/write property. Merged to master.
|
||||
- **#314** -- API audit follow-through complete. (1) Snapshot lock: a public API-surface regression test (`tests/unit/api_surface_snapshot_test.py`) enshrines the frozen contract. (2) **F15**: all 289 raw docstring slots across the 20 frozen binding files converted to `MCRF_*` macros (frozen surface 100% compliant), driven by two one-agent-per-file workflows with build/doc gates and an adversarial signature-accuracy verify pass. Property types now resolve to real types (not `Any`) and read-only flags are correct. (3) A strict frozen-docstring gate (`tools/check_frozen_docstrings.sh`, wired into `generate_all_docs.sh`) locks it against regression. Breaking-change findings (F1/F4/F6/F11/F13) closed earlier; F7/F8/F10 deferred as non-1.0. Code-level bugs surfaced by the verify pass filed as #317/#318/#319.
|
||||
|
||||
### Active Follow-Ups
|
||||
- The memory-model review (#326-#338) is nearly closed out: the five tier1 freeze decisions (#326-#330), #331 (hot-getter fast path), #332 (GridData SoA), #333 (lazy TCOD map rebuild), #334 (DiscreteMap buffer protocol), and #335 (numpy layer views) are all **resolved** on master. Still open: **#338** (per-instance memory diet -- shipped as a safe subset, the `render_sprite` relocation deferred), **#336** (free-threading hardening), **#337** (numpy availability strategy), and **#352** (perspective-grid render early-out).
|
||||
- The grid type-split bugs and the profiling/Gauntlet testability gaps are now **closed**: #369 (`.parent` identity churn), #372 (rotted `tests/demo/`, superseded by #374), #356 (doc/stub generators miss module-level dynamic attributes), #341 (get_metrics render counters read 0), and #350 (headless `step()` bypasses `updatePythonScenes()`).
|
||||
- #349 (declarative scene serialization) is an open tier2 design proposal.
|
||||
- Gauntlet baseline should be recaptured on a quiet system (`tests/benchmarks/gauntlet/run_gauntlet.py`) -- the committed first baseline is real but desktop-noisy for the callback-heavy trials. (#341, render counters read 0 in get_metrics, which blocked richer per-subsystem HUD attribution, is now closed.)
|
||||
- 0.2.8 is released and published: commit `cf844f4` and the `0.2.8` tag are both pushed, and the distribution artifacts are uploaded. Master has since advanced past `36e74e0` (grid input revival + type split) through the July perf/grid sprints and the docs-as-tests thread; the `closes #...` commit trailers take their issues down on merge.
|
||||
|
||||
### Other Post-7DRL Priorities
|
||||
- Progress on the r/roguelikedev tutorial series (#167)
|
||||
- Better pip/virtualenv integration for adding packages to McRogueFace's embedded interpreter
|
||||
|
||||
---
|
||||
|
||||
## Engine Eras
|
||||
|
||||
One engine, accumulating capabilities. Nothing is thrown away.
|
||||
|
||||
| Era | Focus | Status |
|
||||
|-----|-------|--------|
|
||||
| **McRogueFace** | 2D tiles, roguelike systems, procgen | Active -- approaching 1.0 |
|
||||
| **McVectorFace** | Sparse grids, vector graphics, physics | Planned |
|
||||
| **McVoxelFace** | Voxel terrain, 3D gameplay | Proof-of-concept complete |
|
||||
|
||||
---
|
||||
|
||||
## 3D/Voxel Pipeline (Experimental)
|
||||
|
||||
The 3D pipeline is proof-of-concept scouting for the McVoxelFace era. It works and is tested but is explicitly **not** part of the 1.0 API freeze.
|
||||
|
||||
**What exists**: Viewport3D, Camera3D, Entity3D, MeshLayer, Model3D (glTF), Billboard, Shader3D, VoxelGrid with greedy meshing, face culling, RLE serialization, and navigation projection.
|
||||
|
||||
**Known gaps**: Some Entity3D collection methods, animation stubs, shader pipeline incomplete.
|
||||
|
||||
**Maturity track**: These modules will mature on their own timeline, driven by games that need 3D. They won't block 2D stability.
|
||||
|
||||
---
|
||||
|
||||
## Future Directions
|
||||
|
||||
These are ideas on the horizon -- not yet concrete enough for issues, but worth capturing.
|
||||
|
||||
### McRogueFace Lite
|
||||
A spiritual port to MicroPython targeting the PicoCalc and other microcontrollers. Could provide a migration path to retro ROMs or compete in the Pico-8 space. The core idea: strip McRogueFace down to its essential tile/entity/scene model and run it on constrained hardware.
|
||||
|
||||
### McVectorFace Era
|
||||
The next major capability expansion. Sparse grid layers, a polygon/shape rendering class, and eventually physics integration. This would support games that aren't purely tile-based -- top-down action, strategy maps with irregular regions, or hybrid tile+vector visuals. See the [Strategic Direction](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Strategic-Direction) wiki for the full era model.
|
||||
|
||||
### McRogueFace Standard Library
|
||||
A built-in collection of reusable GUI widgets and game UI patterns: menus, dialogs, inventory screens, stat bars, text input fields, scrollable lists. These would ship with the engine as importable Python modules, saving every game from reimplementing the same UI primitives. Think of it as `mcrfpy.widgets` -- batteries included.
|
||||
|
||||
### Pip/Virtualenv Integration
|
||||
Rather than inverting the architecture to make McRogueFace a pip-installable package, the nearer-term goal is better integration in the other direction: making it easy to install and use third-party Python packages within McRogueFace's embedded interpreter. This could mean virtualenv awareness, a `mcrf install` command, or bundling pip itself.
|
||||
|
||||
---
|
||||
|
||||
## Open Issues by Area
|
||||
|
||||
26 open issues across the tracker. Key groupings:
|
||||
|
||||
- **Docs-as-tests fallout** (#380, #374) -- shipped `templates/*/` example programs are gated by nothing; 19 orphan scripts under `tests/demo/screens/` need adoption or retirement. Both spun out of the July snippets-as-tests thread (the docs site's code samples are now in the gated suite). The earlier bug batch (#369 `.parent` identity, #372 rotted `tests/demo/`, #356 doc generators miss module attrs, #341 get_metrics counters, #350 headless `step()` bypass) is **closed**.
|
||||
- **Memory-model review, July 2026** (#336, #337, #338 remaining; #326-#335 resolved) -- per-instance memory diet, free-threading hardening, numpy availability strategy
|
||||
- **Grid / rendering** (#352, #152, #67, #124, #107, #347) -- perspective-grid render early-out; sparse layers; infinite worlds; grid point animation; particle system; SFML vs SDL2/WebGL renderer parity
|
||||
- **Design proposals** (#349) -- hybrid declarative scene serialization (test oracle + save/load)
|
||||
- **Demos / tutorials** (#167, #248, #154, #156, #55) -- r/roguelikedev series, Crypt of Sokoban remaster, LLM agent simulations
|
||||
- **Platform/distribution** (#70, #54, #62, #53) -- Packaging without the embedded interpreter, Jupyter, multiple windows, input methods
|
||||
- **WASM tooling** (#239, #346) -- Automated browser testing; web-build profiling docs (sibling to #345)
|
||||
- **Deferred** (#220, #46, #45) -- Subinterpreter support / tests, accessibility modes
|
||||
|
||||
See the [Forgejo issue tracker](https://dev.ffwf.net/forgejo/john/McRogueFace/issues) for current status.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
- **Issue Tracker**: [Forgejo Issues](https://dev.ffwf.net/forgejo/john/McRogueFace/issues)
|
||||
- **Wiki**: [Strategic Direction](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Strategic-Direction), [Issue Roadmap](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Issue-Roadmap), [Development Workflow](https://dev.ffwf.net/forgejo/john/McRogueFace/wiki/Development-Workflow)
|
||||
- **Build Guide**: See `CLAUDE.md` for build instructions
|
||||
- **Tutorial**: `roguelike_tutorial/` for implementation examples
|
||||
16
_test.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import mcrfpy
|
||||
|
||||
# Create a new scene
|
||||
mcrfpy.createScene("intro")
|
||||
|
||||
# Add a text caption
|
||||
caption = mcrfpy.Caption((50, 50), "Welcome to McRogueFace!")
|
||||
caption.size = 48
|
||||
caption.fill_color = (255, 255, 255)
|
||||
|
||||
# Add to scene
|
||||
mcrfpy.sceneUI("intro").append(caption)
|
||||
|
||||
# Switch to the scene
|
||||
mcrfpy.setScene("intro")
|
||||
|
||||
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 |
54
deps/platform/linux/platform.h
vendored
|
|
@ -1,54 +1,6 @@
|
|||
#ifndef __PLATFORM
|
||||
#define __PLATFORM
|
||||
#define __PLATFORM_SET_PYTHON_SEARCH_PATHS 1
|
||||
|
||||
#ifdef __EMSCRIPTEN__
|
||||
// WASM/Emscripten platform - no /proc filesystem, limited std::filesystem support
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
// In WASM, the executable is at the root of the virtual filesystem
|
||||
return L"/";
|
||||
}
|
||||
|
||||
std::wstring executable_filename()
|
||||
{
|
||||
// In WASM, we use a fixed executable name
|
||||
return L"/mcrogueface";
|
||||
}
|
||||
|
||||
std::wstring working_path()
|
||||
{
|
||||
// In WASM, working directory is root of virtual filesystem
|
||||
return L"/";
|
||||
}
|
||||
|
||||
std::string narrow_string(std::wstring convertme)
|
||||
{
|
||||
// Simple conversion for ASCII/UTF-8 compatible strings
|
||||
std::string result;
|
||||
result.reserve(convertme.size());
|
||||
for (wchar_t wc : convertme) {
|
||||
if (wc < 128) {
|
||||
result.push_back(static_cast<char>(wc));
|
||||
} else {
|
||||
// For non-ASCII, use a simple UTF-8 encoding
|
||||
if (wc < 0x800) {
|
||||
result.push_back(static_cast<char>(0xC0 | (wc >> 6)));
|
||||
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
|
||||
} else {
|
||||
result.push_back(static_cast<char>(0xE0 | (wc >> 12)));
|
||||
result.push_back(static_cast<char>(0x80 | ((wc >> 6) & 0x3F)));
|
||||
result.push_back(static_cast<char>(0x80 | (wc & 0x3F)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#else
|
||||
// Native Linux platform
|
||||
|
||||
std::wstring executable_path()
|
||||
{
|
||||
/*
|
||||
|
|
@ -60,7 +12,7 @@ std::wstring executable_path()
|
|||
return exec_path.wstring();
|
||||
//size_t path_index = exec_path.find_last_of('/');
|
||||
//return exec_path.substr(0, path_index);
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::wstring executable_filename()
|
||||
|
|
@ -85,6 +37,4 @@ std::string narrow_string(std::wstring convertme)
|
|||
return converter.to_bytes(convertme);
|
||||
}
|
||||
|
||||
#endif // __EMSCRIPTEN__
|
||||
|
||||
#endif // __PLATFORM
|
||||
#endif
|
||||
|
|
|
|||
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,89 +0,0 @@
|
|||
"""HeightMap Hills and Craters Demo
|
||||
|
||||
Demonstrates: add_hill, dig_hill
|
||||
Creates volcanic terrain with mountains and craters using ColorLayer visualization.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
# Full screen grid: 60x48 tiles at 16x16 = 960x768
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def height_to_color(h):
|
||||
"""Convert height value to terrain color."""
|
||||
if h < 0.1:
|
||||
return mcrfpy.Color(20, 40, int(80 + h * 400))
|
||||
elif h < 0.3:
|
||||
t = (h - 0.1) / 0.2
|
||||
return mcrfpy.Color(int(40 + t * 30), int(60 + t * 40), 30)
|
||||
elif h < 0.5:
|
||||
t = (h - 0.3) / 0.2
|
||||
return mcrfpy.Color(int(70 - t * 20), int(100 + t * 50), int(30 + t * 20))
|
||||
elif h < 0.7:
|
||||
t = (h - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(120 + t * 40), int(100 + t * 30), int(60 + t * 20))
|
||||
elif h < 0.85:
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(140 + t * 40), int(130 + t * 40), int(120 + t * 40))
|
||||
else:
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(180 + t * 75), int(180 + t * 75), int(180 + t * 75))
|
||||
|
||||
# Setup scene
|
||||
scene = mcrfpy.Scene("hills_demo")
|
||||
|
||||
# Create grid with color layer
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
# Create heightmap
|
||||
hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
|
||||
|
||||
# Add volcanic mountains - large hills
|
||||
hmap.add_hill((15, 24), 18.0, 0.6) # Central volcano base
|
||||
hmap.add_hill((15, 24), 10.0, 0.3) # Volcano peak
|
||||
hmap.add_hill((45, 15), 12.0, 0.5) # Eastern mountain
|
||||
hmap.add_hill((35, 38), 14.0, 0.45) # Southern mountain
|
||||
hmap.add_hill((8, 10), 8.0, 0.35) # Small northern hill
|
||||
|
||||
# Create craters using dig_hill
|
||||
hmap.dig_hill((15, 24), 5.0, 0.1) # Volcanic crater
|
||||
hmap.dig_hill((45, 15), 4.0, 0.25) # Eastern crater
|
||||
hmap.dig_hill((25, 30), 6.0, 0.05) # Impact crater (deep)
|
||||
hmap.dig_hill((50, 40), 3.0, 0.2) # Small crater
|
||||
|
||||
# Add some smaller features for variety
|
||||
for i in range(8):
|
||||
x = 5 + (i * 7) % 55
|
||||
y = 5 + (i * 11) % 40
|
||||
hmap.add_hill((x, y), float(3 + (i % 4)), 0.15)
|
||||
|
||||
# Normalize to use full color range
|
||||
hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Apply heightmap to color layer
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set((x, y), height_to_color(h))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="HeightMap: add_hill + dig_hill (volcanic terrain)", pos=(10, 10))
|
||||
title.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
title.outline = 1
|
||||
title.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(title)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Take screenshot directly (works in headless mode)
|
||||
automation.screenshot("procgen_01_heightmap_hills.png")
|
||||
print("Screenshot saved: procgen_01_heightmap_hills.png")
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
"""HeightMap Noise Integration Demo
|
||||
|
||||
Demonstrates: add_noise, multiply_noise with NoiseSource
|
||||
Shows terrain generation using different noise modes (flat, fbm, turbulence).
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_color(h):
|
||||
"""Height-based terrain coloring."""
|
||||
if h < 0.25:
|
||||
# Water - deep to shallow blue
|
||||
t = h / 0.25
|
||||
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 60), int(120 + t * 80))
|
||||
elif h < 0.35:
|
||||
# Beach/sand
|
||||
t = (h - 0.25) / 0.1
|
||||
return mcrfpy.Color(int(180 + t * 40), int(160 + t * 30), int(100 + t * 20))
|
||||
elif h < 0.6:
|
||||
# Grass - varies with height
|
||||
t = (h - 0.35) / 0.25
|
||||
return mcrfpy.Color(int(50 + t * 30), int(120 + t * 40), int(40 + t * 20))
|
||||
elif h < 0.75:
|
||||
# Forest/hills
|
||||
t = (h - 0.6) / 0.15
|
||||
return mcrfpy.Color(int(40 - t * 10), int(80 + t * 20), int(30 + t * 10))
|
||||
elif h < 0.88:
|
||||
# Rock/mountain
|
||||
t = (h - 0.75) / 0.13
|
||||
return mcrfpy.Color(int(100 + t * 40), int(90 + t * 40), int(80 + t * 40))
|
||||
else:
|
||||
# Snow peaks
|
||||
t = (h - 0.88) / 0.12
|
||||
return mcrfpy.Color(int(200 + t * 55), int(200 + t * 55), int(210 + t * 45))
|
||||
|
||||
def apply_to_layer(hmap, layer):
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = hmap.get((x, y))
|
||||
layer.set(x, y, terrain_color(h))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create three panels showing different noise modes
|
||||
panel_width = GRID_WIDTH // 3
|
||||
right_panel_width = GRID_WIDTH - 2 * panel_width # Handle non-divisible widths
|
||||
|
||||
# Create noise source with consistent seed
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm='simplex',
|
||||
hurst=0.5,
|
||||
lacunarity=2.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Left panel: Flat noise (single octave, raw)
|
||||
left_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
|
||||
left_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='flat', octaves=1)
|
||||
left_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Middle panel: FBM noise (fractal brownian motion - natural terrain)
|
||||
mid_hmap = mcrfpy.HeightMap((panel_width, GRID_HEIGHT), fill=0.0)
|
||||
mid_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='fbm', octaves=6)
|
||||
mid_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Right panel: Turbulence (absolute value - clouds, marble)
|
||||
right_hmap = mcrfpy.HeightMap((right_panel_width, GRID_HEIGHT), fill=0.0)
|
||||
right_hmap.add_noise(noise, world_origin=(0, 0), world_size=(20, 20), mode='turbulence', octaves=6)
|
||||
right_hmap.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to color layer with panel divisions
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if x < panel_width:
|
||||
h = left_hmap.get((x, y))
|
||||
elif x < panel_width * 2:
|
||||
h = mid_hmap.get((x - panel_width, y))
|
||||
else:
|
||||
h = right_hmap.get((x - panel_width * 2, y))
|
||||
color_layer.set(((x, y)), terrain_color(h))
|
||||
|
||||
# Add divider lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_width - 1, y)), mcrfpy.Color(255, 255, 255, 100))
|
||||
color_layer.set(((panel_width * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 100))
|
||||
|
||||
|
||||
# Setup scene
|
||||
scene = mcrfpy.Scene("noise_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
# Labels for each panel
|
||||
labels = [
|
||||
("FLAT (raw)", 10),
|
||||
("FBM (terrain)", GRID_WIDTH * CELL_SIZE // 3 + 10),
|
||||
("TURBULENCE (clouds)", GRID_WIDTH * CELL_SIZE * 2 // 3 + 10)
|
||||
]
|
||||
for text, x in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, 10))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_02_heightmap_noise.png")
|
||||
print("Screenshot saved: procgen_02_heightmap_noise.png")
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""HeightMap Combination Operations Demo
|
||||
|
||||
Demonstrates: add, subtract, multiply, min, max, lerp, copy_from
|
||||
Shows how heightmaps can be combined for complex terrain effects.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_color(h):
|
||||
"""Simple grayscale with color tinting for visibility."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
# Blue-white-red gradient for clear visualization
|
||||
if h < 0.5:
|
||||
t = h / 0.5
|
||||
return mcrfpy.Color(int(50 * t), int(100 * t), int(200 - 100 * t))
|
||||
else:
|
||||
t = (h - 0.5) / 0.5
|
||||
return mcrfpy.Color(int(50 + 200 * t), int(100 + 100 * t), int(100 - 50 * t))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create 6 panels (2 rows x 3 columns)
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Create two base heightmaps for operations
|
||||
noise1 = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
noise2 = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
|
||||
|
||||
base1 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base1.add_noise(noise1, world_size=(10, 10), mode='fbm', octaves=4)
|
||||
base1.normalize(0.0, 1.0)
|
||||
|
||||
base2 = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base2.add_noise(noise2, world_size=(10, 10), mode='fbm', octaves=4)
|
||||
base2.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 1: ADD operation (combined terrain)
|
||||
add_result = base1.copy_from(base1) # Actually need to create new
|
||||
add_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
add_result.copy_from(base1).add(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: SUBTRACT operation (carving)
|
||||
sub_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
sub_result.copy_from(base1).subtract(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: MULTIPLY operation (masking)
|
||||
mul_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
mul_result.copy_from(base1).multiply(base2).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: MIN operation (valleys)
|
||||
min_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
min_result.copy_from(base1).min(base2)
|
||||
|
||||
# Panel 5: MAX operation (ridges)
|
||||
max_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
max_result.copy_from(base1).max(base2)
|
||||
|
||||
# Panel 6: LERP operation (blending)
|
||||
lerp_result = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
lerp_result.copy_from(base1).lerp(base2, 0.5)
|
||||
|
||||
# Apply panels to grid
|
||||
panels = [
|
||||
(add_result, 0, 0, "ADD"),
|
||||
(sub_result, panel_w, 0, "SUBTRACT"),
|
||||
(mul_result, panel_w * 2, 0, "MULTIPLY"),
|
||||
(min_result, 0, panel_h, "MIN"),
|
||||
(max_result, panel_w, panel_h, "MAX"),
|
||||
(lerp_result, panel_w * 2, panel_h, "LERP(0.5)"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_color(h))
|
||||
|
||||
# Add label
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Draw grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(255, 255, 255, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(255, 255, 255, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(255, 255, 255, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("operations_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_03_heightmap_operations.png")
|
||||
print("Screenshot saved: procgen_03_heightmap_operations.png")
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
"""HeightMap Transform Operations Demo
|
||||
|
||||
Demonstrates: scale, clamp, normalize, smooth, kernel_transform
|
||||
Shows value manipulation and convolution effects.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_color(h):
|
||||
"""Grayscale with enhanced contrast."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
v = int(h * 255)
|
||||
return mcrfpy.Color(v, v, v)
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create 6 panels showing different transforms
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Source noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
# Create base terrain with features
|
||||
base = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
base.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=4)
|
||||
base.add_hill((panel_w // 2, panel_h // 2), 8, 0.5)
|
||||
base.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 1: Original
|
||||
original = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
original.copy_from(base)
|
||||
|
||||
# Panel 2: SCALE (amplify contrast)
|
||||
scaled = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
scaled.copy_from(base).add_constant(-0.5).scale(2.0).clamp(0.0, 1.0)
|
||||
|
||||
# Panel 3: CLAMP (plateau effect)
|
||||
clamped = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
clamped.copy_from(base).clamp(0.3, 0.7).normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: SMOOTH (blur/average)
|
||||
smoothed = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
smoothed.copy_from(base).smooth(3)
|
||||
|
||||
# Panel 5: SHARPEN kernel
|
||||
sharpened = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
sharpened.copy_from(base)
|
||||
sharpen_kernel = {
|
||||
(0, -1): -1.0, (-1, 0): -1.0, (0, 0): 5.0, (1, 0): -1.0, (0, 1): -1.0
|
||||
}
|
||||
sharpened.kernel_transform(sharpen_kernel).clamp(0.0, 1.0)
|
||||
|
||||
# Panel 6: EDGE DETECTION kernel
|
||||
edges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
edges.copy_from(base)
|
||||
edge_kernel = {
|
||||
(-1, -1): -1, (0, -1): -1, (1, -1): -1,
|
||||
(-1, 0): -1, (0, 0): 8, (1, 0): -1,
|
||||
(-1, 1): -1, (0, 1): -1, (1, 1): -1,
|
||||
}
|
||||
edges.kernel_transform(edge_kernel).normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(original, 0, 0, "ORIGINAL"),
|
||||
(scaled, panel_w, 0, "SCALE (contrast)"),
|
||||
(clamped, panel_w * 2, 0, "CLAMP (plateau)"),
|
||||
(smoothed, 0, panel_h, "SMOOTH (blur)"),
|
||||
(sharpened, panel_w, panel_h, "SHARPEN kernel"),
|
||||
(edges, panel_w * 2, panel_h, "EDGE DETECT"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), value_to_color(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("transforms_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_04_heightmap_transforms.png")
|
||||
print("Screenshot saved: procgen_04_heightmap_transforms.png")
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
"""HeightMap Erosion and Terrain Generation Demo
|
||||
|
||||
Demonstrates: rain_erosion, mid_point_displacement, smooth
|
||||
Shows natural terrain formation through erosion simulation.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_color(h):
|
||||
"""Natural terrain coloring."""
|
||||
if h < 0.2:
|
||||
# Deep water
|
||||
t = h / 0.2
|
||||
return mcrfpy.Color(int(20 + t * 30), int(40 + t * 40), int(100 + t * 55))
|
||||
elif h < 0.3:
|
||||
# Shallow water
|
||||
t = (h - 0.2) / 0.1
|
||||
return mcrfpy.Color(int(50 + t * 50), int(80 + t * 60), int(155 + t * 40))
|
||||
elif h < 0.35:
|
||||
# Beach
|
||||
t = (h - 0.3) / 0.05
|
||||
return mcrfpy.Color(int(194 - t * 30), int(178 - t * 30), int(128 - t * 20))
|
||||
elif h < 0.55:
|
||||
# Lowland grass
|
||||
t = (h - 0.35) / 0.2
|
||||
return mcrfpy.Color(int(80 + t * 20), int(140 - t * 30), int(60 + t * 10))
|
||||
elif h < 0.7:
|
||||
# Highland grass/forest
|
||||
t = (h - 0.55) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 30), int(100 + t * 10), int(40 + t * 20))
|
||||
elif h < 0.85:
|
||||
# Rock
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 30), int(85 + t * 35))
|
||||
else:
|
||||
# Snow
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(180 + t * 75), int(185 + t * 70), int(190 + t * 65))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Panel 1: Mid-point displacement (raw)
|
||||
mpd_raw = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_raw.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_raw.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: Mid-point displacement + smoothing
|
||||
mpd_smooth = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_smooth.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_smooth.smooth(2)
|
||||
mpd_smooth.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: Mid-point + light erosion
|
||||
mpd_light_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
mpd_light_erode.mid_point_displacement(roughness=0.6, seed=42)
|
||||
mpd_light_erode.rain_erosion(drops=1000, erosion=0.05, sedimentation=0.03, seed=42)
|
||||
mpd_light_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: Noise-based + moderate erosion
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
|
||||
noise_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
noise_erode.add_noise(noise, world_size=(12, 12), mode='fbm', octaves=5)
|
||||
noise_erode.add_hill((panel_w // 2, panel_h // 2), 10, 0.4)
|
||||
noise_erode.rain_erosion(drops=3000, erosion=0.1, sedimentation=0.05, seed=42)
|
||||
noise_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 5: Heavy erosion (river valleys)
|
||||
heavy_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
heavy_erode.mid_point_displacement(roughness=0.7, seed=99)
|
||||
heavy_erode.rain_erosion(drops=8000, erosion=0.15, sedimentation=0.02, seed=42)
|
||||
heavy_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 6: Extreme erosion (canyon-like)
|
||||
extreme_erode = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
extreme_erode.mid_point_displacement(roughness=0.5, seed=77)
|
||||
extreme_erode.rain_erosion(drops=15000, erosion=0.2, sedimentation=0.01, seed=42)
|
||||
extreme_erode.smooth(1)
|
||||
extreme_erode.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(mpd_raw, 0, 0, "MPD Raw"),
|
||||
(mpd_smooth, panel_w, 0, "MPD + Smooth"),
|
||||
(mpd_light_erode, panel_w * 2, 0, "Light Erosion"),
|
||||
(noise_erode, 0, panel_h, "Noise + Erosion"),
|
||||
(heavy_erode, panel_w, panel_h, "Heavy Erosion"),
|
||||
(extreme_erode, panel_w * 2, panel_h, "Extreme Erosion"),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), terrain_color(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("erosion_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_05_heightmap_erosion.png")
|
||||
print("Screenshot saved: procgen_05_heightmap_erosion.png")
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
"""HeightMap Voronoi Demo
|
||||
|
||||
Demonstrates: add_voronoi with different coefficients
|
||||
Shows cell-based patterns useful for biomes, regions, and organic structures.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def biome_color(h):
|
||||
"""Color cells as distinct biomes."""
|
||||
# Use value ranges to create distinct regions
|
||||
h = max(0.0, min(1.0, h))
|
||||
|
||||
if h < 0.15:
|
||||
return mcrfpy.Color(30, 60, 120) # Deep water
|
||||
elif h < 0.25:
|
||||
return mcrfpy.Color(50, 100, 180) # Shallow water
|
||||
elif h < 0.35:
|
||||
return mcrfpy.Color(194, 178, 128) # Beach/desert
|
||||
elif h < 0.5:
|
||||
return mcrfpy.Color(80, 160, 60) # Grassland
|
||||
elif h < 0.65:
|
||||
return mcrfpy.Color(40, 100, 40) # Forest
|
||||
elif h < 0.8:
|
||||
return mcrfpy.Color(100, 80, 60) # Hills
|
||||
elif h < 0.9:
|
||||
return mcrfpy.Color(130, 130, 130) # Mountains
|
||||
else:
|
||||
return mcrfpy.Color(240, 240, 250) # Snow
|
||||
|
||||
def cell_edges_color(h):
|
||||
"""Highlight cell boundaries."""
|
||||
h = max(0.0, min(1.0, h))
|
||||
if h < 0.3:
|
||||
return mcrfpy.Color(40, 40, 60)
|
||||
elif h < 0.6:
|
||||
return mcrfpy.Color(80, 80, 100)
|
||||
else:
|
||||
return mcrfpy.Color(200, 200, 220)
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Panel 1: Standard Voronoi (cell centers high)
|
||||
# coefficients (1, 0) = distance to nearest point
|
||||
v_standard = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_standard.add_voronoi(num_points=15, coefficients=(1.0, 0.0), seed=42)
|
||||
v_standard.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 2: Inverted (cell centers low, edges high)
|
||||
# coefficients (-1, 0) = inverted distance
|
||||
v_inverted = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_inverted.add_voronoi(num_points=15, coefficients=(-1.0, 0.0), seed=42)
|
||||
v_inverted.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 3: Cell difference (creates ridges)
|
||||
# coefficients (1, -1) = distance to nearest - distance to second nearest
|
||||
v_ridges = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_ridges.add_voronoi(num_points=15, coefficients=(1.0, -1.0), seed=42)
|
||||
v_ridges.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 4: Few large cells (biome-scale)
|
||||
v_biomes = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_biomes.add_voronoi(num_points=6, coefficients=(1.0, -0.3), seed=99)
|
||||
v_biomes.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 5: Many small cells (texture-scale)
|
||||
v_texture = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_texture.add_voronoi(num_points=50, coefficients=(1.0, -0.5), seed=77)
|
||||
v_texture.normalize(0.0, 1.0)
|
||||
|
||||
# Panel 6: Voronoi + noise blend (natural regions)
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
v_natural = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
v_natural.add_voronoi(num_points=12, coefficients=(0.8, -0.4), seed=42)
|
||||
v_natural.add_noise(noise, world_size=(15, 15), mode='fbm', octaves=3, scale=0.3)
|
||||
v_natural.normalize(0.0, 1.0)
|
||||
|
||||
# Apply to grid
|
||||
panels = [
|
||||
(v_standard, 0, 0, "Standard (1,0)", biome_color),
|
||||
(v_inverted, panel_w, 0, "Inverted (-1,0)", biome_color),
|
||||
(v_ridges, panel_w * 2, 0, "Ridges (1,-1)", cell_edges_color),
|
||||
(v_biomes, 0, panel_h, "Biomes (6 pts)", biome_color),
|
||||
(v_texture, panel_w, panel_h, "Texture (50 pts)", cell_edges_color),
|
||||
(v_natural, panel_w * 2, panel_h, "Voronoi + Noise", biome_color),
|
||||
]
|
||||
|
||||
for hmap, ox, oy, name, color_func in panels:
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = hmap.get((x, y))
|
||||
color_layer.set(((ox + x, oy + y)), color_func(h))
|
||||
|
||||
label = mcrfpy.Caption(text=name, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 5))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(60, 60, 60))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(60, 60, 60))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("voronoi_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_06_heightmap_voronoi.png")
|
||||
print("Screenshot saved: procgen_06_heightmap_voronoi.png")
|
||||
|
|
@ -1,158 +0,0 @@
|
|||
"""HeightMap Bezier Curves Demo
|
||||
|
||||
Demonstrates: dig_bezier for rivers, roads, and paths
|
||||
Shows path carving with variable width and depth.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import math
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def terrain_with_water(h):
|
||||
"""Terrain coloring with water in low areas."""
|
||||
if h < 0.15:
|
||||
# Water (carved paths)
|
||||
t = h / 0.15
|
||||
return mcrfpy.Color(int(30 + t * 30), int(60 + t * 50), int(140 + t * 40))
|
||||
elif h < 0.25:
|
||||
# Shore/wet ground
|
||||
t = (h - 0.15) / 0.1
|
||||
return mcrfpy.Color(int(80 + t * 40), int(100 + t * 30), int(80 - t * 20))
|
||||
elif h < 0.5:
|
||||
# Lowland
|
||||
t = (h - 0.25) / 0.25
|
||||
return mcrfpy.Color(int(70 + t * 20), int(130 + t * 20), int(50 + t * 10))
|
||||
elif h < 0.7:
|
||||
# Highland
|
||||
t = (h - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(60 + t * 30), int(110 - t * 20), int(45 + t * 15))
|
||||
elif h < 0.85:
|
||||
# Hills
|
||||
t = (h - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(95 + t * 25), int(70 + t * 30))
|
||||
else:
|
||||
# Peaks
|
||||
t = (h - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(150 + t * 60), int(150 + t * 60), int(155 + t * 60))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 2
|
||||
panel_h = GRID_HEIGHT
|
||||
|
||||
# Left panel: River system
|
||||
river_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
|
||||
# Add terrain
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
river_map.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=4, scale=0.3)
|
||||
river_map.add_hill((panel_w // 2, 5), 12, 0.3) # Mountain source
|
||||
river_map.normalize(0.3, 0.9)
|
||||
|
||||
# Main river - wide, flowing from top to bottom
|
||||
river_map.dig_bezier(
|
||||
points=((panel_w // 2, 2), (panel_w // 4, 15), (panel_w * 3 // 4, 30), (panel_w // 2, panel_h - 3)),
|
||||
start_radius=2, end_radius=5,
|
||||
start_height=0.1, end_height=0.05
|
||||
)
|
||||
|
||||
# Tributary from left
|
||||
river_map.dig_bezier(
|
||||
points=((3, 20), (10, 18), (15, 22), (panel_w // 3, 20)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.12, end_height=0.1
|
||||
)
|
||||
|
||||
# Tributary from right
|
||||
river_map.dig_bezier(
|
||||
points=((panel_w - 3, 15), (panel_w - 8, 20), (panel_w - 12, 18), (panel_w * 2 // 3, 25)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.12, end_height=0.1
|
||||
)
|
||||
|
||||
# Right panel: Road network
|
||||
road_map = mcrfpy.HeightMap((panel_w, panel_h), fill=0.5)
|
||||
road_map.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3, scale=0.2)
|
||||
road_map.normalize(0.35, 0.7)
|
||||
|
||||
# Main road - relatively straight
|
||||
road_map.dig_bezier(
|
||||
points=((5, panel_h // 2), (15, panel_h // 2 - 3), (panel_w - 15, panel_h // 2 + 3), (panel_w - 5, panel_h // 2)),
|
||||
start_radius=2, end_radius=2,
|
||||
start_height=0.25, end_height=0.25
|
||||
)
|
||||
|
||||
# North-south crossing road
|
||||
road_map.dig_bezier(
|
||||
points=((panel_w // 2, 5), (panel_w // 2 + 5, 15), (panel_w // 2 - 5, 35), (panel_w // 2, panel_h - 5)),
|
||||
start_radius=2, end_radius=2,
|
||||
start_height=0.25, end_height=0.25
|
||||
)
|
||||
|
||||
# Winding mountain path
|
||||
road_map.dig_bezier(
|
||||
points=((5, 8), (15, 5), (20, 15), (25, 10)),
|
||||
start_radius=1, end_radius=1,
|
||||
start_height=0.28, end_height=0.28
|
||||
)
|
||||
|
||||
# Curved path to settlement
|
||||
road_map.dig_bezier(
|
||||
points=((panel_w - 5, panel_h - 8), (panel_w - 15, panel_h - 5), (panel_w - 10, panel_h - 15), (panel_w // 2 + 5, panel_h - 10)),
|
||||
start_radius=1, end_radius=2,
|
||||
start_height=0.27, end_height=0.26
|
||||
)
|
||||
|
||||
# Apply to grid
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
# Left panel: rivers
|
||||
h = river_map.get((x, y))
|
||||
color_layer.set(((x, y)), terrain_with_water(h))
|
||||
|
||||
# Right panel: roads (use brown for roads)
|
||||
h2 = road_map.get((x, y))
|
||||
if h2 < 0.3:
|
||||
# Road surface
|
||||
t = h2 / 0.3
|
||||
color = mcrfpy.Color(int(140 - t * 40), int(120 - t * 30), int(80 - t * 20))
|
||||
else:
|
||||
color = terrain_with_water(h2)
|
||||
color_layer.set(((panel_w + x, y)), color)
|
||||
|
||||
# Divider
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
# Labels
|
||||
labels = [("Rivers (dig_bezier)", 10, 10), ("Roads & Paths", panel_w * CELL_SIZE + 10, 10)]
|
||||
for text, x, ypos in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, ypos))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bezier_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_07_heightmap_bezier.png")
|
||||
print("Screenshot saved: procgen_07_heightmap_bezier.png")
|
||||
|
|
@ -1,148 +0,0 @@
|
|||
"""HeightMap Thresholds and ColorLayer Integration Demo
|
||||
|
||||
Demonstrates: threshold, threshold_binary, inverse, count_in_range
|
||||
Also: ColorLayer.apply_ranges for multi-threshold coloring
|
||||
Shows terrain classification and visualization techniques.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
# Create source terrain
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
source = mcrfpy.HeightMap((panel_w, panel_h), fill=0.0)
|
||||
source.add_noise(noise, world_size=(10, 10), mode='fbm', octaves=5)
|
||||
source.add_hill((panel_w // 2, panel_h // 2), 8, 0.3)
|
||||
source.normalize(0.0, 1.0)
|
||||
|
||||
# Create derived heightmaps
|
||||
water_mask = source.threshold((0.0, 0.3)) # Returns NEW heightmap with values only in range
|
||||
land_binary = source.threshold_binary((0.3, 1.0), value=1.0) # Binary mask
|
||||
inverted = source.inverse() # Inverted values
|
||||
|
||||
# Count cells in ranges for classification stats
|
||||
water_count = source.count_in_range((0.0, 0.3))
|
||||
land_count = source.count_in_range((0.3, 0.7))
|
||||
mountain_count = source.count_in_range((0.7, 1.0))
|
||||
|
||||
# IMPORTANT: Render apply_ranges FIRST since it affects the whole layer
|
||||
# Panel 6: Using ColorLayer.apply_ranges (bottom-right)
|
||||
# Create a full-size heightmap and copy source data to correct position
|
||||
panel6_hmap = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=-1.0) # -1 won't match any range
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
panel6_hmap.fill(h, pos=(panel_w * 2 + x, panel_h + y), size=(1, 1))
|
||||
|
||||
# apply_ranges colors cells based on height ranges
|
||||
# Cells with -1.0 won't match any range and stay unchanged
|
||||
color_layer.apply_ranges(panel6_hmap, [
|
||||
((0.0, 0.2), (30, 80, 160)), # Deep water
|
||||
((0.2, 0.3), ((60, 120, 180), (120, 160, 140))), # Gradient: shallow to shore
|
||||
((0.3, 0.5), (80, 150, 60)), # Lowland
|
||||
((0.5, 0.7), ((60, 120, 40), (100, 100, 80))), # Gradient: forest to hills
|
||||
((0.7, 0.85), (130, 120, 110)), # Rock
|
||||
((0.85, 1.0), ((180, 180, 190), (250, 250, 255))), # Gradient: rock to snow
|
||||
])
|
||||
|
||||
# Now render the other 5 panels (they will overwrite only their regions)
|
||||
|
||||
# Panel 1 (top-left): Original grayscale
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
v = int(h * 255)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(v, v, v))
|
||||
|
||||
# Panel 2 (top-middle): threshold() - shows only values in range 0.0-0.3
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = water_mask.get((x, y))
|
||||
if h > 0:
|
||||
# Values were preserved in 0.0-0.3 range
|
||||
t = h / 0.3
|
||||
color_layer.set(((panel_w + x, y)), mcrfpy.Color(
|
||||
int(30 + t * 40), int(60 + t * 60), int(150 + t * 50)))
|
||||
else:
|
||||
# Outside threshold range - dark
|
||||
color_layer.set(((panel_w + x, y)), mcrfpy.Color(20, 20, 30))
|
||||
|
||||
# Panel 3 (top-right): threshold_binary() - land mask
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = land_binary.get((x, y))
|
||||
if h > 0:
|
||||
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(80, 140, 60)) # Land
|
||||
else:
|
||||
color_layer.set(((panel_w * 2 + x, y)), mcrfpy.Color(40, 80, 150)) # Water
|
||||
|
||||
# Panel 4 (bottom-left): inverse()
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = inverted.get((x, y))
|
||||
v = int(h * 255)
|
||||
color_layer.set(((x, panel_h + y)), mcrfpy.Color(v, int(v * 0.8), int(v * 0.6)))
|
||||
|
||||
# Panel 5 (bottom-middle): Classification using count_in_range results
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
h = source.get((x, y))
|
||||
if h < 0.3:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(50, 100, 180)) # Water
|
||||
elif h < 0.7:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(70, 140, 50)) # Land
|
||||
else:
|
||||
color_layer.set(((panel_w + x, panel_h + y)), mcrfpy.Color(140, 130, 120)) # Mountain
|
||||
|
||||
# Labels
|
||||
labels = [
|
||||
("Original (grayscale)", 5, 5),
|
||||
("threshold(0-0.3)", panel_w * CELL_SIZE + 5, 5),
|
||||
("threshold_binary(land)", panel_w * 2 * CELL_SIZE + 5, 5),
|
||||
("inverse()", 5, panel_h * CELL_SIZE + 5),
|
||||
(f"Classified (W:{water_count} L:{land_count} M:{mountain_count})", panel_w * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
|
||||
("apply_ranges (biome)", panel_w * 2 * CELL_SIZE + 5, panel_h * CELL_SIZE + 5),
|
||||
]
|
||||
|
||||
for text, x, y in labels:
|
||||
label = mcrfpy.Caption(text=text, pos=(x, y))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Grid divider lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(80, 80, 80))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(80, 80, 80))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("thresholds_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_08_heightmap_thresholds.png")
|
||||
print("Screenshot saved: procgen_08_heightmap_thresholds.png")
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
"""BSP Dungeon Generation Demo
|
||||
|
||||
Demonstrates: BSP, split_recursive, leaves iteration, to_heightmap
|
||||
Classic roguelike dungeon generation with rooms.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create BSP tree covering the map
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
|
||||
|
||||
# Split recursively to create rooms
|
||||
# depth=4 creates up to 16 rooms, min_size ensures rooms aren't too small
|
||||
bsp.split_recursive(depth=4, min_size=(8, 6), max_ratio=1.5, seed=42)
|
||||
|
||||
# Convert to heightmap for visualization
|
||||
# shrink=1 leaves 1-tile border for walls
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=1,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Fill background (walls)
|
||||
color_layer.fill(mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Draw rooms
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if rooms_hmap.get((x, y)) > 0:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 75, 70))
|
||||
|
||||
# Add some visual variety to rooms
|
||||
room_colors = [
|
||||
mcrfpy.Color(85, 80, 75),
|
||||
mcrfpy.Color(75, 70, 65),
|
||||
mcrfpy.Color(90, 85, 80),
|
||||
mcrfpy.Color(70, 65, 60),
|
||||
]
|
||||
|
||||
for i, leaf in enumerate(bsp.leaves()):
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
color = room_colors[i % len(room_colors)]
|
||||
|
||||
# Fill room interior (with shrink)
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Mark room center
|
||||
cx, cy = leaf.center()
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
color_layer.set(((cx, cy)), mcrfpy.Color(200, 180, 100))
|
||||
|
||||
# Simple corridor generation: connect adjacent rooms
|
||||
# Using adjacency graph
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
for leaf_idx in range(len(bsp)):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
cx1, cy1 = leaf.center()
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
if (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)) in connected:
|
||||
continue
|
||||
connected.add((min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx)))
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Draw L-shaped corridor
|
||||
# Horizontal first, then vertical
|
||||
x1, x2 = min(cx1, cx2), max(cx1, cx2)
|
||||
for x in range(x1, x2 + 1):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= cy1 < GRID_HEIGHT:
|
||||
color_layer.set(((x, cy1)), mcrfpy.Color(100, 95, 90))
|
||||
|
||||
y1, y2 = min(cy1, cy2), max(cy1, cy2)
|
||||
for y in range(y1, y2 + 1):
|
||||
if 0 <= cx2 < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
color_layer.set(((cx2, y)), mcrfpy.Color(100, 95, 90))
|
||||
|
||||
# Draw outer border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(60, 50, 70))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(60, 50, 70))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(60, 50, 70))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(60, 50, 70))
|
||||
|
||||
# Stats
|
||||
stats = mcrfpy.Caption(
|
||||
text=f"BSP Dungeon: {len(bsp)} rooms, depth=4, seed=42",
|
||||
pos=(10, 10)
|
||||
)
|
||||
stats.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
stats.outline = 1
|
||||
stats.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(stats)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_dungeon_demo")
|
||||
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
pos=(0, 0),
|
||||
size=(GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
|
||||
layers={}
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(0, 0, 0)
|
||||
color_layer = grid.add_layer("color", z_index=-1)
|
||||
scene.children.append(grid)
|
||||
|
||||
scene.activate()
|
||||
|
||||
# Run the demo
|
||||
run_demo(0)
|
||||
|
||||
# Take screenshot
|
||||
automation.screenshot("procgen_10_bsp_dungeon.png")
|
||||
print("Screenshot saved: procgen_10_bsp_dungeon.png")
|
||||