Tiled XML/JSON import support
This commit is contained in:
parent
71cd2b9b41
commit
b093e087e1
18 changed files with 3040 additions and 0 deletions
167
tests/demo/screens/tiled_analysis.py
Normal file
167
tests/demo/screens/tiled_analysis.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# tiled_analysis.py - Wang set adjacency analysis utility
|
||||
# Prints adjacency graph, terrain chains, and valid/invalid pair counts
|
||||
# for exploring tileset Wang transition rules.
|
||||
#
|
||||
# Usage:
|
||||
# cd build && ./mcrogueface --headless --exec ../tests/demo/screens/tiled_analysis.py
|
||||
|
||||
import mcrfpy
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
|
||||
# -- Configuration --------------------------------------------------------
|
||||
PUNY_BASE = "/home/john/Development/7DRL2026_Liber_Noster_jmccardle/assets_sources/PUNY_WORLD_v1/PUNY_WORLD_v1"
|
||||
TSX_PATH = PUNY_BASE + "/Tiled/punyworld-overworld-tiles.tsx"
|
||||
WANG_SET_NAME = "overworld"
|
||||
|
||||
|
||||
def analyze_wang_set(tileset, wang_set_name):
|
||||
"""Analyze a Wang set and print adjacency information."""
|
||||
ws = tileset.wang_set(wang_set_name)
|
||||
T = ws.terrain_enum()
|
||||
|
||||
print("=" * 60)
|
||||
print(f"Wang Set Analysis: {ws.name}")
|
||||
print(f" Type: {ws.type}")
|
||||
print(f" Colors: {ws.color_count}")
|
||||
print("=" * 60)
|
||||
|
||||
# List all terrains
|
||||
terrains = [t for t in T if t != T.NONE]
|
||||
print(f"\nTerrains ({len(terrains)}):")
|
||||
for t in terrains:
|
||||
print(f" {t.value:3d}: {t.name}")
|
||||
|
||||
# Test all pairs for valid Wang transitions using 2x2 grids
|
||||
adjacency = defaultdict(set) # terrain -> set of valid neighbors
|
||||
valid_pairs = []
|
||||
invalid_pairs = []
|
||||
|
||||
for a in terrains:
|
||||
for b in terrains:
|
||||
if b.value <= a.value:
|
||||
continue
|
||||
# Create a 2x2 map: columns of A and B
|
||||
dm = mcrfpy.DiscreteMap((2, 2))
|
||||
dm.set(0, 0, a)
|
||||
dm.set(1, 0, b)
|
||||
dm.set(0, 1, a)
|
||||
dm.set(1, 1, b)
|
||||
results = ws.resolve(dm)
|
||||
has_invalid = any(r == -1 for r in results)
|
||||
if not has_invalid:
|
||||
valid_pairs.append((a, b))
|
||||
adjacency[a.name].add(b.name)
|
||||
adjacency[b.name].add(a.name)
|
||||
else:
|
||||
invalid_pairs.append((a, b))
|
||||
|
||||
# Print adjacency graph
|
||||
print(f"\nAdjacency Graph ({len(valid_pairs)} valid pairs):")
|
||||
print("-" * 40)
|
||||
for t in terrains:
|
||||
neighbors = sorted(adjacency.get(t.name, set()))
|
||||
if neighbors:
|
||||
print(f" {t.name}")
|
||||
for n in neighbors:
|
||||
print(f" <-> {n}")
|
||||
else:
|
||||
print(f" {t.name} (ISOLATED - no valid neighbors)")
|
||||
|
||||
# Find terrain chains (connected components)
|
||||
print(f"\nTerrain Chains (connected components):")
|
||||
print("-" * 40)
|
||||
visited = set()
|
||||
chains = []
|
||||
|
||||
def bfs(start):
|
||||
chain = []
|
||||
queue = [start]
|
||||
while queue:
|
||||
node = queue.pop(0)
|
||||
if node in visited:
|
||||
continue
|
||||
visited.add(node)
|
||||
chain.append(node)
|
||||
for neighbor in sorted(adjacency.get(node, set())):
|
||||
if neighbor not in visited:
|
||||
queue.append(neighbor)
|
||||
return chain
|
||||
|
||||
for t in terrains:
|
||||
if t.name not in visited:
|
||||
chain = bfs(t.name)
|
||||
if chain:
|
||||
chains.append(chain)
|
||||
|
||||
for i, chain in enumerate(chains):
|
||||
print(f"\n Chain {i+1}: {len(chain)} terrains")
|
||||
for name in chain:
|
||||
neighbors = sorted(adjacency.get(name, set()))
|
||||
connections = ", ".join(neighbors) if neighbors else "(none)"
|
||||
print(f" {name} -> [{connections}]")
|
||||
|
||||
# Find linear paths within chains
|
||||
print(f"\nLinear Paths (degree-1 endpoints to degree-1 endpoints):")
|
||||
print("-" * 40)
|
||||
for chain in chains:
|
||||
# Find nodes with degree 1 (endpoints) or degree > 2 (hubs)
|
||||
endpoints = [n for n in chain if len(adjacency.get(n, set())) == 1]
|
||||
hubs = [n for n in chain if len(adjacency.get(n, set())) > 2]
|
||||
|
||||
if endpoints:
|
||||
print(f" Endpoints: {', '.join(endpoints)}")
|
||||
if hubs:
|
||||
print(f" Hubs: {', '.join(hubs)} (branch points)")
|
||||
|
||||
# Trace from each endpoint
|
||||
for ep in endpoints:
|
||||
path = [ep]
|
||||
current = ep
|
||||
prev = None
|
||||
while True:
|
||||
neighbors = adjacency.get(current, set()) - {prev} if prev else adjacency.get(current, set())
|
||||
if len(neighbors) == 0:
|
||||
break
|
||||
if len(neighbors) > 1:
|
||||
path.append(f"({current} branches)")
|
||||
break
|
||||
nxt = list(neighbors)[0]
|
||||
path.append(nxt)
|
||||
prev = current
|
||||
current = nxt
|
||||
if len(adjacency.get(current, set())) != 2:
|
||||
break # reached endpoint or hub
|
||||
print(f" Path: {' -> '.join(path)}")
|
||||
|
||||
# Summary statistics
|
||||
total_possible = len(terrains) * (len(terrains) - 1) // 2
|
||||
print(f"\nSummary:")
|
||||
print(f" Total terrain types: {len(terrains)}")
|
||||
print(f" Valid transitions: {len(valid_pairs)} / {total_possible} "
|
||||
f"({100*len(valid_pairs)/total_possible:.1f}%)")
|
||||
print(f" Invalid transitions: {len(invalid_pairs)}")
|
||||
print(f" Connected components: {len(chains)}")
|
||||
|
||||
# Print invalid pairs for reference
|
||||
if invalid_pairs:
|
||||
print(f"\nInvalid Pairs ({len(invalid_pairs)}):")
|
||||
for a, b in invalid_pairs:
|
||||
print(f" {a.name} X {b.name}")
|
||||
|
||||
return valid_pairs, invalid_pairs, chains
|
||||
|
||||
|
||||
def main():
|
||||
print("Loading tileset...")
|
||||
tileset = mcrfpy.TileSetFile(TSX_PATH)
|
||||
print(f" {tileset.name}: {tileset.tile_count} tiles "
|
||||
f"({tileset.columns} cols, {tileset.tile_width}x{tileset.tile_height}px)")
|
||||
|
||||
analyze_wang_set(tileset, WANG_SET_NAME)
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
sys.exit(0)
|
||||
504
tests/demo/screens/tiled_demo.py
Normal file
504
tests/demo/screens/tiled_demo.py
Normal file
|
|
@ -0,0 +1,504 @@
|
|||
# tiled_demo.py - Visual demo of Tiled integration
|
||||
# Shows premade maps, Wang auto-tiling, and procgen terrain
|
||||
#
|
||||
# Usage:
|
||||
# Headless: cd build && ./mcrogueface --headless --exec ../tests/demo/screens/tiled_demo.py
|
||||
# Interactive: cd build && ./mcrogueface --exec ../tests/demo/screens/tiled_demo.py
|
||||
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
# -- Asset Paths -------------------------------------------------------
|
||||
PUNY_BASE = "/home/john/Development/7DRL2026_Liber_Noster_jmccardle/assets_sources/PUNY_WORLD_v1/PUNY_WORLD_v1"
|
||||
TSX_PATH = PUNY_BASE + "/Tiled/punyworld-overworld-tiles.tsx"
|
||||
|
||||
# -- Load Shared Assets ------------------------------------------------
|
||||
print("Loading Puny World tileset...")
|
||||
tileset = mcrfpy.TileSetFile(TSX_PATH)
|
||||
texture = tileset.to_texture()
|
||||
overworld_ws = tileset.wang_set("overworld")
|
||||
Terrain = overworld_ws.terrain_enum()
|
||||
|
||||
print(f" Tileset: {tileset.name}")
|
||||
print(f" Tiles: {tileset.tile_count} ({tileset.columns} cols, {tileset.tile_width}x{tileset.tile_height}px)")
|
||||
print(f" Wang set: {overworld_ws.name} ({overworld_ws.type}, {overworld_ws.color_count} colors)")
|
||||
print(f" Terrain enum members: {[t.name for t in Terrain]}")
|
||||
|
||||
# -- Helper: Iterative terrain expansion ----------------------------------
|
||||
def iterative_terrain(hm, wang_set, width, height, passes):
|
||||
"""Build a DiscreteMap by iteratively splitting terrains outward from
|
||||
a valid binary map. Each pass splits one terrain on each end of the
|
||||
chain, validates with wang_set.resolve(), and reverts invalid cells
|
||||
to their previous value.
|
||||
|
||||
hm: HeightMap (normalized 0-1)
|
||||
passes: list of (threshold, lo_old, lo_new, hi_old, hi_new) tuples.
|
||||
Each pass says: cells currently == lo_old with height < threshold
|
||||
become lo_new; cells currently == hi_old with height >= threshold
|
||||
become hi_new.
|
||||
|
||||
Returns (DiscreteMap, stats_dict).
|
||||
"""
|
||||
dm = mcrfpy.DiscreteMap((width, height))
|
||||
|
||||
# Pass 0: binary split - everything is one of two terrains
|
||||
p0 = passes[0]
|
||||
thresh, lo_terrain, hi_terrain = p0
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if hm.get(x, y) < thresh:
|
||||
dm.set(x, y, int(lo_terrain))
|
||||
else:
|
||||
dm.set(x, y, int(hi_terrain))
|
||||
|
||||
# Validate pass 0 and fix any invalid cells (rare edge cases like
|
||||
# checkerboard patterns at the binary boundary)
|
||||
results = wang_set.resolve(dm)
|
||||
inv = sum(1 for r in results if r == -1)
|
||||
if inv > 0:
|
||||
# Fix by flipping invalid cells to the other terrain
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if results[y * width + x] == -1:
|
||||
val = dm.get(x, y)
|
||||
if val == int(lo_terrain):
|
||||
dm.set(x, y, int(hi_terrain))
|
||||
else:
|
||||
dm.set(x, y, int(lo_terrain))
|
||||
results2 = wang_set.resolve(dm)
|
||||
inv = sum(1 for r in results2 if r == -1)
|
||||
stats = {"pass0_invalid": inv}
|
||||
|
||||
# Subsequent passes: split outward
|
||||
for pi, (thresh, lo_old, lo_new, hi_old, hi_new) in enumerate(passes[1:], 1):
|
||||
# Save current state so we can revert invalid cells
|
||||
prev = [dm.get(x, y) for y in range(height) for x in range(width)]
|
||||
|
||||
# Track which cells were changed this pass
|
||||
changed = set()
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
val = dm.get(x, y)
|
||||
h = hm.get(x, y)
|
||||
if val == int(lo_old) and h < thresh:
|
||||
dm.set(x, y, int(lo_new))
|
||||
changed.add((x, y))
|
||||
elif val == int(hi_old) and h >= thresh:
|
||||
dm.set(x, y, int(hi_new))
|
||||
changed.add((x, y))
|
||||
|
||||
# Iteratively revert changed cells that cause invalid tiles.
|
||||
# A changed cell should be reverted if:
|
||||
# - It is itself invalid, OR
|
||||
# - It is a neighbor of an invalid UN-changed cell (it broke
|
||||
# a pre-existing valid cell by being placed next to it)
|
||||
dirs8 = [(-1,-1),(0,-1),(1,-1),(-1,0),(1,0),(-1,1),(0,1),(1,1)]
|
||||
total_reverted = 0
|
||||
for revert_round in range(30):
|
||||
results = wang_set.resolve(dm)
|
||||
to_revert = set()
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
if results[y * width + x] != -1:
|
||||
continue
|
||||
if (x, y) in changed:
|
||||
# This changed cell is invalid - revert it
|
||||
to_revert.add((x, y))
|
||||
else:
|
||||
# Pre-existing cell is now invalid - revert its
|
||||
# changed neighbors to restore it
|
||||
for dx, dy in dirs8:
|
||||
nx, ny = x+dx, y+dy
|
||||
if (nx, ny) in changed:
|
||||
to_revert.add((nx, ny))
|
||||
|
||||
if not to_revert:
|
||||
break
|
||||
|
||||
for (x, y) in to_revert:
|
||||
dm.set(x, y, prev[y * width + x])
|
||||
changed.discard((x, y))
|
||||
total_reverted += 1
|
||||
|
||||
results_final = wang_set.resolve(dm)
|
||||
remaining = sum(1 for r in results_final if r == -1)
|
||||
stats[f"pass{pi}_kept"] = len(changed)
|
||||
stats[f"pass{pi}_reverted"] = total_reverted
|
||||
stats[f"pass{pi}_remaining"] = remaining
|
||||
|
||||
return dm, stats
|
||||
|
||||
|
||||
# -- Helper: Info Panel -------------------------------------------------
|
||||
def make_info_panel(scene, lines, x=560, y=60, w=220, h=None):
|
||||
"""Create a semi-transparent info panel with text lines."""
|
||||
if h is None:
|
||||
h = len(lines) * 22 + 20
|
||||
panel = mcrfpy.Frame(pos=(x, y), size=(w, h),
|
||||
fill_color=mcrfpy.Color(20, 20, 30, 220),
|
||||
outline_color=mcrfpy.Color(80, 80, 120),
|
||||
outline=1.5)
|
||||
scene.children.append(panel)
|
||||
for i, text in enumerate(lines):
|
||||
cap = mcrfpy.Caption(text=text, pos=(10, 10 + i * 22))
|
||||
cap.fill_color = mcrfpy.Color(200, 200, 220)
|
||||
panel.children.append(cap)
|
||||
return panel
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# SCREEN 1: Premade Tiled Map
|
||||
# ======================================================================
|
||||
print("\nSetting up Screen 1: Premade Map...")
|
||||
scene1 = mcrfpy.Scene("tiled_premade")
|
||||
|
||||
bg1 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
|
||||
scene1.children.append(bg1)
|
||||
|
||||
title1 = mcrfpy.Caption(text="Premade Tiled Map (50x50, 3 layers)", pos=(20, 10))
|
||||
title1.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
scene1.children.append(title1)
|
||||
|
||||
# Load samplemap1
|
||||
tm1 = mcrfpy.TileMapFile(PUNY_BASE + "/Tiled/samplemap1.tmj")
|
||||
print(f" Map: {tm1.width}x{tm1.height}, layers: {tm1.tile_layer_names}")
|
||||
|
||||
grid1 = mcrfpy.Grid(grid_size=(tm1.width, tm1.height),
|
||||
pos=(20, 50), size=(520, 520), layers=[])
|
||||
grid1.fill_color = mcrfpy.Color(30, 30, 50)
|
||||
|
||||
# Add a tile layer for each map layer, bottom-up z ordering
|
||||
layer_names_1 = tm1.tile_layer_names
|
||||
for i, name in enumerate(layer_names_1):
|
||||
z = -(len(layer_names_1) - i)
|
||||
layer = mcrfpy.TileLayer(name=name, z_index=z, texture=texture)
|
||||
grid1.add_layer(layer)
|
||||
tm1.apply_to_tile_layer(layer, name, tileset_index=0)
|
||||
print(f" Applied layer '{name}' (z_index={z})")
|
||||
|
||||
# Center camera on map center (pixels = tiles * tile_size)
|
||||
grid1.center = (tm1.width * tileset.tile_width // 2,
|
||||
tm1.height * tileset.tile_height // 2)
|
||||
scene1.children.append(grid1)
|
||||
|
||||
make_info_panel(scene1, [
|
||||
f"Tileset: {tileset.name}",
|
||||
f"Tile size: {tileset.tile_width}x{tileset.tile_height}",
|
||||
f"Tile count: {tileset.tile_count}",
|
||||
f"Map size: {tm1.width}x{tm1.height}",
|
||||
"",
|
||||
"Layers:",
|
||||
] + [f" {name}" for name in layer_names_1] + [
|
||||
"",
|
||||
"Wang sets:",
|
||||
f" {overworld_ws.name} ({overworld_ws.type})",
|
||||
f" pathways (edge)",
|
||||
])
|
||||
|
||||
nav1 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
|
||||
nav1.fill_color = mcrfpy.Color(120, 120, 150)
|
||||
scene1.children.append(nav1)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# SCREEN 2: Procedural Wang Auto-Tile (2-layer approach)
|
||||
# ======================================================================
|
||||
print("\nSetting up Screen 2: Procgen Wang Terrain (2-layer)...")
|
||||
scene2 = mcrfpy.Scene("tiled_procgen")
|
||||
|
||||
bg2 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
|
||||
scene2.children.append(bg2)
|
||||
|
||||
title2 = mcrfpy.Caption(text="Procgen Wang Auto-Tile (60x60, 2 layers)", pos=(20, 10))
|
||||
title2.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
scene2.children.append(title2)
|
||||
|
||||
W, H = 60, 60
|
||||
T = Terrain # shorthand
|
||||
|
||||
# Generate terrain heightmap using NoiseSource
|
||||
noise = mcrfpy.NoiseSource(dimensions=2, seed=42)
|
||||
hm = noise.sample(size=(W, H), mode="fbm", octaves=4, world_size=(4.0, 4.0))
|
||||
hm.normalize(0.0, 1.0)
|
||||
|
||||
# -- Base terrain: iterative expansion from binary map --
|
||||
# Pass 0: binary split at median -> SEAWATER_LIGHT / SAND
|
||||
# Pass 1: split outward -> SEAWATER_MEDIUM from LIGHT, GRASS from SAND
|
||||
# Pass 2: split outward -> SEAWATER_DEEP from MEDIUM, CLIFF from GRASS
|
||||
base_passes = [
|
||||
# Pass 0: (threshold, lo_terrain, hi_terrain)
|
||||
(0.45, T.SEAWATER_LIGHT, T.SAND),
|
||||
# Pass 1+: (threshold, lo_old, lo_new, hi_old, hi_new)
|
||||
(0.30, T.SEAWATER_LIGHT, T.SEAWATER_MEDIUM, T.SAND, T.GRASS),
|
||||
(0.20, T.SEAWATER_MEDIUM, T.SEAWATER_DEEP, T.GRASS, T.CLIFF),
|
||||
]
|
||||
base_dm, base_stats = iterative_terrain(hm, overworld_ws, W, H, base_passes)
|
||||
base_dm.enum_type = T
|
||||
print(f" Base terrain stats: {base_stats}")
|
||||
|
||||
# -- Tree overlay: separate noise, binary TREES/AIR --
|
||||
tree_noise = mcrfpy.NoiseSource(dimensions=2, seed=999)
|
||||
tree_hm = tree_noise.sample(size=(W, H), mode="fbm", octaves=3, world_size=(6.0, 6.0))
|
||||
tree_hm.normalize(0.0, 1.0)
|
||||
|
||||
overlay_dm = mcrfpy.DiscreteMap((W, H))
|
||||
overlay_dm.enum_type = T
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
base_val = base_dm.get(x, y)
|
||||
tree_h = tree_hm.get(x, y)
|
||||
# Trees only on GRASS, driven by separate noise
|
||||
if base_val == int(T.GRASS) and tree_h > 0.45:
|
||||
overlay_dm.set(x, y, int(T.TREES))
|
||||
else:
|
||||
overlay_dm.set(x, y, int(T.AIR))
|
||||
|
||||
# Validate overlay and revert invalid to AIR
|
||||
overlay_results = overworld_ws.resolve(overlay_dm)
|
||||
overlay_reverted = 0
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
if overlay_results[y * W + x] == -1:
|
||||
overlay_dm.set(x, y, int(T.AIR))
|
||||
overlay_reverted += 1
|
||||
print(f" Overlay: {overlay_reverted} tree cells reverted to AIR")
|
||||
|
||||
# Count terrain distribution
|
||||
terrain_counts = {}
|
||||
for t in T:
|
||||
if t == T.NONE:
|
||||
continue
|
||||
c = base_dm.count(int(t))
|
||||
if c > 0:
|
||||
terrain_counts[t.name] = c
|
||||
tree_count = overlay_dm.count(int(T.TREES))
|
||||
terrain_counts["TREES(overlay)"] = tree_count
|
||||
|
||||
print(f" Terrain distribution: {terrain_counts}")
|
||||
|
||||
# Create grid with 2 layers and apply Wang auto-tiling
|
||||
grid2 = mcrfpy.Grid(grid_size=(W, H), pos=(20, 50), size=(520, 520), layers=[])
|
||||
grid2.fill_color = mcrfpy.Color(30, 30, 50)
|
||||
|
||||
base_layer2 = mcrfpy.TileLayer(name="base", z_index=-2, texture=texture)
|
||||
grid2.add_layer(base_layer2)
|
||||
overworld_ws.apply(base_dm, base_layer2)
|
||||
|
||||
overlay_layer2 = mcrfpy.TileLayer(name="trees", z_index=-1, texture=texture)
|
||||
grid2.add_layer(overlay_layer2)
|
||||
overworld_ws.apply(overlay_dm, overlay_layer2)
|
||||
|
||||
# Post-process overlay: AIR resolves to an opaque tile, set to -1 (transparent)
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
if overlay_dm.get(x, y) == int(T.AIR):
|
||||
overlay_layer2.set((x, y), -1)
|
||||
|
||||
grid2.center = (W * tileset.tile_width // 2, H * tileset.tile_height // 2)
|
||||
scene2.children.append(grid2)
|
||||
|
||||
# Info panel
|
||||
info_lines = [
|
||||
"Iterative terrain expansion",
|
||||
f"Seed: 42 (base), 999 (trees)",
|
||||
f"Grid: {W}x{H}, 2 layers",
|
||||
"",
|
||||
"Base (3 passes):",
|
||||
]
|
||||
for name in ["SEAWATER_DEEP", "SEAWATER_MEDIUM", "SEAWATER_LIGHT",
|
||||
"SAND", "GRASS", "CLIFF"]:
|
||||
count = terrain_counts.get(name, 0)
|
||||
info_lines.append(f" {name}: {count}")
|
||||
info_lines.append("")
|
||||
info_lines.append("Tree Overlay:")
|
||||
info_lines.append(f" TREES: {tree_count}")
|
||||
info_lines.append(f" reverted: {overlay_reverted}")
|
||||
|
||||
make_info_panel(scene2, info_lines)
|
||||
|
||||
nav2 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
|
||||
nav2.fill_color = mcrfpy.Color(120, 120, 150)
|
||||
scene2.children.append(nav2)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# SCREEN 3: Side-by-Side Comparison
|
||||
# ======================================================================
|
||||
print("\nSetting up Screen 3: Side-by-Side...")
|
||||
scene3 = mcrfpy.Scene("tiled_compare")
|
||||
|
||||
bg3 = mcrfpy.Frame(pos=(0, 0), size=(1024, 768), fill_color=mcrfpy.Color(10, 10, 15))
|
||||
scene3.children.append(bg3)
|
||||
|
||||
title3 = mcrfpy.Caption(text="Premade vs Procedural", pos=(20, 10))
|
||||
title3.fill_color = mcrfpy.Color(255, 255, 255)
|
||||
scene3.children.append(title3)
|
||||
|
||||
# Left: Premade map (samplemap2, 30x30)
|
||||
tm2 = mcrfpy.TileMapFile(PUNY_BASE + "/Tiled/samplemap2.tmj")
|
||||
print(f" Map2: {tm2.width}x{tm2.height}, layers: {tm2.tile_layer_names}")
|
||||
|
||||
left_label = mcrfpy.Caption(text="Premade (samplemap2)", pos=(20, 38))
|
||||
left_label.fill_color = mcrfpy.Color(180, 220, 255)
|
||||
scene3.children.append(left_label)
|
||||
|
||||
grid_left = mcrfpy.Grid(grid_size=(tm2.width, tm2.height),
|
||||
pos=(20, 60), size=(380, 380), layers=[])
|
||||
grid_left.fill_color = mcrfpy.Color(30, 30, 50)
|
||||
|
||||
for i, name in enumerate(tm2.tile_layer_names):
|
||||
z = -(len(tm2.tile_layer_names) - i)
|
||||
layer = mcrfpy.TileLayer(name=name, z_index=z, texture=texture)
|
||||
grid_left.add_layer(layer)
|
||||
tm2.apply_to_tile_layer(layer, name, tileset_index=0)
|
||||
|
||||
grid_left.center = (tm2.width * tileset.tile_width // 2,
|
||||
tm2.height * tileset.tile_height // 2)
|
||||
scene3.children.append(grid_left)
|
||||
|
||||
# Right: Procgen island
|
||||
right_label = mcrfpy.Caption(text="Procgen Island (2-layer Wang)", pos=(420, 38))
|
||||
right_label.fill_color = mcrfpy.Color(180, 255, 220)
|
||||
scene3.children.append(right_label)
|
||||
|
||||
IW, IH = 30, 30
|
||||
island_noise = mcrfpy.NoiseSource(dimensions=2, seed=7777)
|
||||
island_hm = island_noise.sample(size=(IW, IH), mode="fbm", octaves=3, world_size=(3.0, 3.0))
|
||||
island_hm.normalize(0.0, 1.0)
|
||||
|
||||
# Create island shape: attenuate edges with radial gradient
|
||||
for y in range(IH):
|
||||
for x in range(IW):
|
||||
dx = (x - IW / 2.0) / (IW / 2.0)
|
||||
dy = (y - IH / 2.0) / (IH / 2.0)
|
||||
dist = (dx * dx + dy * dy) ** 0.5
|
||||
falloff = max(0.0, 1.0 - dist * 1.2)
|
||||
h = island_hm.get(x, y) * falloff
|
||||
island_hm[x, y] = h
|
||||
|
||||
island_hm.normalize(0.0, 1.0)
|
||||
|
||||
# Iterative base terrain expansion (same technique as Screen 2)
|
||||
island_passes_def = [
|
||||
(0.40, T.SEAWATER_LIGHT, T.SAND),
|
||||
(0.25, T.SEAWATER_LIGHT, T.SEAWATER_MEDIUM, T.SAND, T.GRASS),
|
||||
(0.15, T.SEAWATER_MEDIUM, T.SEAWATER_DEEP, T.GRASS, T.CLIFF),
|
||||
]
|
||||
island_base_dm, island_stats = iterative_terrain(
|
||||
island_hm, overworld_ws, IW, IH, island_passes_def)
|
||||
island_base_dm.enum_type = T
|
||||
print(f" Island base stats: {island_stats}")
|
||||
|
||||
# Tree overlay with separate noise
|
||||
island_tree_noise = mcrfpy.NoiseSource(dimensions=2, seed=8888)
|
||||
island_tree_hm = island_tree_noise.sample(
|
||||
size=(IW, IH), mode="fbm", octaves=3, world_size=(4.0, 4.0))
|
||||
island_tree_hm.normalize(0.0, 1.0)
|
||||
|
||||
island_overlay_dm = mcrfpy.DiscreteMap((IW, IH))
|
||||
island_overlay_dm.enum_type = T
|
||||
for y in range(IH):
|
||||
for x in range(IW):
|
||||
base_val = island_base_dm.get(x, y)
|
||||
tree_h = island_tree_hm.get(x, y)
|
||||
if base_val == int(T.GRASS) and tree_h > 0.50:
|
||||
island_overlay_dm.set(x, y, int(T.TREES))
|
||||
else:
|
||||
island_overlay_dm.set(x, y, int(T.AIR))
|
||||
|
||||
# Validate overlay
|
||||
island_ov_results = overworld_ws.resolve(island_overlay_dm)
|
||||
for y in range(IH):
|
||||
for x in range(IW):
|
||||
if island_ov_results[y * IW + x] == -1:
|
||||
island_overlay_dm.set(x, y, int(T.AIR))
|
||||
|
||||
grid_right = mcrfpy.Grid(grid_size=(IW, IH),
|
||||
pos=(420, 60), size=(380, 380), layers=[])
|
||||
grid_right.fill_color = mcrfpy.Color(30, 30, 50)
|
||||
|
||||
island_base_layer = mcrfpy.TileLayer(name="island_base", z_index=-2, texture=texture)
|
||||
grid_right.add_layer(island_base_layer)
|
||||
overworld_ws.apply(island_base_dm, island_base_layer)
|
||||
|
||||
island_overlay_layer = mcrfpy.TileLayer(name="island_trees", z_index=-1, texture=texture)
|
||||
grid_right.add_layer(island_overlay_layer)
|
||||
overworld_ws.apply(island_overlay_dm, island_overlay_layer)
|
||||
|
||||
# Post-process: make AIR cells transparent
|
||||
for y in range(IH):
|
||||
for x in range(IW):
|
||||
if island_overlay_dm.get(x, y) == int(T.AIR):
|
||||
island_overlay_layer.set((x, y), -1)
|
||||
|
||||
grid_right.center = (IW * tileset.tile_width // 2, IH * tileset.tile_height // 2)
|
||||
scene3.children.append(grid_right)
|
||||
|
||||
# Info for both
|
||||
make_info_panel(scene3, [
|
||||
"Left: Premade Map",
|
||||
f" samplemap2.tmj",
|
||||
f" {tm2.width}x{tm2.height}, {len(tm2.tile_layer_names)} layers",
|
||||
"",
|
||||
"Right: Procgen Island",
|
||||
f" {IW}x{IH}, seed=7777",
|
||||
" Iterative terrain expansion",
|
||||
" 2-layer Wang auto-tile",
|
||||
"",
|
||||
"Same tileset, same engine",
|
||||
"Different workflows",
|
||||
], x=200, y=460, w=400, h=None)
|
||||
|
||||
nav3 = mcrfpy.Caption(text="[1] Premade [2] Procgen [3] Side-by-Side [ESC] Quit", pos=(20, 740))
|
||||
nav3.fill_color = mcrfpy.Color(120, 120, 150)
|
||||
scene3.children.append(nav3)
|
||||
|
||||
|
||||
# ======================================================================
|
||||
# Navigation & Screenshots
|
||||
# ======================================================================
|
||||
scenes = [scene1, scene2, scene3]
|
||||
scene_names = ["premade", "procgen", "compare"]
|
||||
|
||||
# Keyboard navigation (all scenes share the same handler)
|
||||
def on_key(key, action):
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
if key == mcrfpy.Key.NUM_1:
|
||||
mcrfpy.current_scene = scene1
|
||||
elif key == mcrfpy.Key.NUM_2:
|
||||
mcrfpy.current_scene = scene2
|
||||
elif key == mcrfpy.Key.NUM_3:
|
||||
mcrfpy.current_scene = scene3
|
||||
elif key == mcrfpy.Key.ESCAPE:
|
||||
mcrfpy.exit()
|
||||
|
||||
for s in scenes:
|
||||
s.on_key = on_key
|
||||
|
||||
# Detect headless mode and take screenshots synchronously
|
||||
is_headless = False
|
||||
try:
|
||||
win = mcrfpy.Window.get()
|
||||
is_headless = "headless" in str(win).lower()
|
||||
except:
|
||||
is_headless = True
|
||||
|
||||
if is_headless:
|
||||
# Headless: use step() to advance simulation and take screenshots directly
|
||||
for i, (sc, name) in enumerate(zip(scenes, scene_names)):
|
||||
mcrfpy.current_scene = sc
|
||||
# Step a few frames to let the scene render
|
||||
for _ in range(3):
|
||||
mcrfpy.step(0.016)
|
||||
fname = f"tiled_demo_{name}.png"
|
||||
automation.screenshot(fname)
|
||||
print(f" Screenshot: {fname}")
|
||||
print("\nAll screenshots captured. Done!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
# Interactive: start on screen 1
|
||||
mcrfpy.current_scene = scene1
|
||||
print("\nTiled Demo ready!")
|
||||
print("Press [1] [2] [3] to switch screens, [ESC] to quit")
|
||||
Loading…
Add table
Add a link
Reference in a new issue