#!/usr/bin/env python3 """ River generator and map builder for tiletest. Encodes rivers onto images/rivermap.png such that rivers STRICTLY pass through the exact midpoints of the 6 hexagon edges, ensuring 100% seamless connections. """ from PIL import Image, ImageDraw import numpy as np WIDTH = 3600 HEIGHT = 1500 SCALE_X = 8.0 SCALE_Y = 9.24045 LOD_0_SIZE_Y = 0.4328799197461722 # 6 edge midpoints in local tile space (radius R = 0.5) MIDPOINTS = [ (-0.375, 0.21650635), # Edge 0 (Top-Left) ( 0.0, 0.43301270), # Edge 1 (Top) ( 0.375, 0.21650635), # Edge 2 (Top-Right) ( 0.375, -0.21650635), # Edge 3 (Bottom-Right) ( 0.0, -0.43301270), # Edge 4 (Bottom) (-0.375, -0.21650635) # Edge 5 (Bottom-Left) ] def tile_world_pos(tx, ty): wx = tx * 0.75 wy = ty * (LOD_0_SIZE_Y * 2.0) + (0.0 if tx % 2 == 0 else LOD_0_SIZE_Y) return wx, wy def world_to_pixel(wx, wy): px = int(round(wx * SCALE_X)) py = int(round(wy * SCALE_Y)) return px, HEIGHT - 1 - py def get_hex_neighbor(x, y, edge): even = (x % 2 == 0) if edge == 0: return (x - 1, y if even else y + 1) if edge == 1: return (x, y + 1) if edge == 2: return (x + 1, y if even else y + 1) if edge == 3: return (x + 1, y - 1 if even else y) if edge == 4: return (x, y - 1) if edge == 5: return (x - 1, y - 1 if even else y) return (x, y) def trace_river(start_tile, edge_steps): """Traces a river from a start tile following a sequence of exit edges.""" path = [] curr = start_tile in_edge = None for out_edge in edge_steps: path.append((curr, in_edge, out_edge)) curr = get_hex_neighbor(curr[0], curr[1], out_edge) in_edge = (out_edge + 3) % 6 path.append((curr, in_edge, None)) return path def build_rivermap(output_path="/home/bart/project/tiletest/images/rivermap.png", preview_path="/home/bart/project/tiletest/images/rivermap_preview.png"): river_img = Image.new('RGB', (WIDTH, HEIGHT), (0, 0, 0)) draw = ImageDraw.Draw(river_img) # 1. Main River: starts high on mountain (36, 31) in snow, flows down to South Ocean (32, 14) main_river_steps = [5, 5, 5, 5] + [4] * 15 main_river = trace_river((36, 31), main_river_steps) # 2. Mountain Tributary: starts on ridge (36, 28) and merges with main river at (32, 26) trib_river_steps = [5, 5, 5, 5] tributary = trace_river((36, 28), trib_river_steps) # 3. Canyon River in Prairie (Red Sand & Red Rock) prairie_steps = [4, 4, 5, 4, 4, 3, 4, 4, 4] prairie_river = trace_river((110, 32), prairie_steps) rivers = [main_river, tributary, prairie_river] river_color = (0, 180, 255) for r in rivers: for (tx, ty), e_in, e_out in r: wx, wy = tile_world_pos(tx, ty) cx, cy = world_to_pixel(wx, wy) edges = [e for e in (e_in, e_out) if e is not None] for e in edges: mx, my = MIDPOINTS[e] mpx, mpy = world_to_pixel(wx + mx, wy + my) # Line between midpoint and tile center draw.line([(mpx, mpy), (cx, cy)], fill=river_color, width=2) # Small marker at midpoint for detection draw.rectangle([mpx - 1, mpy - 1, mpx + 1, mpy + 1], fill=river_color) river_img.save(output_path) print(f"Saved rivermap to {output_path}") # Build an overlay preview on top of typemap.png try: type_img = Image.open("/home/bart/project/tiletest/images/typemap.png").convert("RGBA") r_arr = np.array(river_img) t_arr = np.array(type_img) mask = (r_arr[:, :, 2] > 120) & (r_arr[:, :, 0] < 100) t_arr[mask] = [0, 220, 255, 255] preview = Image.fromarray(t_arr) preview.save(preview_path) print(f"Saved preview to {preview_path}") # Build orthographic hex-tile minimap.png h_arr = np.array(Image.open("/home/bart/project/tiletest/images/heightmap.jpg")) R = 8.0 W = 3608 H = 1740 hex_img = Image.new('RGB', (W, H), (15, 20, 30)) hdraw = ImageDraw.Draw(hex_img) angles = [np.pi * 60.0 * i / 180.0 for i in range(6)] corner_dx = [-R * np.cos(a) for a in angles] corner_dy = [ R * np.sin(a) for a in angles] for ty in range(125): for tx in range(300): wx = tx * 0.75 wy = ty * (LOD_0_SIZE_Y * 2.0) + (0.0 if tx % 2 == 0 else LOD_0_SIZE_Y) cx = (wx + 0.5) * 16.0 cy = H - 1 - (wy + LOD_0_SIZE_Y) * 16.0 px = min(3599, max(0, int(round(wx * SCALE_X)))) py = min(1499, max(0, int(round(wy * SCALE_Y)))) h = h_arr[1500 - 1 - py, px] tp = t_arr[1500 - 1 - py, px] dist_pole = abs((wy / 108.22) - 0.5) * 2.0 if h <= 2: color = (220, 235, 250) if dist_pole > 0.85 else (28, 95, 175) # Water (blue) / polar ice elif h > 182 or (dist_pole > 0.80 and h > 20): color = (245, 250, 255) # Snow (white) above 2500m elif h > 73: # Mountain rock (between 1000m and 2500m) r, g, b = int(tp[0]), int(tp[1]), int(tp[2]) if r > 150 and g < 100: color = (175, 75, 55) # Red rock mountain in prairie else: color = (125, 130, 135) # Gray alpine mountain rock else: # Biome ground below 1000m r, g, b = int(tp[0]), int(tp[1]), int(tp[2]) sat = max(r, g, b) - min(r, g, b) if r > 150 and g < 100: color = (195, 75, 45) # Prairie / Red Rock elif r > 150 and g > 150 and b < 100: color = (225, 185, 65) # Sahara yellow elif b > 50 and r < 50: color = (35, 115, 50) # Forest green elif sat < 40 and g < 140: color = (110, 125, 95) # Tundra slate else: color = (85, 175, 55) # Grassland green poly = [(cx + corner_dx[i], cy + corner_dy[i]) for i in range(6)] hdraw.polygon(poly, fill=color, outline=(0, 0, 0, 45)) # Draw rivers on hex map for r in rivers: for (tx, ty), e_in, e_out in r: wx, wy = tile_world_pos(tx, ty) cx = (wx + 0.5) * 16.0 cy = H - 1 - (wy + LOD_0_SIZE_Y) * 16.0 edges = [e for e in (e_in, e_out) if e is not None] for e in edges: mx, my = MIDPOINTS[e] mpx = (wx + mx + 0.5) * 16.0 mpy = H - 1 - (wy + my + LOD_0_SIZE_Y) * 16.0 hdraw.line([(mpx, mpy), (cx, cy)], fill=(0, 220, 255), width=3) hdraw.ellipse([mpx - 2, mpy - 2, mpx + 2, mpy + 2], fill=(0, 240, 255)) hex_img.save("/home/bart/project/tiletest/images/minimap.png") print("Saved orthographic hex minimap to /home/bart/project/tiletest/images/minimap.png") except Exception as ex: print(f"Minimap/preview generation skipped: {ex}") if __name__ == "__main__": build_rivermap()