22from PIL import Image , ImageDraw
33import xml .etree .ElementTree as ET
44
5- def generate_donut_map (path = 'donut.png' , size = 128 ):
5+ # TODO: Have palette be imported from yaml files instead of hard-coded
6+ TERRAIN = {
7+ (0 , 128 , 0 ): "TERRAIN_LUSH" ,
8+ (255 , 200 , 0 ): "TERRAIN_ARID" ,
9+ (255 , 255 , 0 ): "TERRAIN_SAND" ,
10+ (0 , 255 , 0 ): "TERRAIN_TEMPERATE" ,
11+ (222 , 222 , 222 ): "TERRAIN_TUNDRA" ,
12+ (0 , 222 , 111 ): "TERRAIN_MARSH" ,
13+ (222 , 0 , 111 ): "TERRAIN_URBAN" ,
14+ (0 , 0 , 128 ): "TERRAIN_WATER" ,
15+ (0 ,0 ,0 ) : "Unknown"
16+ }
17+ HEIGHT = {
18+ (222 , 222 , 222 ): "HEIGHT_MOUNTAIN" ,
19+ (144 , 144 , 144 ): "HEIGHT_HILL" ,
20+ (111 , 111 , 111 ): "HEIGHT_FLAT" ,
21+ (0 , 255 , 255 ): "HEIGHT_LAKE" ,
22+ (0 , 111 , 255 ): "HEIGHT_COAST" ,
23+ (0 , 0 , 255 ): "HEIGHT_OCEAN" ,
24+ (255 , 0 , 0 ): "HEIGHT_VOLCANO"
25+ }
26+ VEGET = {
27+ (0 , 90 , 0 ): "VEGETATION_TREES" ,
28+ (200 , 128 , 0 ): "VEGETATION_SCRUB" ,
29+ }
30+
31+ def generate_donut_map (path = '.' , size = 128 ):
632 """Generate a donut-shaped map with concentric circles of different terrain types."""
7- img = Image .new ('RGB' , (size , size ), (255 , 255 , 255 )) # ocean
8- draw = ImageDraw .Draw (img )
33+ # --------------------------- TERRAIN ---------------------------
34+ terrain = Image .new ('RGB' , (size , size ), (0 , 0 , 128 )) # ocean
35+ draw = ImageDraw .Draw (terrain )
936
1037 center = size // 2
1138 outer_radius = int (size * 0.4 )
1239 middle_radius = int (size * 0.3 )
1340 inner_radius = int (size * 0.1 )
1441
15- # Draw circles from largest to smallest
1642 draw .ellipse ([(center - outer_radius , center - outer_radius ),
1743 (center + outer_radius , center + outer_radius )],
18- fill = (255 , 255 , 255 ))
44+ fill = (255 , 200 , 0 ))
1945
2046 draw .ellipse ([(center - middle_radius , center - middle_radius ),
2147 (center + middle_radius , center + middle_radius )],
22- fill = (0 , 255 , 0 ))
48+ fill = (0 , 128 , 0 ))
2349
2450 draw .ellipse ([(center - inner_radius , center - inner_radius ),
2551 (center + inner_radius , center + inner_radius )],
26- fill = (255 , 255 , 255 ))
52+ fill = (0 , 0 , 128 ))
2753
28- img .save (path )
54+ terrain .save (f"{ path } /terrain_ex.png" )
55+
56+ #----------------------VEG------------------------------------
57+ veg = Image .new ('RGB' , (size , size ), (0 , 0 , 0 )) # ocean
58+ draw = ImageDraw .Draw (veg )
59+
60+ center = size // 2
61+ outer_radius = int (size * 0.25 )
62+ inner_radius = int (size * 0.12 )
63+
64+ draw .ellipse ([(center - outer_radius , center - outer_radius ),
65+ (center + outer_radius , center + outer_radius )],
66+ fill = (0 , 90 , 0 ))
67+
68+ draw .ellipse ([(center - inner_radius , center - inner_radius ),
69+ (center + inner_radius , center + inner_radius )],
70+ fill = (0 , 0 , 0 ))
71+
72+ veg .save (f"{ path } /veg_ex.png" )
73+ # -----------------HEIGHT--------------------------------------
74+ height = Image .new ('RGB' , (size , size ), (0 , 111 , 255 )) # ocean
75+ draw = ImageDraw .Draw (height )
76+
77+ center = size // 2
78+ outer_radius = int (size * 0.4 )
79+ middle_radius = int (size * 0.3 )
80+ inner_radius = int (size * 0.1 )
81+
82+ draw .ellipse ([(center - outer_radius , center - outer_radius ),
83+ (center + outer_radius , center + outer_radius )],
84+ fill = (144 , 144 , 144 ))
85+
86+ draw .ellipse ([(center - middle_radius , center - middle_radius ),
87+ (center + middle_radius , center + middle_radius )],
88+ fill = (111 , 111 , 111 ))
89+
90+ draw .ellipse ([(center - inner_radius , center - inner_radius ),
91+ (center + inner_radius , center + inner_radius )],
92+ fill = (0 , 255 , 255 ))
93+
94+ height .save (f"{ path } /height_ex.png" )
95+
2996
3097def generate_palette (palette , filename ):
3198 """Generate a PNG image with one pixel for each color in the palette."""
@@ -43,112 +110,89 @@ def generate_palette(palette, filename):
43110 # TODO: Save palette as svg instead of png
44111 img .save (filename )
45112
46- # TODO: Rewrite interpret_generic or impl. process_layer
47- # Not amenable to current pipeline, rewrite or impl. process_layer
113+
48114def interpret_generic (rgb , tilemap , tolerance = 15 ):
49115 """
50116 More generic function, not sure if this is the best way to do it.
51117 """
52- tiletype = tilemap .get (rgb , "Unknown" )
53-
54- if tiletype != "Unknown" and tiletype != "None" :
118+ tiletype = tilemap .get (rgb , None )
119+ if tiletype :
55120 return tiletype
56-
121+
122+ best_fit = 255 , "Unknown"
57123 for center_rgb , tiletype in tilemap .items ():
58124 # Manhattan distance
59125 distance = sum (abs (a - b ) for a , b in zip (rgb , center_rgb ))
60- if distance <= tolerance * 3 : # tolerance per channel * 3 channels
61- return tiletype
62-
63- return "Unknown"
64-
65- # Define mappings for terrain, height, vegetation, and rivers
66- def interpret_rgb_as_terrain (rgb ):
67- """Map RGB values to terrain types."""
68- terrain_map = {
69- (0 , 128 , 0 ): "TERRAIN_LUSH" ,
70- (255 , 200 , 0 ): "TERRAIN_ARID" ,
71- (255 , 255 , 0 ): "TERRAIN_SAND" ,
72- (0 , 255 , 0 ): "TERRAIN_TEMPERATE" ,
73- (222 , 222 , 222 ): "TERRAIN_TUNDRA" ,
74- (0 , 0 , 0 ): "TERRAIN_URBAN" ,
75- (0 , 0 , 128 ): "TERRAIN_WATER"
76- }
77- return terrain_map .get (rgb , "Unknown" )
78-
79- def interpret_rgb_as_height (rgb ):
80- """Map RGB values to height levels."""
81- height_map = {
82- (222 , 222 , 222 ): "HEIGHT_MOUNTAIN" ,
83- (144 , 144 , 144 ): "HEIGHT_HILL" ,
84- (111 , 111 , 111 ): "HEIGHT_FLAT" ,
85- (0 , 255 , 255 ): "HEIGHT_LAKE" ,
86- (0 , 111 , 255 ): "HEIGHT_COAST" ,
87- (0 , 0 , 255 ): "HEIGHT_OCEAN" ,
88- (255 , 0 , 0 ): "HEIGHT_VOLCANO"
89- }
90- return height_map .get (rgb , "None" )
91-
92- def interpret_rgb_as_vegetation (rgb ):
93- """Map RGB values to vegetation types."""
94- vegetation_map = {
95- (0 , 90 , 0 ): "VEGETATION_TREES" ,
96- (200 , 128 , 0 ): "VEGETATION_SCRUB" ,
97- (255 ,255 ,255 ): "None"
98- }
99- return vegetation_map .get (rgb , "None" )
126+ if distance <= tolerance * 3 or distance < best_fit [0 ]: # tolerance per channel * 3 channels
127+ best_fit = distance , tiletype
128+
129+ return best_fit [1 ]
100130
101131# TODO: Implement process_layer to make it modular
102- def process_layer () :
132+ def process_layer (imagefile , tilemap ) -> ET . ElementTree :
103133 """Generic version of loop code in process_map_images"""
104134 ...
105135
106136# TODO: Add default value when no layer given so all layers are optional
107- def process_map_images (terrain_file , height_file , vegetation_file , output_file ):
137+ def process_map_images (height_file , terrain_file , vegetation_file , output_file ):
108138 """Generate an XML map file from input PNG maps."""
109139 # Open images
110- terrain_img = Image .open (terrain_file )
111- height_img = Image .open (height_file )
112- vegetation_img = Image .open (vegetation_file )
140+ height_img = Image .open (height_file ) if height_file else None
141+ terrain_img = Image .open (terrain_file ) if terrain_file else None
142+ vegetation_img = Image .open (vegetation_file ) if vegetation_file else None
143+
144+ imgs = [height_img , terrain_img , vegetation_img ]
145+
146+ # Check if there are any images to render
147+ # TODO: Error message / raise for no images given
148+ if all ([not isinstance (img , Image .Image ) for img in imgs ]): return
113149
114150 # Ensure all images have the same dimensions
115- w , h = terrain_img .size
116- if any (img .size != (w , h ) for img in [ height_img , vegetation_img ] ):
151+ w ,h = imgs [ 0 ] .size
152+ if any (img .size != (w , h ) for img in imgs if isinstance ( img , Image . Image ) ):
117153 raise ValueError ("All input images must have the same dimensions." )
118154
119- # Prepare XML structure
155+ # Prepare XML
120156 root = ET .Element ("Root" , MapWidth = str (w ), MapHeight = str (h ), MapEdgesSafe = "False" )
121157
122158 id = 0
123159 for y in reversed (range (h )):
124160 for x in range (w ):
125- # Get pixel data
126- terrain_rgb = terrain_img .getpixel ((x , y ))[:3 ] # RGB tuple
127- height_rgb = height_img .getpixel ((x , y ))[:3 ]
128- vegetation_rgb = vegetation_img .getpixel ((x , y ))[:3 ]
129-
130- # Interpret pixel data
131- terrain = interpret_rgb_as_terrain (terrain_rgb )
132- height = interpret_rgb_as_height (height_rgb )
133- vegetation = interpret_rgb_as_vegetation (vegetation_rgb )
134-
135161 # Create tile element
136162 tile = ET .SubElement (root , "Tile" , ID = str (id ))
137- ET .SubElement (tile , "Terrain" ).text = terrain
138- ET .SubElement (tile , "Height" ).text = height
139- if vegetation :
140- ET .SubElement (tile , "Vegetation" ).text = vegetation
163+
164+ # Terrain
165+ if terrain_img :
166+ terrain_rgb = terrain_img .getpixel ((x , y ))[:3 ] # RGB tuple
167+ terrain = interpret_generic (terrain_rgb , TERRAIN )
168+ ET .SubElement (tile , "Terrain" ).text = terrain
169+
170+ # Height
171+ if height_img :
172+ height_rgb = height_img .getpixel ((x , y ))[:3 ]
173+ height = interpret_generic (height_rgb , HEIGHT )
174+ if height != "Unknown" :
175+ ET .SubElement (tile , "Height" ).text = height
176+
177+
178+ # Vegetation
179+ if vegetation_img :
180+ vegetation_rgb = vegetation_img .getpixel ((x , y ))[:3 ]
181+ vegetation = interpret_generic (vegetation_rgb , VEGET )
182+
183+ if vegetation != 'Unknown' :
184+ ET .SubElement (tile , "Vegetation" ).text = vegetation
185+
186+
141187 id += 1
142- # Write to output file
188+
189+ # Write out to XML doc
143190 tree = ET .ElementTree (root )
144191 tree .write (output_file , encoding = "utf-8" , xml_declaration = True )
145192
146- # Example usage
147193if __name__ == "__main__" :
148- terrain_file = "docs/terrainmap.png"
149- height_file = "docs/heightmap.png"
150- vegetation_file = "docs/vegmap.png"
151- output_file = "docs/map.xml"
152194
153- process_map_images (terrain_file , height_file , vegetation_file , output_file )
154- #generate_donut_map('docs/donut.png')
195+ generate_palette (HEIGHT , 'docs/heightpalette.png' )
196+ generate_palette (TERRAIN , 'docs/terrainpalette.png' )
197+
198+ generate_palette (VEGET , 'docs/vegepalette.png' )
0 commit comments