Cookbook: draft docs
89
docs/cookbook/procgen/01_heightmap_hills.py
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
"""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")
|
||||
124
docs/cookbook/procgen/02_heightmap_noise.py
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
"""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")
|
||||
116
docs/cookbook/procgen/03_heightmap_operations.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""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")
|
||||
116
docs/cookbook/procgen/04_heightmap_transforms.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""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")
|
||||
135
docs/cookbook/procgen/05_heightmap_erosion.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
"""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")
|
||||
133
docs/cookbook/procgen/06_heightmap_voronoi.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""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")
|
||||
158
docs/cookbook/procgen/07_heightmap_bezier.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""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")
|
||||
148
docs/cookbook/procgen/08_heightmap_thresholds.py
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
"""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")
|
||||
130
docs/cookbook/procgen/10_bsp_dungeon.py
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
"""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")
|
||||
178
docs/cookbook/procgen/11_bsp_traversal.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""BSP Traversal Orders Demo
|
||||
|
||||
Demonstrates: traverse() with different Traversal orders
|
||||
Shows how traversal order affects leaf enumeration.
|
||||
"""
|
||||
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
|
||||
|
||||
traversal_orders = [
|
||||
(mcrfpy.Traversal.PRE_ORDER, "PRE_ORDER", "Root first, then children"),
|
||||
(mcrfpy.Traversal.IN_ORDER, "IN_ORDER", "Left, node, right"),
|
||||
(mcrfpy.Traversal.POST_ORDER, "POST_ORDER", "Children before parent"),
|
||||
(mcrfpy.Traversal.LEVEL_ORDER, "LEVEL_ORDER", "Breadth-first by level"),
|
||||
(mcrfpy.Traversal.INVERTED_LEVEL_ORDER, "INV_LEVEL", "Deepest levels first"),
|
||||
]
|
||||
|
||||
panels = [
|
||||
(0, 0), (panel_w, 0), (panel_w * 2, 0),
|
||||
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
|
||||
]
|
||||
|
||||
# Distinct color palette for 8+ leaves
|
||||
leaf_colors = [
|
||||
mcrfpy.Color(220, 60, 60), # Red
|
||||
mcrfpy.Color(60, 180, 60), # Green
|
||||
mcrfpy.Color(60, 100, 220), # Blue
|
||||
mcrfpy.Color(220, 180, 40), # Yellow
|
||||
mcrfpy.Color(180, 60, 180), # Magenta
|
||||
mcrfpy.Color(60, 200, 200), # Cyan
|
||||
mcrfpy.Color(220, 120, 60), # Orange
|
||||
mcrfpy.Color(160, 100, 200), # Purple
|
||||
mcrfpy.Color(100, 200, 120), # Mint
|
||||
mcrfpy.Color(200, 100, 140), # Pink
|
||||
]
|
||||
|
||||
for panel_idx, (order, name, desc) in enumerate(traversal_orders):
|
||||
if panel_idx >= 6:
|
||||
break
|
||||
|
||||
ox, oy = panels[panel_idx]
|
||||
|
||||
# Create BSP for this panel
|
||||
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
|
||||
|
||||
# Fill panel background (dark gray = walls)
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Traverse and color ONLY LEAVES by their position in traversal
|
||||
leaf_idx = 0
|
||||
for node in bsp.traverse(order):
|
||||
if not node.is_leaf:
|
||||
continue # Skip branch nodes
|
||||
|
||||
color = leaf_colors[leaf_idx % len(leaf_colors)]
|
||||
pos = node.pos
|
||||
size = node.size
|
||||
|
||||
# Shrink by 1 to show walls between rooms
|
||||
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)
|
||||
|
||||
# Draw leaf index in center
|
||||
cx, cy = node.center()
|
||||
# Draw index as a darker spot
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
dark = mcrfpy.Color(color.r // 2, color.g // 2, color.b // 2)
|
||||
color_layer.set(((cx, cy)), dark)
|
||||
if cx + 1 < GRID_WIDTH:
|
||||
color_layer.set(((cx + 1, cy)), dark)
|
||||
|
||||
leaf_idx += 1
|
||||
|
||||
# Add labels
|
||||
label = mcrfpy.Caption(text=f"{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)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=f"{desc} ({leaf_idx} leaves)", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Panel 6: Show tree depth levels (branch AND leaf nodes)
|
||||
ox, oy = panels[5]
|
||||
bsp = mcrfpy.BSP(pos=(ox + 2, oy + 4), size=(panel_w - 4, panel_h - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(5, 4), seed=42)
|
||||
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(40, 35, 45))
|
||||
|
||||
# Draw by level - deepest first so leaves are on top
|
||||
level_colors = [
|
||||
mcrfpy.Color(60, 40, 40), # Level 0 (root) - dark
|
||||
mcrfpy.Color(80, 60, 50), # Level 1
|
||||
mcrfpy.Color(100, 80, 60), # Level 2
|
||||
mcrfpy.Color(140, 120, 80), # Level 3 (leaves usually)
|
||||
]
|
||||
|
||||
# Use INVERTED_LEVEL_ORDER so leaves are drawn last
|
||||
for node in bsp.traverse(mcrfpy.Traversal.INVERTED_LEVEL_ORDER):
|
||||
level = node.level
|
||||
color = level_colors[min(level, len(level_colors) - 1)]
|
||||
|
||||
# Make leaves brighter
|
||||
if node.is_leaf:
|
||||
color = mcrfpy.Color(
|
||||
min(255, color.r + 80),
|
||||
min(255, color.g + 80),
|
||||
min(255, color.b + 60)
|
||||
)
|
||||
|
||||
pos = node.pos
|
||||
size = node.size
|
||||
|
||||
for y in range(pos[1], pos[1] + size[1]):
|
||||
for x in range(pos[0], pos[0] + size[0]):
|
||||
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
|
||||
# Draw border
|
||||
if x == pos[0] or x == pos[0] + size[0] - 1 or \
|
||||
y == pos[1] or y == pos[1] + size[1] - 1:
|
||||
border = mcrfpy.Color(20, 20, 30)
|
||||
color_layer.set(((x, y)), border)
|
||||
else:
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
label = mcrfpy.Caption(text="BY LEVEL (depth)", 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)
|
||||
|
||||
desc_label = mcrfpy.Caption(text="Darker=root, Bright=leaves", pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_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("bsp_traversal_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_11_bsp_traversal.png")
|
||||
print("Screenshot saved: procgen_11_bsp_traversal.png")
|
||||
160
docs/cookbook/procgen/12_bsp_adjacency.py
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"""BSP Adjacency Graph Demo
|
||||
|
||||
Demonstrates: adjacency property, get_leaf, adjacent_tiles
|
||||
Shows room connectivity for corridor generation.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Create dungeon BSP
|
||||
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.4, seed=42)
|
||||
|
||||
# Fill with wall color
|
||||
color_layer.fill(mcrfpy.Color(50, 45, 55))
|
||||
|
||||
# Generate distinct colors for each room
|
||||
num_rooms = len(bsp)
|
||||
room_colors = []
|
||||
for i in range(num_rooms):
|
||||
hue = (i * 137.5) % 360 # Golden angle for good distribution
|
||||
# HSV to RGB (simplified, saturation=0.6, value=0.7)
|
||||
h = hue / 60
|
||||
c = 0.42 # 0.6 * 0.7
|
||||
x = c * (1 - abs(h % 2 - 1))
|
||||
m = 0.28 # 0.7 - c
|
||||
|
||||
if h < 1: r, g, b = c, x, 0
|
||||
elif h < 2: r, g, b = x, c, 0
|
||||
elif h < 3: r, g, b = 0, c, x
|
||||
elif h < 4: r, g, b = 0, x, c
|
||||
elif h < 5: r, g, b = x, 0, c
|
||||
else: r, g, b = c, 0, x
|
||||
|
||||
room_colors.append(mcrfpy.Color(
|
||||
int((r + m) * 255),
|
||||
int((g + m) * 255),
|
||||
int((b + m) * 255)
|
||||
))
|
||||
|
||||
# Draw rooms with unique colors
|
||||
for i, leaf in enumerate(bsp.leaves()):
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
color = room_colors[i]
|
||||
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
color_layer.set(((x, y)), color)
|
||||
|
||||
# Room label
|
||||
cx, cy = leaf.center()
|
||||
label = mcrfpy.Caption(text=str(i), pos=(cx * CELL_SIZE - 4, cy * CELL_SIZE - 8))
|
||||
label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Draw corridors using adjacency graph
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
corridor_color = mcrfpy.Color(100, 95, 90)
|
||||
door_color = mcrfpy.Color(180, 140, 80)
|
||||
|
||||
for leaf_idx in range(num_rooms):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
|
||||
# Get adjacent_tiles for this leaf
|
||||
adj_tiles = leaf.adjacent_tiles
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
|
||||
if pair in connected:
|
||||
continue
|
||||
connected.add(pair)
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
|
||||
# Find shared wall tiles
|
||||
if neighbor_idx in adj_tiles:
|
||||
wall_tiles = adj_tiles[neighbor_idx]
|
||||
if len(wall_tiles) > 0:
|
||||
# Pick middle tile for door
|
||||
mid_tile = wall_tiles[len(wall_tiles) // 2]
|
||||
dx, dy = int(mid_tile.x), int(mid_tile.y)
|
||||
|
||||
# Draw door
|
||||
color_layer.set(((dx, dy)), door_color)
|
||||
|
||||
# Simple corridor: connect room centers through door
|
||||
cx1, cy1 = leaf.center()
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Path from room 1 to door
|
||||
for x in range(min(cx1, dx), max(cx1, dx) + 1):
|
||||
color_layer.set(((x, cy1)), corridor_color)
|
||||
for y in range(min(cy1, dy), max(cy1, dy) + 1):
|
||||
color_layer.set(((dx, y)), corridor_color)
|
||||
|
||||
# Path from door to room 2
|
||||
for x in range(min(dx, cx2), max(dx, cx2) + 1):
|
||||
color_layer.set(((x, dy)), corridor_color)
|
||||
for y in range(min(dy, cy2), max(dy, cy2) + 1):
|
||||
color_layer.set(((cx2, y)), corridor_color)
|
||||
else:
|
||||
# Fallback: L-shaped corridor
|
||||
cx1, cy1 = leaf.center()
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
for x in range(min(cx1, cx2), max(cx1, cx2) + 1):
|
||||
color_layer.set(((x, cy1)), corridor_color)
|
||||
for y in range(min(cy1, cy2), max(cy1, cy2) + 1):
|
||||
color_layer.set(((cx2, y)), corridor_color)
|
||||
|
||||
# Title and stats
|
||||
title = mcrfpy.Caption(
|
||||
text=f"BSP Adjacency: {num_rooms} rooms, {len(connected)} connections",
|
||||
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)
|
||||
|
||||
# Legend
|
||||
legend = mcrfpy.Caption(
|
||||
text="Numbers = room index, Gold = doors, Brown = corridors",
|
||||
pos=(10, GRID_HEIGHT * CELL_SIZE - 25)
|
||||
)
|
||||
legend.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
legend.outline = 1
|
||||
legend.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(legend)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_adjacency_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_12_bsp_adjacency.png")
|
||||
print("Screenshot saved: procgen_12_bsp_adjacency.png")
|
||||
178
docs/cookbook/procgen/13_bsp_shrink.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
"""BSP Shrink Parameter Demo
|
||||
|
||||
Demonstrates: to_heightmap with different shrink values
|
||||
Shows room padding for walls and varied room sizes.
|
||||
"""
|
||||
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
|
||||
|
||||
# Use reasonable shrink values relative to room sizes
|
||||
shrink_values = [
|
||||
(0, "shrink=0", "Rooms fill BSP bounds"),
|
||||
(1, "shrink=1", "Standard 1-tile walls"),
|
||||
(2, "shrink=2", "Thick fortress walls"),
|
||||
(3, "shrink=3", "Wide hallway spacing"),
|
||||
(-1, "Random shrink", "Per-room variation"),
|
||||
(-2, "Gradient", "Shrink by leaf index"),
|
||||
]
|
||||
|
||||
panels = [
|
||||
(0, 0), (panel_w, 0), (panel_w * 2, 0),
|
||||
(0, panel_h), (panel_w, panel_h), (panel_w * 2, panel_h)
|
||||
]
|
||||
|
||||
for panel_idx, (shrink, title, desc) in enumerate(shrink_values):
|
||||
ox, oy = panels[panel_idx]
|
||||
|
||||
# Create BSP - use depth=2 for larger rooms, bigger min_size
|
||||
bsp = mcrfpy.BSP(pos=(ox + 1, oy + 3), size=(panel_w - 2, panel_h - 4))
|
||||
bsp.split_recursive(depth=2, min_size=(8, 6), seed=42)
|
||||
|
||||
# Fill panel background (stone wall)
|
||||
color_layer.fill_rect((ox, oy), (panel_w, panel_h), mcrfpy.Color(50, 45, 55))
|
||||
|
||||
if shrink >= 0:
|
||||
# Standard shrink value using to_heightmap
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=shrink,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Draw floors with color based on shrink level
|
||||
floor_colors = [
|
||||
mcrfpy.Color(140, 120, 100), # shrink=0: tan/full
|
||||
mcrfpy.Color(110, 100, 90), # shrink=1: gray-brown
|
||||
mcrfpy.Color(90, 95, 100), # shrink=2: blue-gray
|
||||
mcrfpy.Color(80, 90, 110), # shrink=3: slate
|
||||
]
|
||||
floor_color = floor_colors[min(shrink, len(floor_colors) - 1)]
|
||||
|
||||
for y in range(oy, oy + panel_h):
|
||||
for x in range(ox, ox + panel_w):
|
||||
if rooms_hmap.get((x, y)) > 0:
|
||||
# Add subtle tile pattern
|
||||
var = ((x + y) % 2) * 8
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
elif shrink == -1:
|
||||
# Random shrink per room
|
||||
import random
|
||||
rand = random.Random(42)
|
||||
for leaf in bsp.leaves():
|
||||
room_shrink = rand.randint(0, 3)
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
|
||||
x1 = pos[0] + room_shrink
|
||||
y1 = pos[1] + room_shrink
|
||||
x2 = pos[0] + size[0] - room_shrink
|
||||
y2 = pos[1] + size[1] - room_shrink
|
||||
|
||||
if x2 > x1 and y2 > y1:
|
||||
colors = [
|
||||
mcrfpy.Color(160, 130, 100), # Full
|
||||
mcrfpy.Color(130, 120, 100),
|
||||
mcrfpy.Color(100, 110, 110),
|
||||
mcrfpy.Color(80, 90, 100), # Most shrunk
|
||||
]
|
||||
floor_color = colors[room_shrink]
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
|
||||
var = ((x + y) % 2) * 6
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
else:
|
||||
# Gradient shrink by leaf index
|
||||
leaves = list(bsp.leaves())
|
||||
for i, leaf in enumerate(leaves):
|
||||
# Shrink increases with leaf index
|
||||
room_shrink = min(3, i)
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
|
||||
x1 = pos[0] + room_shrink
|
||||
y1 = pos[1] + room_shrink
|
||||
x2 = pos[0] + size[0] - room_shrink
|
||||
y2 = pos[1] + size[1] - room_shrink
|
||||
|
||||
if x2 > x1 and y2 > y1:
|
||||
# Color gradient: warm to cool as shrink increases
|
||||
t = i / max(1, len(leaves) - 1)
|
||||
floor_color = mcrfpy.Color(
|
||||
int(180 - t * 80),
|
||||
int(120 + t * 20),
|
||||
int(80 + t * 60)
|
||||
)
|
||||
|
||||
for y in range(y1, y2):
|
||||
for x in range(x1, x2):
|
||||
if ox <= x < ox + panel_w and oy <= y < oy + panel_h:
|
||||
var = ((x + y) % 2) * 6
|
||||
c = mcrfpy.Color(
|
||||
floor_color.r + var,
|
||||
floor_color.g + var - 2,
|
||||
floor_color.b + var
|
||||
)
|
||||
color_layer.set(((x, y)), c)
|
||||
|
||||
# Add labels
|
||||
label = mcrfpy.Caption(text=title, 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)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, oy * CELL_SIZE + 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_label)
|
||||
|
||||
# Grid lines
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((panel_w - 1, y)), mcrfpy.Color(30, 30, 35))
|
||||
color_layer.set(((panel_w * 2 - 1, y)), mcrfpy.Color(30, 30, 35))
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, panel_h - 1)), mcrfpy.Color(30, 30, 35))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_shrink_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_13_bsp_shrink.png")
|
||||
print("Screenshot saved: procgen_13_bsp_shrink.png")
|
||||
150
docs/cookbook/procgen/14_bsp_manual_split.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
"""BSP Manual Split Demo
|
||||
|
||||
Demonstrates: split_once for controlled layouts
|
||||
Shows handcrafted room placement with manual BSP control.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Fill background
|
||||
color_layer.fill(mcrfpy.Color(50, 45, 55))
|
||||
|
||||
# Create main BSP covering most of the map
|
||||
bsp = mcrfpy.BSP(pos=(2, 2), size=(GRID_WIDTH - 4, GRID_HEIGHT - 4))
|
||||
|
||||
# Manual split strategy for a temple-like layout:
|
||||
# 1. Split horizontally to create upper/lower sections
|
||||
# 2. Upper section: main hall (large) + side rooms
|
||||
# 3. Lower section: entrance + storage areas
|
||||
|
||||
# First split: horizontal, creating top (sanctuary) and bottom (entrance) areas
|
||||
# Split at about 60% height
|
||||
split_y = 2 + int((GRID_HEIGHT - 4) * 0.6)
|
||||
bsp.split_once(horizontal=True, position=split_y)
|
||||
|
||||
# Now manually color the structure
|
||||
root = bsp.root
|
||||
|
||||
# Get the two main regions
|
||||
upper = root.left # Sanctuary area
|
||||
lower = root.right # Entrance area
|
||||
|
||||
# Color the sanctuary (upper area) - golden temple floor
|
||||
if upper:
|
||||
pos, size = upper.pos, upper.size
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
# Create a pattern
|
||||
if (x + y) % 4 == 0:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(180, 150, 80))
|
||||
else:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(160, 130, 70))
|
||||
|
||||
# Add altar in center of sanctuary
|
||||
cx, cy = upper.center()
|
||||
for dy in range(-2, 3):
|
||||
for dx in range(-3, 4):
|
||||
nx, ny = cx + dx, cy + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
if abs(dx) <= 1 and abs(dy) <= 1:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(200, 180, 100)) # Altar
|
||||
else:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(140, 100, 60)) # Altar base
|
||||
|
||||
# Color the entrance (lower area) - stone floor
|
||||
if lower:
|
||||
pos, size = lower.pos, lower.size
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
base = 80 + ((x * 3 + y * 7) % 20)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 5, base - 10))
|
||||
|
||||
# Add entrance path
|
||||
cx = pos[0] + size[0] // 2
|
||||
for y in range(pos[1] + size[1] - 1, pos[1], -1):
|
||||
for dx in range(-2, 3):
|
||||
nx = cx + dx
|
||||
if pos[0] < nx < pos[0] + size[0] - 1:
|
||||
color_layer.set(((nx, y)), mcrfpy.Color(100, 95, 85))
|
||||
|
||||
# Add pillars along the sides
|
||||
if upper:
|
||||
pos, size = upper.pos, upper.size
|
||||
for y in range(pos[1] + 3, pos[1] + size[1] - 3, 4):
|
||||
# Left pillars
|
||||
color_layer.set(((pos[0] + 3, y)), mcrfpy.Color(120, 110, 100))
|
||||
color_layer.set(((pos[0] + 3, y + 1)), mcrfpy.Color(120, 110, 100))
|
||||
# Right pillars
|
||||
color_layer.set(((pos[0] + size[0] - 4, y)), mcrfpy.Color(120, 110, 100))
|
||||
color_layer.set(((pos[0] + size[0] - 4, y + 1)), mcrfpy.Color(120, 110, 100))
|
||||
|
||||
# Add side chambers using manual rectangles
|
||||
# Left chamber
|
||||
chamber_w, chamber_h = 8, 6
|
||||
for y in range(10, 10 + chamber_h):
|
||||
for x in range(4, 4 + chamber_w):
|
||||
if x == 4 or x == 4 + chamber_w - 1 or y == 10 or y == 10 + chamber_h - 1:
|
||||
continue # Skip border (walls)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(100, 80, 90)) # Purple-ish storage
|
||||
|
||||
# Right chamber
|
||||
for y in range(10, 10 + chamber_h):
|
||||
for x in range(GRID_WIDTH - 4 - chamber_w, GRID_WIDTH - 4):
|
||||
if x == GRID_WIDTH - 4 - chamber_w or x == GRID_WIDTH - 5 or y == 10 or y == 10 + chamber_h - 1:
|
||||
continue
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 100, 90)) # Green-ish treasury
|
||||
|
||||
# Connect chambers to main hall
|
||||
hall_y = 12
|
||||
for x in range(4 + chamber_w, 15):
|
||||
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
|
||||
for x in range(GRID_WIDTH - 15, GRID_WIDTH - 4 - chamber_w):
|
||||
color_layer.set(((x, hall_y)), mcrfpy.Color(90, 85, 80))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="BSP split_once: Temple Layout", 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)
|
||||
|
||||
# Labels for areas
|
||||
labels = [
|
||||
("SANCTUARY", GRID_WIDTH // 2 * CELL_SIZE - 40, 80),
|
||||
("ENTRANCE", GRID_WIDTH // 2 * CELL_SIZE - 35, split_y * CELL_SIZE + 30),
|
||||
("Storage", 50, 180),
|
||||
("Treasury", (GRID_WIDTH - 10) * CELL_SIZE - 30, 180),
|
||||
]
|
||||
for text, x, y in labels:
|
||||
lbl = mcrfpy.Caption(text=text, pos=(x, y))
|
||||
lbl.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
lbl.outline = 1
|
||||
lbl.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(lbl)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("bsp_manual_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_14_bsp_manual_split.png")
|
||||
print("Screenshot saved: procgen_14_bsp_manual_split.png")
|
||||
125
docs/cookbook/procgen/20_noise_algorithms.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""NoiseSource Algorithms Demo
|
||||
|
||||
Demonstrates: simplex, perlin, wavelet noise algorithms
|
||||
Shows visual differences between noise types.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_terrain(h):
|
||||
"""Convert noise value (-1 to 1) to terrain color."""
|
||||
# Normalize from -1..1 to 0..1
|
||||
h = (h + 1) / 2
|
||||
h = max(0.0, min(1.0, h))
|
||||
|
||||
if h < 0.3:
|
||||
t = h / 0.3
|
||||
return mcrfpy.Color(int(30 + t * 40), int(60 + t * 60), int(140 + t * 40))
|
||||
elif h < 0.45:
|
||||
t = (h - 0.3) / 0.15
|
||||
return mcrfpy.Color(int(70 + t * 120), int(120 + t * 60), int(100 - t * 60))
|
||||
elif h < 0.6:
|
||||
t = (h - 0.45) / 0.15
|
||||
return mcrfpy.Color(int(60 + t * 20), int(130 + t * 20), int(50 + t * 10))
|
||||
elif h < 0.75:
|
||||
t = (h - 0.6) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 50), int(110 - t * 20), int(40 + t * 20))
|
||||
elif h < 0.88:
|
||||
t = (h - 0.75) / 0.13
|
||||
return mcrfpy.Color(int(100 + t * 40), int(95 + t * 35), int(80 + t * 40))
|
||||
else:
|
||||
t = (h - 0.88) / 0.12
|
||||
return mcrfpy.Color(int(180 + t * 70), int(180 + t * 70), int(190 + t * 60))
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 2
|
||||
|
||||
algorithms = [
|
||||
('simplex', "SIMPLEX", "Fast, no visible artifacts"),
|
||||
('perlin', "PERLIN", "Classic, slight grid bias"),
|
||||
('wavelet', "WAVELET", "Smooth, no tiling"),
|
||||
]
|
||||
|
||||
# Top row: FBM (natural terrain)
|
||||
# Bottom row: Raw noise (single octave)
|
||||
for col, (algo, name, desc) in enumerate(algorithms):
|
||||
ox = col * panel_w
|
||||
|
||||
# Create noise source
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm=algo,
|
||||
hurst=0.5,
|
||||
lacunarity=2.0,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Top panel: FBM
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
# Sample at world coordinates
|
||||
wx = x * 0.15
|
||||
wy = y * 0.15
|
||||
val = noise.fbm((wx, wy), octaves=5)
|
||||
color_layer.set(((ox + x, y)), value_to_terrain(val))
|
||||
|
||||
# Bottom panel: Raw (flat)
|
||||
for y in range(panel_h):
|
||||
for x in range(panel_w):
|
||||
wx = x * 0.15
|
||||
wy = y * 0.15
|
||||
val = noise.get((wx, wy))
|
||||
color_layer.set(((ox + x, panel_h + y)), value_to_terrain(val))
|
||||
|
||||
# Labels
|
||||
top_label = mcrfpy.Caption(text=f"{name} (FBM)", pos=(ox * CELL_SIZE + 5, 5))
|
||||
top_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
top_label.outline = 1
|
||||
top_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(top_label)
|
||||
|
||||
bottom_label = mcrfpy.Caption(text=f"{name} (raw)", pos=(ox * CELL_SIZE + 5, panel_h * CELL_SIZE + 5))
|
||||
bottom_label.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
bottom_label.outline = 1
|
||||
bottom_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(bottom_label)
|
||||
|
||||
desc_label = mcrfpy.Caption(text=desc, pos=(ox * CELL_SIZE + 5, 22))
|
||||
desc_label.fill_color = mcrfpy.Color(200, 200, 200)
|
||||
desc_label.outline = 1
|
||||
desc_label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(desc_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("noise_algo_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_20_noise_algorithms.png")
|
||||
print("Screenshot saved: procgen_20_noise_algorithms.png")
|
||||
115
docs/cookbook/procgen/21_noise_parameters.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
"""NoiseSource Parameters Demo
|
||||
|
||||
Demonstrates: hurst (roughness), lacunarity (frequency scaling), octaves
|
||||
Shows how parameters affect terrain character.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def value_to_gray(h):
|
||||
"""Simple grayscale visualization."""
|
||||
h = (h + 1) / 2 # -1..1 to 0..1
|
||||
h = max(0.0, min(1.0, h))
|
||||
v = int(h * 255)
|
||||
return mcrfpy.Color(v, v, v)
|
||||
|
||||
def run_demo(runtime):
|
||||
panel_w = GRID_WIDTH // 3
|
||||
panel_h = GRID_HEIGHT // 3
|
||||
|
||||
# 3x3 grid showing parameter variations
|
||||
# Rows: different hurst values (roughness)
|
||||
# Cols: different lacunarity values
|
||||
|
||||
hurst_values = [0.2, 0.5, 0.8]
|
||||
lacunarity_values = [1.5, 2.0, 3.0]
|
||||
|
||||
for row, hurst in enumerate(hurst_values):
|
||||
for col, lacunarity in enumerate(lacunarity_values):
|
||||
ox = col * panel_w
|
||||
oy = row * panel_h
|
||||
|
||||
# Create noise with these parameters
|
||||
noise = mcrfpy.NoiseSource(
|
||||
dimensions=2,
|
||||
algorithm='simplex',
|
||||
hurst=hurst,
|
||||
lacunarity=lacunarity,
|
||||
seed=42
|
||||
)
|
||||
|
||||
# Sample using heightmap for efficiency
|
||||
hmap = noise.sample(
|
||||
size=(panel_w, panel_h),
|
||||
world_origin=(0, 0),
|
||||
world_size=(10, 10),
|
||||
mode='fbm',
|
||||
octaves=6
|
||||
)
|
||||
|
||||
# Apply to color layer
|
||||
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_gray(h))
|
||||
|
||||
# Parameter label
|
||||
label = mcrfpy.Caption(
|
||||
text=f"H={hurst} L={lacunarity}",
|
||||
pos=(ox * CELL_SIZE + 3, oy * CELL_SIZE + 3)
|
||||
)
|
||||
label.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
# Row/Column labels
|
||||
row_labels = ["Low Hurst (rough)", "Mid Hurst (natural)", "High Hurst (smooth)"]
|
||||
for row, text in enumerate(row_labels):
|
||||
label = mcrfpy.Caption(text=text, pos=(5, row * panel_h * CELL_SIZE + panel_h * CELL_SIZE - 20))
|
||||
label.fill_color = mcrfpy.Color(255, 200, 100)
|
||||
label.outline = 1
|
||||
label.outline_color = mcrfpy.Color(0, 0, 0)
|
||||
scene.children.append(label)
|
||||
|
||||
col_labels = ["Low Lacunarity", "Standard (2.0)", "High Lacunarity"]
|
||||
for col, text in enumerate(col_labels):
|
||||
label = mcrfpy.Caption(text=text, pos=(col * panel_w * CELL_SIZE + 5, GRID_HEIGHT * CELL_SIZE - 20))
|
||||
label.fill_color = mcrfpy.Color(100, 200, 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(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))
|
||||
color_layer.set(((x, panel_h * 2 - 1)), mcrfpy.Color(100, 100, 100))
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("noise_params_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_21_noise_parameters.png")
|
||||
print("Screenshot saved: procgen_21_noise_parameters.png")
|
||||
163
docs/cookbook/procgen/30_advanced_cave_dungeon.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"""Advanced: Cave-Carved Dungeon
|
||||
|
||||
Combines: BSP (room structure) + Noise (organic cave walls) + Erosion
|
||||
Creates a dungeon where rooms have been carved from natural cave formations.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base cave system using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
cave_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
cave_map.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4)
|
||||
cave_map.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create BSP rooms
|
||||
bsp = mcrfpy.BSP(pos=(3, 3), size=(GRID_WIDTH - 6, GRID_HEIGHT - 6))
|
||||
bsp.split_recursive(depth=3, min_size=(10, 8), max_ratio=1.5, seed=42)
|
||||
|
||||
rooms_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=2,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Step 3: Combine - rooms carve into cave, cave affects walls
|
||||
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
combined.copy_from(cave_map)
|
||||
|
||||
# Scale cave values to mid-range so rooms stand out
|
||||
combined.scale(0.5)
|
||||
combined.add_constant(0.2)
|
||||
|
||||
# Add room interiors (rooms become high values)
|
||||
combined.max(rooms_hmap)
|
||||
|
||||
# Step 4: Apply GENTLE erosion for organic edges
|
||||
# Use fewer drops and lower erosion rate
|
||||
combined.rain_erosion(drops=100, erosion=0.02, sedimentation=0.01, seed=42)
|
||||
|
||||
# Re-normalize to ensure we use the full value range
|
||||
combined.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Create corridor connections
|
||||
adjacency = bsp.adjacency
|
||||
connected = set()
|
||||
|
||||
corridor_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
|
||||
for leaf_idx in range(len(bsp)):
|
||||
leaf = bsp.get_leaf(leaf_idx)
|
||||
cx1, cy1 = leaf.center()
|
||||
|
||||
for neighbor_idx in adjacency[leaf_idx]:
|
||||
pair = (min(leaf_idx, neighbor_idx), max(leaf_idx, neighbor_idx))
|
||||
if pair in connected:
|
||||
continue
|
||||
connected.add(pair)
|
||||
|
||||
neighbor = bsp.get_leaf(neighbor_idx)
|
||||
cx2, cy2 = neighbor.center()
|
||||
|
||||
# Draw corridor using bezier for organic feel
|
||||
mid_x = (cx1 + cx2) // 2 + ((leaf_idx * 3) % 5 - 2)
|
||||
mid_y = (cy1 + cy2) // 2 + ((neighbor_idx * 7) % 5 - 2)
|
||||
|
||||
corridor_map.dig_bezier(
|
||||
points=((cx1, cy1), (mid_x, cy1), (mid_x, cy2), (cx2, cy2)),
|
||||
start_radius=1.5, end_radius=1.5,
|
||||
start_height=0.0, end_height=0.0
|
||||
)
|
||||
|
||||
# Add corridors - dig_bezier creates low values where corridors are
|
||||
# We want high values there, so invert the corridor map logic
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
corr_val = corridor_map.get((x, y))
|
||||
if corr_val < 0.5: # Corridor was dug here
|
||||
current = combined.get((x, y))
|
||||
combined.fill(max(current, 0.7), pos=(x, y), size=(1, 1))
|
||||
|
||||
# Step 6: Render with cave aesthetics
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
h = combined.get((x, y))
|
||||
|
||||
if h < 0.30:
|
||||
# Solid rock/wall - darker
|
||||
base = 30 + int(cave_map.get((x, y)) * 20)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif h < 0.40:
|
||||
# Cave wall edge (rough transition)
|
||||
t = (h - 0.30) / 0.10
|
||||
base = int(40 + t * 15)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif h < 0.55:
|
||||
# Cave floor (natural stone)
|
||||
t = (h - 0.40) / 0.15
|
||||
base = 65 + int(t * 20)
|
||||
var = ((x * 7 + y * 11) % 10)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 10))
|
||||
elif h < 0.70:
|
||||
# Corridor/worked passage
|
||||
base = 85 + ((x + y) % 2) * 5
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 3, base - 6))
|
||||
else:
|
||||
# Room floor (finely worked stone)
|
||||
base = 105 + ((x + y) % 2) * 8
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base, base - 8, base - 12))
|
||||
|
||||
# Mark room centers with special tile
|
||||
for leaf in bsp.leaves():
|
||||
cx, cy = leaf.center()
|
||||
if 0 <= cx < GRID_WIDTH and 0 <= cy < GRID_HEIGHT:
|
||||
color_layer.set(((cx, cy)), mcrfpy.Color(160, 140, 120))
|
||||
# Cross pattern
|
||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nx, ny = cx + dx, cy + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(140, 125, 105))
|
||||
|
||||
# Outer border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(20, 15, 25))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 15, 25))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(20, 15, 25))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 15, 25))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Cave-Carved Dungeon: BSP + Noise + Erosion", 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)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("cave_dungeon")
|
||||
|
||||
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_30_advanced_cave_dungeon.png")
|
||||
print("Screenshot saved: procgen_30_advanced_cave_dungeon.png")
|
||||
140
docs/cookbook/procgen/31_advanced_island.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
"""Advanced: Island Terrain Generation
|
||||
|
||||
Combines: Noise (base terrain) + Voronoi (biomes) + Hills + Erosion + Bezier (rivers)
|
||||
Creates a tropical island with varied biomes and water features.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def biome_color(elevation, moisture):
|
||||
"""Determine color based on elevation and moisture."""
|
||||
if elevation < 0.25:
|
||||
# Water
|
||||
t = elevation / 0.25
|
||||
return mcrfpy.Color(int(30 + t * 30), int(80 + t * 40), int(160 + t * 40))
|
||||
elif elevation < 0.32:
|
||||
# Beach
|
||||
return mcrfpy.Color(220, 200, 150)
|
||||
elif elevation < 0.5:
|
||||
# Lowland - varies by moisture
|
||||
if moisture < 0.3:
|
||||
return mcrfpy.Color(180, 170, 110) # Desert/savanna
|
||||
elif moisture < 0.6:
|
||||
return mcrfpy.Color(80, 140, 60) # Grassland
|
||||
else:
|
||||
return mcrfpy.Color(40, 100, 50) # Rainforest
|
||||
elif elevation < 0.7:
|
||||
# Highland
|
||||
if moisture < 0.4:
|
||||
return mcrfpy.Color(100, 90, 70) # Dry hills
|
||||
else:
|
||||
return mcrfpy.Color(50, 90, 45) # Forest
|
||||
elif elevation < 0.85:
|
||||
# Mountain
|
||||
return mcrfpy.Color(110, 105, 100)
|
||||
else:
|
||||
# Peak
|
||||
return mcrfpy.Color(220, 225, 230)
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base elevation using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
elevation = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
elevation.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=5)
|
||||
elevation.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create island shape using radial falloff
|
||||
cx, cy = GRID_WIDTH / 2, GRID_HEIGHT / 2
|
||||
max_dist = min(cx, cy) * 0.85
|
||||
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
dist = ((x - cx) ** 2 + (y - cy) ** 2) ** 0.5
|
||||
falloff = max(0, 1 - (dist / max_dist) ** 1.5)
|
||||
current = elevation.get((x, y))
|
||||
elevation.fill(current * falloff, pos=(x, y), size=(1, 1))
|
||||
|
||||
# Step 3: Add central mountain range
|
||||
elevation.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 15, 0.5)
|
||||
elevation.add_hill((GRID_WIDTH // 2 - 8, GRID_HEIGHT // 2 + 3), 8, 0.3)
|
||||
elevation.add_hill((GRID_WIDTH // 2 + 10, GRID_HEIGHT // 2 - 5), 6, 0.25)
|
||||
|
||||
# Step 4: Create moisture map using different noise
|
||||
moisture_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=123)
|
||||
moisture = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
moisture.add_noise(moisture_noise, world_size=(8, 8), mode='fbm', octaves=3)
|
||||
moisture.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Add voronoi for biome boundaries
|
||||
biome_regions = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
biome_regions.add_voronoi(num_points=8, coefficients=(0.5, -0.3), seed=77)
|
||||
biome_regions.normalize(0.0, 1.0)
|
||||
|
||||
# Blend voronoi into moisture
|
||||
moisture.lerp(biome_regions, 0.4)
|
||||
|
||||
# Step 6: Apply erosion to elevation
|
||||
elevation.rain_erosion(drops=2000, erosion=0.08, sedimentation=0.04, seed=42)
|
||||
elevation.normalize(0.0, 1.0)
|
||||
|
||||
# Step 7: Carve rivers from mountains to sea
|
||||
# Main river
|
||||
elevation.dig_bezier(
|
||||
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 - 5),
|
||||
(GRID_WIDTH // 2 - 10, GRID_HEIGHT // 2),
|
||||
(GRID_WIDTH // 4, GRID_HEIGHT // 2 + 5),
|
||||
(5, GRID_HEIGHT // 2 + 8)),
|
||||
start_radius=0.5, end_radius=2,
|
||||
start_height=0.3, end_height=0.15
|
||||
)
|
||||
|
||||
# Secondary river
|
||||
elevation.dig_bezier(
|
||||
points=((GRID_WIDTH // 2 + 5, GRID_HEIGHT // 2),
|
||||
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 3),
|
||||
(GRID_WIDTH - 15, GRID_HEIGHT // 4),
|
||||
(GRID_WIDTH - 5, GRID_HEIGHT // 4 + 3)),
|
||||
start_radius=0.5, end_radius=1.5,
|
||||
start_height=0.32, end_height=0.18
|
||||
)
|
||||
|
||||
# Step 8: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
elev = elevation.get((x, y))
|
||||
moist = moisture.get((x, y))
|
||||
color_layer.set(((x, y)), biome_color(elev, moist))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Island Terrain: Noise + Voronoi + Hills + Erosion + Rivers", 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)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("island")
|
||||
|
||||
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_31_advanced_island.png")
|
||||
print("Screenshot saved: procgen_31_advanced_island.png")
|
||||
164
docs/cookbook/procgen/32_advanced_city.py
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
"""Advanced: Procedural City Map
|
||||
|
||||
Combines: BSP (city blocks/buildings) + Noise (terrain/parks) + Voronoi (districts)
|
||||
Creates a city map with districts, buildings, roads, and parks.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create district map using voronoi
|
||||
districts = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
districts.add_voronoi(num_points=6, coefficients=(1.0, 0.0), seed=42)
|
||||
districts.normalize(0.0, 1.0)
|
||||
|
||||
# District types based on value
|
||||
# 0.0-0.2: Residential (green-ish)
|
||||
# 0.2-0.4: Commercial (blue-ish)
|
||||
# 0.4-0.6: Industrial (gray)
|
||||
# 0.6-0.8: Park/nature
|
||||
# 0.8-1.0: Downtown (tall buildings)
|
||||
|
||||
# Step 2: Create building blocks using BSP
|
||||
bsp = mcrfpy.BSP(pos=(1, 1), size=(GRID_WIDTH - 2, GRID_HEIGHT - 2))
|
||||
bsp.split_recursive(depth=4, min_size=(6, 5), max_ratio=2.0, seed=42)
|
||||
|
||||
# Step 3: Create park areas using noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=99)
|
||||
parks = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
parks.add_noise(noise, world_size=(8, 8), mode='fbm', octaves=3)
|
||||
parks.normalize(0.0, 1.0)
|
||||
|
||||
# Step 4: Render base (roads)
|
||||
color_layer.fill(mcrfpy.Color(60, 60, 65)) # Asphalt
|
||||
|
||||
# Step 5: Draw buildings based on BSP and district type
|
||||
for leaf in bsp.leaves():
|
||||
pos = leaf.pos
|
||||
size = leaf.size
|
||||
cx, cy = leaf.center()
|
||||
|
||||
# Get district type at center
|
||||
district_val = districts.get((cx, cy))
|
||||
|
||||
# Shrink for roads between buildings
|
||||
shrink = 1
|
||||
|
||||
# Determine building style based on district
|
||||
if district_val < 0.2:
|
||||
# Residential
|
||||
building_color = mcrfpy.Color(140, 160, 140)
|
||||
roof_color = mcrfpy.Color(160, 100, 80)
|
||||
shrink = 2 # More space between houses
|
||||
elif district_val < 0.4:
|
||||
# Commercial
|
||||
building_color = mcrfpy.Color(120, 140, 170)
|
||||
roof_color = mcrfpy.Color(80, 100, 130)
|
||||
elif district_val < 0.6:
|
||||
# Industrial
|
||||
building_color = mcrfpy.Color(100, 100, 105)
|
||||
roof_color = mcrfpy.Color(70, 70, 75)
|
||||
elif district_val < 0.8:
|
||||
# Park area - check noise for actual park placement
|
||||
park_val = parks.get((cx, cy))
|
||||
if park_val > 0.4:
|
||||
# This block is a park
|
||||
for y in range(pos[1] + 1, pos[1] + size[1] - 1):
|
||||
for x in range(pos[0] + 1, pos[0] + size[0] - 1):
|
||||
t = parks.get((x, y))
|
||||
if t > 0.6:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(50, 120, 50)) # Trees
|
||||
else:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 150, 80)) # Grass
|
||||
continue
|
||||
else:
|
||||
building_color = mcrfpy.Color(130, 150, 130)
|
||||
roof_color = mcrfpy.Color(100, 80, 70)
|
||||
else:
|
||||
# Downtown
|
||||
building_color = mcrfpy.Color(150, 155, 165)
|
||||
roof_color = mcrfpy.Color(90, 95, 110)
|
||||
shrink = 1 # Dense buildings
|
||||
|
||||
# Draw building
|
||||
for y in range(pos[1] + shrink, pos[1] + size[1] - shrink):
|
||||
for x in range(pos[0] + shrink, pos[0] + size[0] - shrink):
|
||||
# Building edge (roof)
|
||||
if y == pos[1] + shrink or y == pos[1] + size[1] - shrink - 1:
|
||||
color_layer.set(((x, y)), roof_color)
|
||||
elif x == pos[0] + shrink or x == pos[0] + size[0] - shrink - 1:
|
||||
color_layer.set(((x, y)), roof_color)
|
||||
else:
|
||||
color_layer.set(((x, y)), building_color)
|
||||
|
||||
# Step 6: Add main roads (cross the city)
|
||||
road_color = mcrfpy.Color(70, 70, 75)
|
||||
marking_color = mcrfpy.Color(200, 200, 100)
|
||||
|
||||
# Horizontal main road
|
||||
main_y = GRID_HEIGHT // 2
|
||||
for x in range(GRID_WIDTH):
|
||||
for dy in range(-1, 2):
|
||||
if 0 <= main_y + dy < GRID_HEIGHT:
|
||||
color_layer.set(((x, main_y + dy)), road_color)
|
||||
# Road markings
|
||||
if x % 4 == 0:
|
||||
color_layer.set(((x, main_y)), marking_color)
|
||||
|
||||
# Vertical main road
|
||||
main_x = GRID_WIDTH // 2
|
||||
for y in range(GRID_HEIGHT):
|
||||
for dx in range(-1, 2):
|
||||
if 0 <= main_x + dx < GRID_WIDTH:
|
||||
color_layer.set(((main_x + dx, y)), road_color)
|
||||
if y % 4 == 0:
|
||||
color_layer.set(((main_x, y)), marking_color)
|
||||
|
||||
# Intersection
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
color_layer.set(((main_x + dx, main_y + dy)), road_color)
|
||||
|
||||
# Step 7: Add a central plaza
|
||||
plaza_x, plaza_y = main_x, main_y
|
||||
for dy in range(-3, 4):
|
||||
for dx in range(-4, 5):
|
||||
nx, ny = plaza_x + dx, plaza_y + dy
|
||||
if 0 <= nx < GRID_WIDTH and 0 <= ny < GRID_HEIGHT:
|
||||
if abs(dx) <= 1 and abs(dy) <= 1:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(180, 160, 140)) # Fountain
|
||||
else:
|
||||
color_layer.set(((nx, ny)), mcrfpy.Color(160, 150, 140)) # Plaza tiles
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Procedural City: BSP + Voronoi Districts + Noise Parks", 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)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("city")
|
||||
|
||||
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_32_advanced_city.png")
|
||||
print("Screenshot saved: procgen_32_advanced_city.png")
|
||||
163
docs/cookbook/procgen/33_advanced_caves.py
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
"""Advanced: Natural Cave System
|
||||
|
||||
Combines: Noise (cave formation) + Threshold (open areas) + Kernel (smoothing) + BSP (structured areas)
|
||||
Creates organic cave networks with some structured rooms.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Generate cave base using turbulent noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
cave_noise = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
cave_noise.add_noise(noise, world_size=(10, 8), mode='turbulence', octaves=4)
|
||||
cave_noise.normalize(0.0, 1.0)
|
||||
|
||||
# Step 2: Create cave mask via threshold
|
||||
# Values > 0.45 become open cave, rest is rock
|
||||
cave_mask = cave_noise.threshold_binary((0.4, 1.0), 1.0)
|
||||
|
||||
# Step 3: Apply smoothing kernel to remove isolated pixels
|
||||
smooth_kernel = {
|
||||
(-1, -1): 1, (0, -1): 2, (1, -1): 1,
|
||||
(-1, 0): 2, (0, 0): 4, (1, 0): 2,
|
||||
(-1, 1): 1, (0, 1): 2, (1, 1): 1,
|
||||
}
|
||||
cave_mask.kernel_transform(smooth_kernel)
|
||||
cave_mask.normalize(0.0, 1.0)
|
||||
|
||||
# Re-threshold after smoothing
|
||||
cave_mask = cave_mask.threshold_binary((0.5, 1.0), 1.0)
|
||||
|
||||
# Step 4: Add some structured rooms using BSP in one corner
|
||||
# This represents ancient ruins within the caves
|
||||
bsp = mcrfpy.BSP(pos=(GRID_WIDTH - 22, GRID_HEIGHT - 18), size=(18, 14))
|
||||
bsp.split_recursive(depth=2, min_size=(6, 5), seed=42)
|
||||
|
||||
ruins_hmap = bsp.to_heightmap(
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
select='leaves',
|
||||
shrink=1,
|
||||
value=1.0
|
||||
)
|
||||
|
||||
# Step 5: Combine caves and ruins
|
||||
combined = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
combined.copy_from(cave_mask)
|
||||
combined.max(ruins_hmap)
|
||||
|
||||
# Step 6: Add connecting tunnels from ruins to main cave
|
||||
# Find a cave entrance point
|
||||
tunnel_points = []
|
||||
for y in range(GRID_HEIGHT - 18, GRID_HEIGHT - 10):
|
||||
for x in range(GRID_WIDTH - 25, GRID_WIDTH - 20):
|
||||
if cave_mask.get((x, y)) > 0.5:
|
||||
tunnel_points.append((x, y))
|
||||
break
|
||||
if tunnel_points:
|
||||
break
|
||||
|
||||
if tunnel_points:
|
||||
tx, ty = tunnel_points[0]
|
||||
# Carve tunnel to ruins entrance
|
||||
combined.dig_bezier(
|
||||
points=((tx, ty), (tx + 3, ty), (GRID_WIDTH - 22, ty + 2), (GRID_WIDTH - 20, GRID_HEIGHT - 15)),
|
||||
start_radius=1.5, end_radius=1.5,
|
||||
start_height=1.0, end_height=1.0
|
||||
)
|
||||
|
||||
# Step 7: Add large cavern (central chamber)
|
||||
combined.add_hill((GRID_WIDTH // 3, GRID_HEIGHT // 2), 8, 0.6)
|
||||
|
||||
# Step 8: Create water pools in low noise areas
|
||||
water_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=99)
|
||||
water_map = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
water_map.add_noise(water_noise, world_size=(15, 12), mode='fbm', octaves=3)
|
||||
water_map.normalize(0.0, 1.0)
|
||||
|
||||
# Step 9: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
cave_val = combined.get((x, y))
|
||||
water_val = water_map.get((x, y))
|
||||
original_noise = cave_noise.get((x, y))
|
||||
|
||||
# Check if in ruins area
|
||||
in_ruins = (x >= GRID_WIDTH - 22 and x < GRID_WIDTH - 4 and
|
||||
y >= GRID_HEIGHT - 18 and y < GRID_HEIGHT - 4)
|
||||
|
||||
if cave_val < 0.3:
|
||||
# Solid rock
|
||||
base = 30 + int(original_noise * 25)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base + 15))
|
||||
elif cave_val < 0.5:
|
||||
# Cave wall edge
|
||||
color_layer.set(((x, y)), mcrfpy.Color(45, 40, 50))
|
||||
else:
|
||||
# Open cave floor
|
||||
if water_val > 0.7 and not in_ruins:
|
||||
# Water pool
|
||||
t = (water_val - 0.7) / 0.3
|
||||
color_layer.set(((x, y)), mcrfpy.Color(
|
||||
int(30 + t * 20), int(50 + t * 30), int(100 + t * 50)
|
||||
))
|
||||
elif in_ruins and ruins_hmap.get((x, y)) > 0.5:
|
||||
# Ruins floor (worked stone)
|
||||
base = 85 + ((x + y) % 3) * 5
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + 10, base + 5, base))
|
||||
else:
|
||||
# Natural cave floor
|
||||
base = 55 + int(original_noise * 20)
|
||||
var = ((x * 3 + y * 5) % 8)
|
||||
color_layer.set(((x, y)), mcrfpy.Color(base + var, base - 5 + var, base - 8))
|
||||
|
||||
# Glowing fungi spots
|
||||
fungi_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=777)
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
if combined.get((x, y)) > 0.5: # Only in open areas
|
||||
fungi_val = fungi_noise.get((x * 0.5, y * 0.5))
|
||||
if fungi_val > 0.8:
|
||||
color_layer.set(((x, y)), mcrfpy.Color(80, 180, 120))
|
||||
|
||||
# Border
|
||||
for x in range(GRID_WIDTH):
|
||||
color_layer.set(((x, 0)), mcrfpy.Color(20, 18, 25))
|
||||
color_layer.set(((x, GRID_HEIGHT - 1)), mcrfpy.Color(20, 18, 25))
|
||||
for y in range(GRID_HEIGHT):
|
||||
color_layer.set(((0, y)), mcrfpy.Color(20, 18, 25))
|
||||
color_layer.set(((GRID_WIDTH - 1, y)), mcrfpy.Color(20, 18, 25))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Cave System: Noise + Threshold + Kernel + BSP Ruins", 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)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("caves")
|
||||
|
||||
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_33_advanced_caves.png")
|
||||
print("Screenshot saved: procgen_33_advanced_caves.png")
|
||||
187
docs/cookbook/procgen/34_advanced_volcanic.py
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
"""Advanced: Volcanic Crater Region
|
||||
|
||||
Combines: Hills (mountains) + dig_hill (craters) + Voronoi (lava flows) + Erosion + Noise
|
||||
Creates a volcanic landscape with active lava, ash fields, and rocky terrain.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
|
||||
GRID_WIDTH, GRID_HEIGHT = 64, 48
|
||||
CELL_SIZE = 16
|
||||
|
||||
def volcanic_color(elevation, lava_intensity, ash_level):
|
||||
"""Color based on elevation, lava presence, and ash coverage."""
|
||||
# Lava overrides everything
|
||||
if lava_intensity > 0.6:
|
||||
t = (lava_intensity - 0.6) / 0.4
|
||||
return mcrfpy.Color(
|
||||
int(200 + t * 55),
|
||||
int(80 + t * 80),
|
||||
int(20 + t * 30)
|
||||
)
|
||||
elif lava_intensity > 0.4:
|
||||
# Cooling lava
|
||||
t = (lava_intensity - 0.4) / 0.2
|
||||
return mcrfpy.Color(
|
||||
int(80 + t * 120),
|
||||
int(30 + t * 50),
|
||||
int(20)
|
||||
)
|
||||
|
||||
# Check for crater interior (very low elevation)
|
||||
if elevation < 0.15:
|
||||
t = elevation / 0.15
|
||||
return mcrfpy.Color(int(40 + t * 30), int(20 + t * 20), int(10 + t * 15))
|
||||
|
||||
# Ash coverage
|
||||
if ash_level > 0.6:
|
||||
t = (ash_level - 0.6) / 0.4
|
||||
base = int(60 + t * 40)
|
||||
return mcrfpy.Color(base, base - 5, base - 10)
|
||||
|
||||
# Normal terrain by elevation
|
||||
if elevation < 0.3:
|
||||
# Volcanic plain
|
||||
t = (elevation - 0.15) / 0.15
|
||||
return mcrfpy.Color(int(50 + t * 30), int(40 + t * 25), int(35 + t * 20))
|
||||
elif elevation < 0.5:
|
||||
# Rocky slopes
|
||||
t = (elevation - 0.3) / 0.2
|
||||
return mcrfpy.Color(int(70 + t * 20), int(60 + t * 15), int(50 + t * 15))
|
||||
elif elevation < 0.7:
|
||||
# Mountain sides
|
||||
t = (elevation - 0.5) / 0.2
|
||||
return mcrfpy.Color(int(85 + t * 25), int(75 + t * 20), int(65 + t * 20))
|
||||
elif elevation < 0.85:
|
||||
# High slopes
|
||||
t = (elevation - 0.7) / 0.15
|
||||
return mcrfpy.Color(int(100 + t * 30), int(90 + t * 25), int(80 + t * 25))
|
||||
else:
|
||||
# Peaks
|
||||
t = (elevation - 0.85) / 0.15
|
||||
return mcrfpy.Color(int(130 + t * 50), int(120 + t * 50), int(115 + t * 50))
|
||||
|
||||
def run_demo(runtime):
|
||||
# Step 1: Create base terrain with noise
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, algorithm='simplex', seed=42)
|
||||
|
||||
terrain = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.3)
|
||||
terrain.add_noise(noise, world_size=(12, 10), mode='fbm', octaves=4, scale=0.2)
|
||||
|
||||
# Step 2: Add volcanic mountains
|
||||
# Main volcano
|
||||
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 20, 0.7)
|
||||
terrain.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 12, 0.3) # Steep peak
|
||||
|
||||
# Secondary volcanoes
|
||||
terrain.add_hill((15, 15), 10, 0.4)
|
||||
terrain.add_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 8, 0.35)
|
||||
terrain.add_hill((10, GRID_HEIGHT - 10), 6, 0.25)
|
||||
|
||||
# Step 3: Create craters
|
||||
terrain.dig_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 6, 0.1) # Main crater
|
||||
terrain.dig_hill((15, 15), 4, 0.15) # Secondary crater
|
||||
terrain.dig_hill((GRID_WIDTH - 12, GRID_HEIGHT - 15), 3, 0.18) # Third crater
|
||||
|
||||
# Step 4: Create lava flow pattern using voronoi
|
||||
lava = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
lava.add_voronoi(num_points=12, coefficients=(1.0, -0.8), seed=77)
|
||||
lava.normalize(0.0, 1.0)
|
||||
|
||||
# Lava originates from craters - enhance around crater centers
|
||||
lava.add_hill((GRID_WIDTH // 2, GRID_HEIGHT // 2), 8, 0.5)
|
||||
lava.add_hill((15, 15), 5, 0.3)
|
||||
|
||||
# Lava flows downhill - multiply by inverted terrain
|
||||
terrain_inv = terrain.inverse()
|
||||
terrain_inv.normalize(0.0, 1.0)
|
||||
lava.multiply(terrain_inv)
|
||||
lava.normalize(0.0, 1.0)
|
||||
|
||||
# Step 5: Create ash distribution using noise
|
||||
ash_noise = mcrfpy.NoiseSource(dimensions=2, algorithm='perlin', seed=123)
|
||||
ash = mcrfpy.HeightMap((GRID_WIDTH, GRID_HEIGHT), fill=0.0)
|
||||
ash.add_noise(ash_noise, world_size=(8, 6), mode='turbulence', octaves=3)
|
||||
ash.normalize(0.0, 1.0)
|
||||
|
||||
# Ash settles on lower areas
|
||||
ash.multiply(terrain_inv)
|
||||
|
||||
# Step 6: Apply erosion for realistic channels
|
||||
terrain.rain_erosion(drops=1500, erosion=0.1, sedimentation=0.03, seed=42)
|
||||
terrain.normalize(0.0, 1.0)
|
||||
|
||||
# Step 7: Add lava rivers from craters
|
||||
lava.dig_bezier(
|
||||
points=((GRID_WIDTH // 2, GRID_HEIGHT // 2 + 5),
|
||||
(GRID_WIDTH // 2 - 5, GRID_HEIGHT // 2 + 15),
|
||||
(GRID_WIDTH // 3, GRID_HEIGHT - 10),
|
||||
(10, GRID_HEIGHT - 5)),
|
||||
start_radius=2, end_radius=3,
|
||||
start_height=0.9, end_height=0.7
|
||||
)
|
||||
|
||||
lava.dig_bezier(
|
||||
points=((GRID_WIDTH // 2 + 3, GRID_HEIGHT // 2 + 3),
|
||||
(GRID_WIDTH // 2 + 15, GRID_HEIGHT // 2 + 8),
|
||||
(GRID_WIDTH - 15, GRID_HEIGHT // 2 + 5),
|
||||
(GRID_WIDTH - 5, GRID_HEIGHT // 2 + 10)),
|
||||
start_radius=1.5, end_radius=2.5,
|
||||
start_height=0.85, end_height=0.65
|
||||
)
|
||||
|
||||
# Step 8: Render
|
||||
for y in range(GRID_HEIGHT):
|
||||
for x in range(GRID_WIDTH):
|
||||
elev = terrain.get((x, y))
|
||||
lava_val = lava.get((x, y))
|
||||
ash_val = ash.get((x, y))
|
||||
|
||||
color_layer.set(((x, y)), volcanic_color(elev, lava_val, ash_val))
|
||||
|
||||
# Add smoke/steam particles around crater rims
|
||||
crater_centers = [
|
||||
(GRID_WIDTH // 2, GRID_HEIGHT // 2, 6),
|
||||
(15, 15, 4),
|
||||
(GRID_WIDTH - 12, GRID_HEIGHT - 15, 3)
|
||||
]
|
||||
|
||||
import math
|
||||
for cx, cy, radius in crater_centers:
|
||||
for angle in range(0, 360, 30):
|
||||
rad = math.radians(angle)
|
||||
px = int(cx + math.cos(rad) * radius)
|
||||
py = int(cy + math.sin(rad) * radius)
|
||||
if 0 <= px < GRID_WIDTH and 0 <= py < GRID_HEIGHT:
|
||||
# Smoke color
|
||||
color_layer.set(((px, py)), mcrfpy.Color(150, 140, 130, 180))
|
||||
|
||||
# Title
|
||||
title = mcrfpy.Caption(text="Volcanic Region: Hills + Craters + Voronoi Lava + Erosion", 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)
|
||||
|
||||
|
||||
# Setup
|
||||
scene = mcrfpy.Scene("volcanic")
|
||||
|
||||
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_34_advanced_volcanic.png")
|
||||
print("Screenshot saved: procgen_34_advanced_volcanic.png")
|
||||
BIN
docs/cookbook/procgen/procgen_01_heightmap_hills.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
docs/cookbook/procgen/procgen_02_heightmap_noise.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/cookbook/procgen/procgen_03_heightmap_operations.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/cookbook/procgen/procgen_04_heightmap_transforms.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/cookbook/procgen/procgen_05_heightmap_erosion.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
docs/cookbook/procgen/procgen_06_heightmap_voronoi.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
docs/cookbook/procgen/procgen_07_heightmap_bezier.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/cookbook/procgen/procgen_08_heightmap_thresholds.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/cookbook/procgen/procgen_10_bsp_dungeon.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/cookbook/procgen/procgen_11_bsp_traversal.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
docs/cookbook/procgen/procgen_12_bsp_adjacency.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
docs/cookbook/procgen/procgen_13_bsp_shrink.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/cookbook/procgen/procgen_14_bsp_manual_split.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
docs/cookbook/procgen/procgen_20_noise_algorithms.png
Normal file
|
After Width: | Height: | Size: 77 KiB |
BIN
docs/cookbook/procgen/procgen_21_noise_parameters.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
docs/cookbook/procgen/procgen_30_advanced_cave_dungeon.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
docs/cookbook/procgen/procgen_31_advanced_island.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
docs/cookbook/procgen/procgen_32_advanced_city.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs/cookbook/procgen/procgen_33_advanced_caves.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/cookbook/procgen/procgen_34_advanced_volcanic.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
83
docs/cookbook/procgen/run_all_demos.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"""Run all procgen demos and capture screenshots.
|
||||
Execute this script from the build directory.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
DEMOS = [
|
||||
"01_heightmap_hills.py",
|
||||
"02_heightmap_noise.py",
|
||||
"03_heightmap_operations.py",
|
||||
"04_heightmap_transforms.py",
|
||||
"05_heightmap_erosion.py",
|
||||
"06_heightmap_voronoi.py",
|
||||
"07_heightmap_bezier.py",
|
||||
"08_heightmap_thresholds.py",
|
||||
"10_bsp_dungeon.py",
|
||||
"11_bsp_traversal.py",
|
||||
"12_bsp_adjacency.py",
|
||||
"13_bsp_shrink.py",
|
||||
"14_bsp_manual_split.py",
|
||||
"20_noise_algorithms.py",
|
||||
"21_noise_parameters.py",
|
||||
"30_advanced_cave_dungeon.py",
|
||||
"31_advanced_island.py",
|
||||
"32_advanced_city.py",
|
||||
"33_advanced_caves.py",
|
||||
"34_advanced_volcanic.py",
|
||||
]
|
||||
|
||||
def main():
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
build_dir = os.path.abspath(os.path.join(script_dir, "../../../build"))
|
||||
|
||||
if not os.path.exists(os.path.join(build_dir, "mcrogueface")):
|
||||
print(f"Error: mcrogueface not found in {build_dir}")
|
||||
print("Please run from the build directory or adjust paths.")
|
||||
return 1
|
||||
|
||||
os.chdir(build_dir)
|
||||
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
for demo in DEMOS:
|
||||
demo_path = os.path.join(script_dir, demo)
|
||||
if not os.path.exists(demo_path):
|
||||
print(f"SKIP: {demo} (not found)")
|
||||
continue
|
||||
|
||||
print(f"Running: {demo}...", end=" ", flush=True)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["./mcrogueface", "--headless", "--exec", demo_path],
|
||||
timeout=30,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
# Check if screenshot was created
|
||||
png_name = f"procgen_{demo.replace('.py', '.png')}"
|
||||
if os.path.exists(png_name):
|
||||
print(f"OK -> {png_name}")
|
||||
success += 1
|
||||
else:
|
||||
print(f"FAIL (no screenshot)")
|
||||
if result.stderr:
|
||||
print(f" stderr: {result.stderr[:200]}")
|
||||
failed += 1
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print("TIMEOUT")
|
||||
failed += 1
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\nResults: {success} passed, {failed} failed")
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
397
docs/cookbook/tools/sprite_labeler.py
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
"""McRogueFace - Sprite Labeling Tool
|
||||
|
||||
A development tool for rapid sprite sheet labeling during game jams.
|
||||
Creates a dictionary mapping sprite indices to custom labels.
|
||||
|
||||
Usage:
|
||||
./mcrogueface docs/cookbook/tools/sprite_labeler.py
|
||||
|
||||
Console commands (while running):
|
||||
# Save labels to file
|
||||
import json; json.dump(labels, open('sprite_labels.json', 'w'), indent=2)
|
||||
|
||||
# Load labels from file
|
||||
labels.update(json.load(open('sprite_labels.json')))
|
||||
|
||||
# Print current labels
|
||||
for k, v in sorted(labels.items()): print(f"{k}: {v}")
|
||||
"""
|
||||
|
||||
import mcrfpy
|
||||
|
||||
# === Global State ===
|
||||
labels = {} # sprite_index (int) -> label (str)
|
||||
selected_label = None # Currently selected label name
|
||||
current_sprite_index = 0 # Currently hovered sprite
|
||||
|
||||
# Label categories - customize these for your game!
|
||||
DEFAULT_LABELS = [
|
||||
"player",
|
||||
"enemy",
|
||||
"wall",
|
||||
"floor",
|
||||
"door",
|
||||
"item",
|
||||
"trap",
|
||||
"decoration",
|
||||
]
|
||||
|
||||
# === Configuration ===
|
||||
# Change these to match your texture!
|
||||
TEXTURE_PATH = "assets/kenney_tinydungeon.png" # Path relative to build dir
|
||||
TILE_SIZE = 16 # Size of each tile in the texture
|
||||
GRID_COLS = 12 # Number of sprite columns in texture (texture_width / tile_size)
|
||||
GRID_ROWS = 11 # Number of sprite rows in texture (texture_height / tile_size)
|
||||
|
||||
# UI Layout
|
||||
WINDOW_WIDTH = 1024
|
||||
WINDOW_HEIGHT = 768
|
||||
GRID_X, GRID_Y = 50, 50
|
||||
GRID_WIDTH, GRID_HEIGHT = 12 * 16 * 2, 11 * 16 * 2
|
||||
PREVIEW_X, PREVIEW_Y = 480, 50
|
||||
PREVIEW_SCALE = 4.0
|
||||
PANEL_X = 480 + 180
|
||||
PANEL_Y = 50
|
||||
|
||||
# Colors
|
||||
BG_COLOR = mcrfpy.Color(30, 30, 40)
|
||||
PANEL_COLOR = mcrfpy.Color(40, 45, 55)
|
||||
BUTTON_COLOR = mcrfpy.Color(60, 65, 80)
|
||||
BUTTON_HOVER = mcrfpy.Color(80, 85, 100)
|
||||
BUTTON_SELECTED = mcrfpy.Color(80, 140, 80)
|
||||
TEXT_COLOR = mcrfpy.Color(220, 220, 230)
|
||||
LABEL_COLOR = mcrfpy.Color(100, 180, 255)
|
||||
INPUT_BG = mcrfpy.Color(25, 25, 35)
|
||||
INPUT_ACTIVE = mcrfpy.Color(35, 35, 50)
|
||||
|
||||
# === Scene Setup ===
|
||||
scene = mcrfpy.Scene("sprite_labeler")
|
||||
ui = scene.children
|
||||
|
||||
# Background
|
||||
bg = mcrfpy.Frame(pos=(0, 0), size=(WINDOW_WIDTH, WINDOW_HEIGHT))
|
||||
bg.fill_color = BG_COLOR
|
||||
ui.append(bg)
|
||||
|
||||
# Load texture
|
||||
texture = mcrfpy.Texture(TEXTURE_PATH, TILE_SIZE, TILE_SIZE)
|
||||
|
||||
# === Grid (shows all sprites) ===
|
||||
grid = mcrfpy.Grid(
|
||||
grid_size=(GRID_COLS, GRID_ROWS),
|
||||
pos=(GRID_X, GRID_Y),
|
||||
size=(GRID_WIDTH, GRID_HEIGHT),
|
||||
texture=texture,
|
||||
zoom=2.0
|
||||
)
|
||||
grid.fill_color = mcrfpy.Color(20, 20, 30)
|
||||
|
||||
# Initialize grid with all sprites
|
||||
for row in range(GRID_ROWS):
|
||||
for col in range(GRID_COLS):
|
||||
sprite_index = row * GRID_COLS + col
|
||||
cell = grid.at(col, row)
|
||||
if cell:
|
||||
cell.tilesprite = sprite_index
|
||||
|
||||
ui.append(grid)
|
||||
|
||||
# === Preview Section ===
|
||||
preview_frame = mcrfpy.Frame(pos=(PREVIEW_X, PREVIEW_Y), size=(100, 100))
|
||||
preview_frame.fill_color = PANEL_COLOR
|
||||
preview_frame.outline = 1
|
||||
preview_frame.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
ui.append(preview_frame)
|
||||
|
||||
preview_sprite = mcrfpy.Sprite((18, 18), texture, 0)
|
||||
preview_sprite.scale = PREVIEW_SCALE
|
||||
preview_frame.children.append(preview_sprite)
|
||||
|
||||
# Index caption
|
||||
index_caption = mcrfpy.Caption(pos=(PREVIEW_X, PREVIEW_Y + 110), text="Index: 0")
|
||||
index_caption.font_size = 18
|
||||
index_caption.fill_color = TEXT_COLOR
|
||||
ui.append(index_caption)
|
||||
|
||||
# Label display (shows current label for hovered sprite)
|
||||
label_display = mcrfpy.Caption(pos=(PREVIEW_X, PREVIEW_Y + 135), text="Label: (none)")
|
||||
label_display.font_size = 16
|
||||
label_display.fill_color = LABEL_COLOR
|
||||
ui.append(label_display)
|
||||
|
||||
# === Input Section (for adding new labels) ===
|
||||
input_panel = mcrfpy.Frame(pos=(PANEL_X, PANEL_Y), size=(300, 80))
|
||||
input_panel.fill_color = PANEL_COLOR
|
||||
input_panel.outline = 1
|
||||
input_panel.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
ui.append(input_panel)
|
||||
|
||||
input_title = mcrfpy.Caption(pos=(10, 8), text="Add New Label:")
|
||||
input_title.font_size = 14
|
||||
input_title.fill_color = TEXT_COLOR
|
||||
input_panel.children.append(input_title)
|
||||
|
||||
# Text input field (frame + caption)
|
||||
input_field = mcrfpy.Frame(pos=(10, 35), size=(200, 30))
|
||||
input_field.fill_color = INPUT_BG
|
||||
input_field.outline = 1
|
||||
input_field.outline_color = mcrfpy.Color(60, 60, 80)
|
||||
input_panel.children.append(input_field)
|
||||
|
||||
input_text = mcrfpy.Caption(pos=(8, 6), text="")
|
||||
input_text.font_size = 14
|
||||
input_text.fill_color = TEXT_COLOR
|
||||
input_field.children.append(input_text)
|
||||
|
||||
# Text input state
|
||||
input_buffer = ""
|
||||
input_active = False
|
||||
|
||||
# Submit button
|
||||
submit_btn = mcrfpy.Frame(pos=(220, 35), size=(70, 30))
|
||||
submit_btn.fill_color = BUTTON_COLOR
|
||||
submit_btn.outline = 1
|
||||
submit_btn.outline_color = mcrfpy.Color(100, 100, 120)
|
||||
input_panel.children.append(submit_btn)
|
||||
|
||||
submit_text = mcrfpy.Caption(pos=(12, 6), text="Add")
|
||||
submit_text.font_size = 14
|
||||
submit_text.fill_color = TEXT_COLOR
|
||||
submit_btn.children.append(submit_text)
|
||||
|
||||
# === Label Selection Panel ===
|
||||
labels_panel = mcrfpy.Frame(pos=(PANEL_X, PANEL_Y + 100), size=(300, 350))
|
||||
labels_panel.fill_color = PANEL_COLOR
|
||||
labels_panel.outline = 1
|
||||
labels_panel.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
ui.append(labels_panel)
|
||||
|
||||
labels_title = mcrfpy.Caption(pos=(10, 8), text="Select Label (click to choose):")
|
||||
labels_title.font_size = 14
|
||||
labels_title.fill_color = TEXT_COLOR
|
||||
labels_panel.children.append(labels_title)
|
||||
|
||||
# Store label button references for updating selection highlight
|
||||
label_buttons = [] # list of (frame, caption, label_name)
|
||||
|
||||
def create_label_button(label_name, index):
|
||||
"""Create a clickable label button"""
|
||||
row = index // 2
|
||||
col = index % 2
|
||||
btn_x = 10 + col * 145
|
||||
btn_y = 35 + row * 40
|
||||
|
||||
btn = mcrfpy.Frame(pos=(btn_x, btn_y), size=(135, 32))
|
||||
btn.fill_color = BUTTON_COLOR
|
||||
btn.outline = 1
|
||||
btn.outline_color = mcrfpy.Color(80, 80, 100)
|
||||
|
||||
btn_caption = mcrfpy.Caption(pos=(8, 7), text=label_name[:14])
|
||||
btn_caption.font_size = 12
|
||||
btn_caption.fill_color = TEXT_COLOR
|
||||
btn.children.append(btn_caption)
|
||||
|
||||
labels_panel.children.append(btn)
|
||||
label_buttons.append((btn, btn_caption, label_name))
|
||||
|
||||
# Set click handler
|
||||
def on_label_click(x, y, button):
|
||||
select_label(label_name)
|
||||
#return True
|
||||
btn.on_click = on_label_click
|
||||
|
||||
return btn
|
||||
|
||||
def select_label(label_name):
|
||||
"""Select a label for applying to sprites"""
|
||||
global selected_label
|
||||
selected_label = label_name
|
||||
|
||||
# Update button colors
|
||||
for btn, caption, name in label_buttons:
|
||||
if name == selected_label:
|
||||
btn.fill_color = BUTTON_SELECTED
|
||||
else:
|
||||
btn.fill_color = BUTTON_COLOR
|
||||
|
||||
def add_new_label(label_name):
|
||||
"""Add a new label to the selection list"""
|
||||
global input_buffer
|
||||
|
||||
# Don't add duplicates or empty labels
|
||||
label_name = label_name.strip()
|
||||
if not label_name:
|
||||
return
|
||||
|
||||
for _, _, existing in label_buttons:
|
||||
if existing == label_name:
|
||||
return
|
||||
|
||||
# Create button for new label
|
||||
create_label_button(label_name, len(label_buttons))
|
||||
|
||||
# Clear input
|
||||
input_buffer = ""
|
||||
input_text.text = ""
|
||||
|
||||
# Select the new label
|
||||
select_label(label_name)
|
||||
|
||||
# Initialize default labels
|
||||
for i, label in enumerate(DEFAULT_LABELS):
|
||||
create_label_button(label, i)
|
||||
|
||||
# Select first label by default
|
||||
if DEFAULT_LABELS:
|
||||
select_label(DEFAULT_LABELS[0])
|
||||
|
||||
# === Event Handlers ===
|
||||
|
||||
def on_cell_enter(cell):
|
||||
"""Handle mouse hovering over grid cells"""
|
||||
global current_sprite_index
|
||||
|
||||
x, y = int(cell[0]), int(cell[1])
|
||||
sprite_index = y * GRID_COLS + x
|
||||
current_sprite_index = sprite_index
|
||||
|
||||
# Update preview
|
||||
preview_sprite.sprite_index = sprite_index
|
||||
index_caption.text = f"Index: {sprite_index}"
|
||||
|
||||
# Update label display
|
||||
if sprite_index in labels:
|
||||
label_display.text = f"Label: {labels[sprite_index]}"
|
||||
label_display.fill_color = BUTTON_SELECTED
|
||||
else:
|
||||
label_display.text = "Label: (none)"
|
||||
label_display.fill_color = LABEL_COLOR
|
||||
|
||||
def on_cell_click(cell):
|
||||
"""Handle clicking on grid cells to apply labels"""
|
||||
global labels
|
||||
|
||||
x, y = int(cell[0]), int(cell[1])
|
||||
sprite_index = y * GRID_COLS + x
|
||||
|
||||
if selected_label:
|
||||
labels[sprite_index] = selected_label
|
||||
print(f"Labeled sprite {sprite_index} as '{selected_label}'")
|
||||
|
||||
# Update display if this is the current sprite
|
||||
if sprite_index == current_sprite_index:
|
||||
label_display.text = f"Label: {selected_label}"
|
||||
label_display.fill_color = BUTTON_SELECTED
|
||||
|
||||
grid.on_cell_enter = on_cell_enter
|
||||
grid.on_cell_click = on_cell_click
|
||||
|
||||
# Submit button click handler
|
||||
def on_submit_click(x, y, button):
|
||||
"""Handle submit button click"""
|
||||
add_new_label(input_buffer)
|
||||
#return True
|
||||
|
||||
submit_btn.on_click = on_submit_click
|
||||
|
||||
# Input field click handler (activate text input)
|
||||
def on_input_click(x, y, button):
|
||||
"""Handle input field click to activate typing"""
|
||||
global input_active
|
||||
input_active = True
|
||||
input_field.fill_color = INPUT_ACTIVE
|
||||
#return True
|
||||
|
||||
input_field.on_click = on_input_click
|
||||
|
||||
# === Keyboard Handler ===
|
||||
|
||||
def on_keypress(key, state):
|
||||
"""Handle keyboard input for text entry"""
|
||||
global input_buffer, input_active
|
||||
|
||||
if state != "start":
|
||||
return
|
||||
|
||||
# Escape clears input focus
|
||||
if key == "Escape":
|
||||
input_active = False
|
||||
input_field.fill_color = INPUT_BG
|
||||
return
|
||||
|
||||
# Enter submits the label
|
||||
if key == "Return":
|
||||
if input_active and input_buffer:
|
||||
add_new_label(input_buffer)
|
||||
input_active = False
|
||||
input_field.fill_color = INPUT_BG
|
||||
return
|
||||
|
||||
# Only process text input when field is active
|
||||
if not input_active:
|
||||
return
|
||||
|
||||
# Backspace
|
||||
if key == "BackSpace":
|
||||
if input_buffer:
|
||||
input_buffer = input_buffer[:-1]
|
||||
input_text.text = input_buffer
|
||||
return
|
||||
|
||||
# Handle alphanumeric and common characters
|
||||
# Map key names to characters
|
||||
if len(key) == 1 and key.isalpha():
|
||||
input_buffer += key.lower()
|
||||
input_text.text = input_buffer
|
||||
elif key.startswith("Num") and len(key) == 4:
|
||||
# Numpad numbers
|
||||
input_buffer += key[3]
|
||||
input_text.text = input_buffer
|
||||
elif key == "Space":
|
||||
input_buffer += " "
|
||||
input_text.text = input_buffer
|
||||
elif key == "Minus":
|
||||
input_buffer += "-"
|
||||
input_text.text = input_buffer
|
||||
elif key == "Period":
|
||||
input_buffer += "."
|
||||
input_text.text = input_buffer
|
||||
|
||||
scene.on_key = on_keypress
|
||||
|
||||
# === Instructions Caption ===
|
||||
#instructions = mcrfpy.Caption(
|
||||
# pos=(GRID_X, GRID_Y + GRID_HEIGHT + 20),
|
||||
# text="Hover: preview sprite | Click grid: apply selected label | Type: add new labels"
|
||||
#)
|
||||
#instructions.font_size = 12
|
||||
#instructions.fill_color = mcrfpy.Color(150, 150, 160)
|
||||
#ui.append(instructions)
|
||||
#
|
||||
#instructions2 = mcrfpy.Caption(
|
||||
# pos=(GRID_X, GRID_Y + GRID_HEIGHT + 40),
|
||||
# text="Console: labels (dict), json.dump(labels, open('labels.json','w'))"
|
||||
#)
|
||||
#instructions2.font_size = 12
|
||||
#instructions2.fill_color = mcrfpy.Color(150, 150, 160)
|
||||
#ui.append(instructions2)
|
||||
|
||||
# Activate the scene
|
||||
scene.activate()
|
||||
|
||||
#print("=== Sprite Labeler Tool ===")
|
||||
#print(f"Texture: {TEXTURE_PATH}")
|
||||
#print(f"Grid: {GRID_COLS}x{GRID_ROWS} = {GRID_COLS * GRID_ROWS} sprites")
|
||||
#print("")
|
||||
#print("Usage:")
|
||||
#print(" - Hover over grid to preview sprites")
|
||||
#print(" - Click a label button to select it")
|
||||
#print(" - Click on grid cells to apply the selected label")
|
||||
#print(" - Type in the text field to add new labels")
|
||||
#print("")
|
||||
#print("Console commands:")
|
||||
#print(" labels # View all labels")
|
||||
#print(" labels[42] = 'custom' # Manual labeling")
|
||||
#print(" import json")
|
||||
#print(" json.dump(labels, open('sprite_labels.json', 'w'), indent=2) # Save")
|
||||
#print(" labels.update(json.load(open('sprite_labels.json'))) # Load")
|
||||