mapGeneration.py 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205
  1. import numpy as np
  2. import yaml
  3. import base64
  4. import struct
  5. import random
  6. import sys
  7. from pyfastnoiselite.pyfastnoiselite import (
  8. FastNoiseLite,
  9. NoiseType,
  10. FractalType,
  11. CellularReturnType,
  12. CellularDistanceFunction,
  13. DomainWarpType,
  14. )
  15. import time
  16. import os
  17. if len(sys.argv) == 1:
  18. mapWidth = 300
  19. mapHeight = 300
  20. print(f"No custom mapsize specified, using defaults: {mapWidth}w x {mapHeight}h")
  21. else:
  22. mapWidth = int(sys.argv[1])
  23. mapHeight = int(sys.argv[2])
  24. print(f"Using specified mapsize: {mapWidth}w x {mapHeight}h")
  25. # -----------------------------------------------------------------------------
  26. # Tilemap
  27. # -----------------------------------------------------------------------------
  28. TILEMAP = {
  29. 0: "Space",
  30. 1: "FloorDirt",
  31. 2: "FloorPlanetGrass",
  32. 3: "FloorGrassDark",
  33. 4: "FloorSand",
  34. 5: "FloorDirtRock",
  35. }
  36. TILEMAP_REVERSE = {v: k for k, v in TILEMAP.items()}
  37. # -----------------------------------------------------------------------------
  38. # Helper Functions
  39. # -----------------------------------------------------------------------------
  40. def round_to_chunk(number, chunk):
  41. """Rounds a number to the inferior multiplier of a chunk."""
  42. return number - (number % chunk)
  43. def add_border(tile_map, border_value):
  44. """Adds a border to tile_map with the specified value."""
  45. bordered = np.pad(
  46. tile_map, pad_width=1, mode="constant", constant_values=border_value
  47. )
  48. return bordered.astype(np.int32)
  49. def encode_tiles(tile_map):
  50. """Codifies the tiles in base64 for the YAML."""
  51. tile_bytes = bytearray()
  52. for y in range(tile_map.shape[0]): # u
  53. for x in range(tile_map.shape[1]):
  54. tile_id = tile_map[y, x]
  55. flags = 0
  56. variant = 0
  57. tile_bytes.extend(struct.pack("<I", tile_id)) # 4 bytes tile_id
  58. tile_bytes.append(flags) # 1 byte flag
  59. tile_bytes.append(variant) # 1 byte variant
  60. return base64.b64encode(tile_bytes).decode("utf-8")
  61. # -----------------------------------------------------------------------------
  62. # Generating a TileMap with multiple layers
  63. # -----------------------------------------------------------------------------
  64. def generate_tile_map(width, height, biome_tile_layers, seed_base=None):
  65. """Generates the tile_map based on the layers defined in biome_tile_layers."""
  66. tile_map = np.full((height, width), TILEMAP_REVERSE["FloorDirt"], dtype=np.int32)
  67. # Orders the layers by priority (largest to smallest)
  68. sorted_layers = sorted(
  69. biome_tile_layers, key=lambda layer: layer.get("priority", 1)
  70. )
  71. for layer in sorted_layers:
  72. noise = FastNoiseLite()
  73. noise.noise_type = layer["noise_type"]
  74. noise.fractal_octaves = layer["octaves"]
  75. noise.frequency = layer["frequency"]
  76. noise.fractal_type = layer["fractal_type"]
  77. if "cellular_distance_function" in layer:
  78. noise.cellular_distance_function = layer["cellular_distance_function"]
  79. if "cellular_return_type" in layer:
  80. noise.cellular_return_type = layer["cellular_return_type"]
  81. if "cellular_jitter" in layer:
  82. noise.cellular_jitter = layer["cellular_jitter"]
  83. if "fractal_lacunarity" in layer:
  84. noise.fractal_lacunarity = layer["fractal_lacunarity"]
  85. if seed_base is not None:
  86. seed_key = layer.get("seed_key", layer["tile_type"])
  87. noise.seed = (seed_base + hash(seed_key)) % (2**31)
  88. # Modulation config, if present
  89. mod_noise = None
  90. if "modulation" in layer:
  91. mod_config = layer["modulation"]
  92. mod_noise = FastNoiseLite()
  93. mod_noise.noise_type = mod_config.get(
  94. "noise_type", NoiseType.NoiseType_OpenSimplex2
  95. )
  96. if "cellular_distance_function" in mod_config:
  97. mod_noise.cellular_distance_function = mod_config[
  98. "cellular_distance_function"
  99. ]
  100. if "cellular_return_type" in mod_config:
  101. mod_noise.cellular_return_type = mod_config["cellular_return_type"]
  102. if "cellular_jitter" in mod_config:
  103. mod_noise.cellular_jitter = mod_config["cellular_jitter"]
  104. if "fractal_lacunarity" in mod_config:
  105. mod_noise.fractal_lacunarity = mod_config["fractal_lacunarity"]
  106. mod_noise.frequency = mod_config.get("frequency", 0.010)
  107. mod_noise.seed = (seed_base + hash(seed_key + "_mod")) % (2**31)
  108. threshold_min = mod_config.get("threshold_min", 0.4)
  109. threshold_max = mod_config.get("threshold_max", 0.6)
  110. count = 0
  111. dont_overwrite = [TILEMAP_REVERSE[t] for t in layer.get("dontOverwrite", [])]
  112. for y in range(height):
  113. for x in range(width):
  114. noise_value = noise.get_noise(x, y)
  115. noise_value = (noise_value + 1) / 2 # Normalise into [0, 1]
  116. place_tile = False
  117. if mod_noise:
  118. mod_value = mod_noise.get_noise(x, y)
  119. mod_value = (mod_value + 1) / 2
  120. if noise_value > layer["threshold"]:
  121. if mod_value > threshold_max:
  122. place_tile = True
  123. elif mod_value > threshold_min:
  124. probability = (mod_value - threshold_min) / (
  125. threshold_max - threshold_min
  126. )
  127. place_tile = random.random() < probability
  128. else:
  129. if noise_value > layer["threshold"]:
  130. place_tile = True
  131. if place_tile:
  132. current_tile = tile_map[y, x]
  133. if current_tile not in dont_overwrite:
  134. if (
  135. layer.get("overwrite", True)
  136. or current_tile == TILEMAP_REVERSE["Space"]
  137. ):
  138. tile_map[y, x] = TILEMAP_REVERSE[layer["tile_type"]]
  139. count += 1
  140. print(f"Layer {layer['tile_type']}: {count} tiles placed")
  141. return tile_map
  142. # -----------------------------------------------------------------------------
  143. # Entity generation
  144. # -----------------------------------------------------------------------------
  145. global_uid = 3
  146. def next_uid():
  147. """Generates an unique UID for each entity."""
  148. global global_uid
  149. uid = global_uid
  150. global_uid += 1
  151. return uid
  152. def generate_dynamic_entities(tile_map, biome_entity_layers, seed_base=None):
  153. """Generates dynamic entities based on the entity layers, respecting priorities."""
  154. groups = {}
  155. entity_count = {} # Count entities by proto
  156. h, w = tile_map.shape
  157. occupied_positions = set() # Set to trace occupied positions
  158. # Order layers by priority. Highest first
  159. sorted_layers = sorted(
  160. biome_entity_layers, key=lambda layer: layer.get("priority", 0), reverse=True
  161. )
  162. for layer in sorted_layers:
  163. # Get entity_protos list
  164. entity_protos = layer["entity_protos"]
  165. if isinstance(entity_protos, str): # If its a string, turns it into a list
  166. entity_protos = [entity_protos]
  167. # Set layer noise
  168. noise = FastNoiseLite()
  169. noise.noise_type = layer["noise_type"]
  170. noise.fractal_octaves = layer["octaves"]
  171. noise.frequency = layer["frequency"]
  172. noise.fractal_type = layer["fractal_type"]
  173. if "cellular_distance_function" in layer:
  174. noise.cellular_distance_function = layer["cellular_distance_function"]
  175. if "cellular_return_type" in layer:
  176. noise.cellular_return_type = layer["cellular_return_type"]
  177. if "cellular_jitter" in layer:
  178. noise.cellular_jitter = layer["cellular_jitter"]
  179. if "fractal_lacunarity" in layer:
  180. noise.fractal_lacunarity = layer["fractal_lacunarity"]
  181. if seed_base is not None:
  182. # Uses "seed_key" if available, if not uses a hash based on entity_protos
  183. seed_key = layer.get("seed_key", tuple(entity_protos))
  184. noise.seed = (seed_base + hash(seed_key)) % (2**31)
  185. for y in range(h):
  186. for x in range(w):
  187. if x == 0 or x == w - 1 or y == 0 or y == h - 1:
  188. continue
  189. if (x, y) in occupied_positions:
  190. continue
  191. tile_val = tile_map[y, x]
  192. noise_value = noise.get_noise(x, y)
  193. noise_value = (noise_value + 1) / 2 # Normalise into [0, 1]
  194. if noise_value > layer["threshold"] and layer["tile_condition"](
  195. tile_val
  196. ):
  197. # Chooses randomly a proto
  198. proto = random.choice(entity_protos)
  199. if proto not in groups:
  200. groups[proto] = []
  201. groups[proto].append(
  202. {
  203. "uid": next_uid(),
  204. "components": [
  205. {"type": "Transform", "parent": 2, "pos": f"{x},{y}"}
  206. ],
  207. }
  208. )
  209. occupied_positions.add((x, y))
  210. # Counts entities by proto
  211. entity_count[proto] = entity_count.get(proto, 0) + 1
  212. # Surrounding undestructible walls
  213. groups["WallRockIndestructible"] = []
  214. for y in range(h):
  215. for x in range(w):
  216. if x == 0 or x == w - 1 or y == 0 or y == h - 1:
  217. groups["WallRockIndestructible"].append(
  218. {
  219. "uid": next_uid(),
  220. "components": [
  221. {"type": "Transform", "parent": 2, "pos": f"{x},{y}"}
  222. ],
  223. }
  224. )
  225. # Count undestructible walls
  226. entity_count["WallRockIndestructible"] = (
  227. entity_count.get("WallRockIndestructible", 0) + 1
  228. )
  229. dynamic_groups = [
  230. {"proto": proto, "entities": ents} for proto, ents in groups.items()
  231. ]
  232. # Print generated protos
  233. for proto, count in entity_count.items():
  234. print(f"Generated {count} amount of {proto}")
  235. return dynamic_groups
  236. def generate_decals(tile_map, biome_decal_layers, seed_base=None, chunk_size=16):
  237. """Generate decals using biome_decal_layers and log the count of each decal type."""
  238. decals_by_id = {}
  239. h, w = tile_map.shape
  240. occupied_tiles = set()
  241. decal_count = {}
  242. for layer in biome_decal_layers:
  243. noise = FastNoiseLite()
  244. noise.noise_type = layer["noise_type"]
  245. noise.fractal_octaves = layer["octaves"]
  246. noise.frequency = layer["frequency"]
  247. noise.fractal_type = layer["fractal_type"]
  248. if seed_base is not None:
  249. seed_key = layer.get(
  250. "seed_key",
  251. (
  252. tuple(layer["decal_id"])
  253. if isinstance(layer["decal_id"], list)
  254. else layer["decal_id"]
  255. ),
  256. )
  257. noise.seed = (seed_base + hash(seed_key)) % (2**31)
  258. decal_ids = (
  259. layer["decal_id"]
  260. if isinstance(layer["decal_id"], list)
  261. else [layer["decal_id"]]
  262. )
  263. for y in range(h):
  264. for x in range(w):
  265. if x == 0 or x == w - 1 or y == 0 or y == h - 1:
  266. continue
  267. if (x, y) in occupied_tiles:
  268. continue
  269. tile_val = tile_map[y, x]
  270. noise_value = noise.get_noise(x, y)
  271. noise_value = (noise_value + 1) / 2
  272. if noise_value > layer["threshold"] and layer["tile_condition"](
  273. tile_val
  274. ):
  275. chosen_decal_id = random.choice(decal_ids)
  276. if chosen_decal_id not in decals_by_id:
  277. decals_by_id[chosen_decal_id] = []
  278. # Small random offset for decals
  279. offset_x = (
  280. noise.get_noise(x + 1000, y + 1000) + 1
  281. ) / 4 - 0.25 # Between -0.25 and 0.25
  282. offset_y = (
  283. noise.get_noise(x + 2000, y + 2000) + 1
  284. ) / 4 - 0.25 # Between -0.25 and 0.25
  285. pos_x = x + offset_x
  286. pos_y = y + offset_y
  287. pos_str = f"{pos_x:.7f},{pos_y:.7f}"
  288. decals_by_id[chosen_decal_id].append(
  289. {"color": layer.get("color", "#FFFFFFFF"), "position": pos_str}
  290. )
  291. occupied_tiles.add((x, y))
  292. decal_count[chosen_decal_id] = (
  293. decal_count.get(chosen_decal_id, 0) + 1
  294. )
  295. return decals_by_id
  296. # Defines uniqueMixes for the atmosphere
  297. unique_mixes = [
  298. {
  299. "volume": 2500,
  300. "immutable": True,
  301. "temperature": 278.15,
  302. "moles": [21.82478, 82.10312, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  303. },
  304. {
  305. "volume": 2500,
  306. "temperature": 278.15,
  307. "moles": [21.824879, 82.10312, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  308. },
  309. ]
  310. def generate_atmosphere_tiles(width, height, chunk_size):
  311. """Generates the atmos tiles based on the map size."""
  312. max_x = (width + chunk_size - 1) // chunk_size - 1
  313. max_y = (height + chunk_size - 1) // chunk_size - 1
  314. tiles = {}
  315. for y in range(-1, max_y + 1):
  316. for x in range(-1, max_x + 1):
  317. if x == -1 or x == max_x or y == -1 or y == max_y:
  318. tiles[f"{x},{y}"] = {0: 65535}
  319. else:
  320. tiles[f"{x},{y}"] = {1: 65535}
  321. return tiles
  322. def generate_main_entities(tile_map, chunk_size=16, decals_by_id=None):
  323. """Generates entities, decals and atmos."""
  324. if decals_by_id is None:
  325. decals_by_id = {}
  326. h, w = tile_map.shape
  327. chunks = {}
  328. for cy in range(0, h, chunk_size):
  329. for cx in range(0, w, chunk_size):
  330. chunk_key = f"{cx//chunk_size},{cy//chunk_size}"
  331. chunk_tiles = tile_map[cy : cy + chunk_size, cx : cx + chunk_size]
  332. if chunk_tiles.shape[0] < chunk_size or chunk_tiles.shape[1] < chunk_size:
  333. full_chunk = np.zeros((chunk_size, chunk_size), dtype=np.int32)
  334. full_chunk[: chunk_tiles.shape[0], : chunk_tiles.shape[1]] = chunk_tiles
  335. chunk_tiles = full_chunk
  336. chunks[chunk_key] = {
  337. "ind": f"{cx//chunk_size},{cy//chunk_size}",
  338. "tiles": encode_tiles(chunk_tiles),
  339. "version": 6,
  340. }
  341. atmosphere_chunk_size = 4
  342. atmosphere_tiles = generate_atmosphere_tiles(w, h, atmosphere_chunk_size)
  343. # Decals generation
  344. decal_nodes = []
  345. global_index = 0
  346. for decal_id, decals in decals_by_id.items():
  347. if decals:
  348. node_decals = {}
  349. for decal in decals:
  350. node_decals[str(global_index)] = decal["position"]
  351. global_index += 1
  352. node = {
  353. "node": {"color": decals[0]["color"], "id": decal_id},
  354. "decals": node_decals,
  355. }
  356. decal_nodes.append(node)
  357. print(f"Total decal nodes generated: {len(decal_nodes)}")
  358. print(f"Total decals: {global_index}")
  359. main = {
  360. "proto": "",
  361. "entities": [
  362. {
  363. "uid": 1,
  364. "components": [
  365. {"type": "MetaData", "name": "Map Entity"},
  366. {"type": "Transform"},
  367. {"type": "LightCycle"},
  368. {"type": "MapLight", "ambientLightColor": "#D8B059FF"},
  369. {"type": "Map", "mapPaused": True},
  370. {"type": "PhysicsMap"},
  371. {"type": "GridTree"},
  372. {"type": "MovedGrids"},
  373. {"type": "Broadphase"},
  374. {"type": "OccluderTree"},
  375. ],
  376. },
  377. {
  378. "uid": 2,
  379. "components": [
  380. {"type": "MetaData", "name": "grid"},
  381. {"type": "Transform", "parent": 1, "pos": "0,0"},
  382. {"type": "MapGrid", "chunks": chunks},
  383. {"type": "Broadphase"},
  384. {
  385. "type": "Physics",
  386. "angularDamping": 0.05,
  387. "bodyStatus": "InAir",
  388. "bodyType": "Dynamic",
  389. "fixedRotation": True,
  390. "linearDamping": 0.05,
  391. },
  392. {"type": "Fixtures", "fixtures": {}},
  393. {"type": "OccluderTree"},
  394. {"type": "SpreaderGrid"},
  395. {"type": "Shuttle"},
  396. {"type": "SunShadow"},
  397. {"type": "SunShadowCycle"},
  398. {"type": "GridPathfinding"},
  399. {
  400. "type": "Gravity",
  401. "gravityShakeSound": {
  402. "!type:SoundPathSpecifier": {
  403. "path": "/Audio/Effects/alert.ogg"
  404. }
  405. },
  406. "inherent": True,
  407. "enabled": True,
  408. },
  409. {"type": "BecomesStation", "id": "Nomads"},
  410. {"type": "Weather"},
  411. {
  412. "type": "WeatherNomads",
  413. "enabledWeathers": [
  414. "Rain",
  415. "Storm",
  416. "SnowfallLight",
  417. "SnowfallMedium",
  418. "SnowfallHeavy",
  419. ],
  420. "minSeasonMinutes": 30,
  421. "maxSeasonMinutes": 45,
  422. "minPrecipitationDurationMinutes": 5,
  423. "maxPrecipitationDurationMinutes": 10
  424. },
  425. {
  426. "type": "DecalGrid",
  427. "chunkCollection": {"version": 2, "nodes": decal_nodes},
  428. },
  429. {
  430. "type": "GridAtmosphere",
  431. "version": 2,
  432. "data": {
  433. "tiles": atmosphere_tiles,
  434. "uniqueMixes": unique_mixes,
  435. "chunkSize": atmosphere_chunk_size,
  436. },
  437. },
  438. {"type": "GasTileOverlay"},
  439. {"type": "RadiationGridResistance"},
  440. ],
  441. },
  442. ],
  443. }
  444. return main
  445. def generate_all_entities(tile_map, chunk_size=16, biome_layers=None, seed_base=None):
  446. """Combines tiles, entities and decals."""
  447. entities = []
  448. if biome_layers is None:
  449. biome_layers = []
  450. biome_tile_layers = [
  451. layer for layer in biome_layers if layer["type"] == "BiomeTileLayer"
  452. ]
  453. biome_entity_layers = [
  454. layer for layer in biome_layers if layer["type"] == "BiomeEntityLayer"
  455. ]
  456. biome_decal_layers = [
  457. layer for layer in biome_layers if layer["type"] == "BiomeDecalLayer"
  458. ]
  459. dynamic_groups = generate_dynamic_entities(tile_map, biome_entity_layers, seed_base)
  460. decals_by_chunk = generate_decals(
  461. tile_map, biome_decal_layers, seed_base, chunk_size
  462. )
  463. main_entities = generate_main_entities(tile_map, chunk_size, decals_by_chunk)
  464. entities.append(main_entities)
  465. entities.extend(dynamic_groups)
  466. spawn_points = generate_spawn_points(tile_map)
  467. entities.extend(spawn_points)
  468. return entities
  469. # -----------------------------------------------------------------------------
  470. # Save YAML
  471. # -----------------------------------------------------------------------------
  472. def represent_sound_path_specifier(dumper, data):
  473. """Customised representation for the SoundPathSpecifier in the YAML."""
  474. for key, value in data.items():
  475. if isinstance(key, str) and key.startswith("!type:"):
  476. tag = key
  477. if isinstance(value, dict) and "path" in value:
  478. return dumper.represent_mapping(tag, value)
  479. return dumper.represent_dict(data)
  480. def save_map_to_yaml(
  481. tile_map,
  482. biome_layers,
  483. output_dir,
  484. filename="output.yml",
  485. chunk_size=16,
  486. seed_base=None,
  487. ):
  488. """Saves the generated map in a YAML file in the specified folder."""
  489. all_entities = generate_all_entities(tile_map, chunk_size, biome_layers, seed_base)
  490. count = sum(len(group.get("entities", [])) for group in all_entities)
  491. map_data = {
  492. "meta": {
  493. "format": 7,
  494. "category": "Map",
  495. "engineVersion": "249.0.0",
  496. "forkId": "",
  497. "forkVersion": "",
  498. "time": "03/23/2025 18:21:23",
  499. "entityCount": count,
  500. },
  501. "maps": [1],
  502. "grids": [2],
  503. "orphans": [],
  504. "nullspace": [],
  505. "tilemap": TILEMAP,
  506. "entities": all_entities,
  507. }
  508. yaml.add_representer(dict, represent_sound_path_specifier)
  509. output_path = os.path.join(output_dir, filename)
  510. with open(output_path, "w") as outfile:
  511. yaml.dump(map_data, outfile, default_flow_style=False, sort_keys=False)
  512. import numpy as np
  513. from collections import defaultdict
  514. def apply_erosion(tile_map, tile_type, min_neighbors=3):
  515. h, w = tile_map.shape
  516. new_map = tile_map.copy()
  517. for y in range(1, h - 1):
  518. for x in range(1, w - 1):
  519. if tile_map[y, x] == tile_type:
  520. neighbors = 0
  521. neighbor_types = []
  522. for dy in [-1, 0, 1]:
  523. for dx in [-1, 0, 1]:
  524. if dy == 0 and dx == 0:
  525. continue
  526. neighbor_y = y + dy
  527. neighbor_x = x + dx
  528. if 0 <= neighbor_y < h and 0 <= neighbor_x < w:
  529. nt = tile_map[neighbor_y, neighbor_x]
  530. neighbor_types.append(nt)
  531. if nt == tile_type:
  532. neighbors += 1
  533. if neighbors < min_neighbors:
  534. counts = defaultdict(int)
  535. for nt in neighbor_types:
  536. counts[nt] += 1
  537. if counts:
  538. max_count = max(counts.values())
  539. candidates = [k for k, v in counts.items() if v == max_count]
  540. majority_type = candidates[0] # Defines majority_type here
  541. new_map[y, x] = majority_type
  542. return new_map
  543. def count_isolated_tiles(tile_map, tile_type, min_neighbors=3):
  544. h, w = tile_map.shape
  545. isolated = 0
  546. for y in range(1, h - 1):
  547. for x in range(1, w - 1):
  548. if tile_map[y, x] == tile_type:
  549. neighbors = sum(
  550. 1
  551. for dy in [-1, 0, 1]
  552. for dx in [-1, 0, 1]
  553. if not (dy == 0 and dx == 0)
  554. and 0 <= y + dy < h
  555. and 0 <= x + dx < w
  556. and tile_map[y + dy, x + dx] == tile_type
  557. )
  558. if neighbors < min_neighbors:
  559. isolated += 1
  560. return isolated
  561. def apply_iterative_erosion(tile_map, tile_type, min_neighbors=3, max_iterations=10):
  562. """Applies erosion interactively untill there are no more tiles with the declared min neighbors"""
  563. iteration = 0
  564. while iteration < max_iterations:
  565. isolated_before = count_isolated_tiles(tile_map, tile_type, min_neighbors)
  566. tile_map = apply_erosion(tile_map, tile_type, min_neighbors)
  567. isolated_after = count_isolated_tiles(tile_map, tile_type, min_neighbors)
  568. if isolated_after == isolated_before or isolated_after == 0:
  569. break
  570. iteration += 1
  571. return tile_map
  572. # -----------------------------------------------------------------------------
  573. # Spawn Point Generation
  574. # -----------------------------------------------------------------------------
  575. def generate_spawn_points(tile_map, num_points_per_corner=1):
  576. """Generates 4 SpawnPointNomads and 4 SpawnPointLatejoin, one on each corner, on FloorPlanetGrass."""
  577. h, w = tile_map.shape
  578. spawn_positions = set()
  579. nomads_entities = []
  580. latejoin_entities = []
  581. corners = ["top_left", "top_right", "bottom_left", "bottom_right"]
  582. astro_grass_id = TILEMAP_REVERSE["FloorPlanetGrass"]
  583. directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
  584. for corner in corners:
  585. found = False
  586. initial_size = 15 # Initial size to search for positions
  587. while not found and initial_size <= min(w, h) // 2:
  588. x_min, x_max, y_min, y_max = get_corner_region(corner, w, h, initial_size)
  589. candidates = []
  590. # Searchs for AstroTileGrass in the initial size in the corners
  591. for y in range(y_min, y_max + 1):
  592. for x in range(x_min, x_max + 1):
  593. if (
  594. tile_map[y, x] == astro_grass_id
  595. and (x, y) not in spawn_positions
  596. ):
  597. # Verifies adjacent valid tiles
  598. adjacent = []
  599. for dx, dy in directions:
  600. nx, ny = x + dx, y + dy
  601. if (
  602. 0 <= nx < w
  603. and 0 <= ny < h
  604. and tile_map[ny, nx] == astro_grass_id
  605. and (nx, ny) not in spawn_positions
  606. ):
  607. adjacent.append((nx, ny))
  608. if adjacent:
  609. candidates.append((x, y, adjacent))
  610. if candidates:
  611. x, y, adjacent = random.choice(candidates)
  612. adj_x, adj_y = random.choice(adjacent)
  613. if random.random() < 0.5:
  614. nomads_pos = (x, y)
  615. latejoin_pos = (adj_x, adj_y)
  616. else:
  617. nomads_pos = (adj_x, adj_y)
  618. latejoin_pos = (x, y)
  619. nomads_entities.append(
  620. {
  621. "uid": next_uid(),
  622. "components": [
  623. {
  624. "type": "Transform",
  625. "parent": 2,
  626. "pos": f"{nomads_pos[0]},{nomads_pos[1]}",
  627. }
  628. ],
  629. }
  630. )
  631. latejoin_entities.append(
  632. {
  633. "uid": next_uid(),
  634. "components": [
  635. {
  636. "type": "Transform",
  637. "parent": 2,
  638. "pos": f"{latejoin_pos[0]},{latejoin_pos[1]}",
  639. }
  640. ],
  641. }
  642. )
  643. spawn_positions.add(nomads_pos)
  644. spawn_positions.add(latejoin_pos)
  645. found = True
  646. else:
  647. initial_size += 1
  648. if not found:
  649. print(
  650. f"Possible to find an available position at the corner for spawn points {corner}"
  651. )
  652. print("SpawnPointNomads positions:")
  653. for ent in nomads_entities:
  654. pos = ent["components"][0]["pos"]
  655. print(pos)
  656. print("SpawnPointLatejoin positions:")
  657. for ent in latejoin_entities:
  658. pos = ent["components"][0]["pos"]
  659. print(pos)
  660. # Retorna as entidades no formato correto para o YAML
  661. return [
  662. {"proto": "SpawnPointNomads", "entities": nomads_entities},
  663. {"proto": "SpawnPointLatejoin", "entities": latejoin_entities},
  664. ]
  665. def get_corner_region(corner, w, h, initial_size):
  666. """Defines a region to search in the map's corners."""
  667. if corner == "top_left":
  668. x_min = 1
  669. x_max = min(initial_size, w - 2)
  670. y_min = 1
  671. y_max = min(initial_size, h - 2)
  672. elif corner == "top_right":
  673. x_min = max(w - 1 - initial_size, 1)
  674. x_max = w - 2
  675. y_min = 1
  676. y_max = min(initial_size, h - 2)
  677. elif corner == "bottom_left":
  678. x_min = 1
  679. x_max = min(initial_size, w - 2)
  680. y_min = max(h - 1 - initial_size, 1)
  681. y_max = h - 2
  682. elif corner == "bottom_right":
  683. x_min = max(w - 1 - initial_size, 1)
  684. x_max = w - 2
  685. y_min = max(h - 1 - initial_size, 1)
  686. y_max = h - 2
  687. else:
  688. raise ValueError("Invalid corner")
  689. return x_min, x_max, y_min, y_max
  690. # -----------------------------------------------------------------------------
  691. # Configuração do Mapa (MAP_CONFIG)
  692. # -----------------------------------------------------------------------------
  693. MAP_CONFIG = [
  694. { # Rock dirt formations
  695. "type": "BiomeTileLayer",
  696. "tile_type": "FloorDirtRock",
  697. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  698. "octaves": 2,
  699. "frequency": 0.01,
  700. "fractal_type": FractalType.FractalType_None,
  701. "threshold": -1.0,
  702. "overwrite": True,
  703. },
  704. { # Sprinkled dirt around the map
  705. "type": "BiomeTileLayer",
  706. "tile_type": "FloorDirt",
  707. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  708. "octaves": 10,
  709. "frequency": 0.3,
  710. "fractal_type": FractalType.FractalType_FBm,
  711. "threshold": 0.825,
  712. "overwrite": True,
  713. "dontOverwrite": ["FloorSand", "FloorDirtRock"],
  714. "priority": 10,
  715. },
  716. {
  717. "type": "BiomeTileLayer",
  718. "tile_type": "FloorPlanetGrass",
  719. "noise_type": NoiseType.NoiseType_Perlin,
  720. "octaves": 3,
  721. "frequency": 0.02,
  722. "fractal_type": FractalType.FractalType_None,
  723. "threshold": 0.4,
  724. "overwrite": True,
  725. },
  726. { # Boulders for flints
  727. "type": "BiomeEntityLayer",
  728. "entity_protos": "FloraRockSolid",
  729. "noise_type": NoiseType.NoiseType_OpenSimplex2S,
  730. "octaves": 6,
  731. "frequency": 0.3,
  732. "fractal_type": FractalType.FractalType_FBm,
  733. "threshold": 0.815,
  734. "tile_condition": lambda tile: tile
  735. in [
  736. TILEMAP_REVERSE["FloorPlanetGrass"],
  737. TILEMAP_REVERSE["FloorDirt"],
  738. TILEMAP_REVERSE["FloorDirtRock"],
  739. ],
  740. "priority": 1,
  741. },
  742. { # Rocks
  743. "type": "BiomeEntityLayer",
  744. "entity_protos": "WallRock",
  745. "noise_type": NoiseType.NoiseType_Cellular,
  746. "cellular_distance_function": CellularDistanceFunction.CellularDistanceFunction_Hybrid,
  747. "cellular_return_type": CellularReturnType.CellularReturnType_CellValue,
  748. "cellular_jitter": 1.070,
  749. "octaves": 2,
  750. "frequency": 0.015,
  751. "fractal_type": FractalType.FractalType_FBm,
  752. "threshold": 0.30,
  753. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorDirtRock"],
  754. "priority": 2,
  755. },
  756. { # Wild crops
  757. "type": "BiomeEntityLayer",
  758. "entity_protos": [
  759. "WildPlantPotato",
  760. "WildPlantCorn",
  761. "WildPlantRice",
  762. "WildPlantWheat",
  763. "WildPlantHemp",
  764. "WildPlantPoppy",
  765. "WildPlantAloe",
  766. "WildPlantYarrow",
  767. "WildPlantElderflower",
  768. "WildPlantMilkThistle",
  769. "WildPlantComfrey",
  770. ],
  771. "noise_type": NoiseType.NoiseType_OpenSimplex2S,
  772. "octaves": 6,
  773. "frequency": 0.3,
  774. "fractal_type": FractalType.FractalType_FBm,
  775. "threshold": 0.84,
  776. "tile_condition": lambda tile: tile in [TILEMAP_REVERSE["FloorPlanetGrass"]],
  777. "priority": 1,
  778. },
  779. { # Rivers
  780. "type": "BiomeEntityLayer",
  781. "entity_protos": "FloorWaterEntity",
  782. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  783. "octaves": 1,
  784. "fractal_lacunarity": 1.50,
  785. "frequency": 0.003,
  786. "fractal_type": FractalType.FractalType_Ridged,
  787. "threshold": 0.95,
  788. "tile_condition": lambda tile: True,
  789. "priority": 10,
  790. "seed_key": "river_noise",
  791. },
  792. { # Deep River Water (in the middle)
  793. "type": "BiomeEntityLayer",
  794. "entity_protos": "FloorWaterDeepEntity", # The deep water entity
  795. "noise_type": NoiseType.NoiseType_OpenSimplex2, # Same noise type as river
  796. "octaves": 1, # Same octaves as river
  797. "fractal_lacunarity": 1.50, # Same lacunarity as river
  798. "frequency": 0.003, # Same frequency as river
  799. "fractal_type": FractalType.FractalType_Ridged, # Same fractal type as river
  800. "threshold": 0.975, # HIGHER threshold than river (adjust if needed)
  801. "tile_condition": lambda tile: True, # Place wherever noise is high enough
  802. "priority": 11, # HIGHER priority than river (to overwrite)
  803. "seed_key": "river_noise", # MUST use the same noise seed as river
  804. },
  805. { # River sand
  806. "type": "BiomeTileLayer",
  807. "tile_type": "FloorSand",
  808. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  809. "octaves": 1,
  810. "frequency": 0.003, # Same as the river
  811. "fractal_type": FractalType.FractalType_Ridged,
  812. "threshold": 0.935, # Larger than the river
  813. "overwrite": True,
  814. "seed_key": "river_noise",
  815. },
  816. { # Additional River Sand with More Curves
  817. "type": "BiomeTileLayer",
  818. "tile_type": "FloorSand",
  819. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  820. "octaves": 1,
  821. "frequency": 0.003,
  822. "fractal_type": FractalType.FractalType_Ridged,
  823. "threshold": 0.92, # Slightly lower than the original
  824. "overwrite": True,
  825. "seed_key": "river_noise", # Same as the original to follow its path
  826. "modulation": {
  827. "noise_type": NoiseType.NoiseType_Perlin, # Different noise for variation
  828. "frequency": 0.01, # Controls the scale of the variation
  829. "threshold_min": 0.43, # Lower bound where sand starts appearing
  830. "threshold_max": 0.55, # Upper bound for a smooth transition
  831. },
  832. },
  833. { # Trees
  834. "type": "BiomeEntityLayer",
  835. "entity_protos": "TreeTemperate",
  836. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  837. "octaves": 1,
  838. "frequency": 0.5,
  839. "fractal_type": FractalType.FractalType_FBm,
  840. "threshold": 0.9,
  841. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  842. "priority": 0,
  843. },
  844. ####### PREDATORS
  845. { # Wolves
  846. "type": "BiomeEntityLayer",
  847. "entity_protos": "SpawnMobGreyWolf",
  848. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  849. "octaves": 1,
  850. "frequency": 0.1,
  851. "fractal_type": FractalType.FractalType_FBm,
  852. "threshold": 0.9981,
  853. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  854. "priority": 11,
  855. },
  856. { # Bears
  857. "type": "BiomeEntityLayer",
  858. "entity_protos": "SpawnMobBear",
  859. "noise_type": NoiseType.NoiseType_Perlin,
  860. "octaves": 1,
  861. "frequency": 0.300,
  862. "fractal_type": FractalType.FractalType_FBm,
  863. "threshold": 0.958,
  864. "tile_condition": lambda tile: tile
  865. in [TILEMAP_REVERSE["FloorPlanetGrass"], TILEMAP_REVERSE["FloorDirtRock"]],
  866. "priority": 1,
  867. },
  868. { # Sabertooth
  869. "type": "BiomeEntityLayer",
  870. "entity_protos": "SpawnMobSabertooth",
  871. "noise_type": NoiseType.NoiseType_Perlin,
  872. "octaves": 1,
  873. "frequency": 0.300,
  874. "fractal_type": FractalType.FractalType_FBm,
  875. "threshold": 0.96882,
  876. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  877. "priority": 11,
  878. },
  879. ####### Preys
  880. { # Rabbits
  881. "type": "BiomeEntityLayer",
  882. "entity_protos": "SpawnMobRabbit",
  883. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  884. "octaves": 1,
  885. "frequency": 0.1,
  886. "fractal_type": FractalType.FractalType_FBm,
  887. "threshold": 0.9989,
  888. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  889. "priority": 11,
  890. },
  891. { # Chicken
  892. "type": "BiomeEntityLayer",
  893. "entity_protos": "SpawnMobChicken",
  894. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  895. "octaves": 1,
  896. "frequency": 0.1,
  897. "fractal_type": FractalType.FractalType_FBm,
  898. "threshold": 0.9989,
  899. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  900. "priority": 11,
  901. },
  902. { # Deers
  903. "type": "BiomeEntityLayer",
  904. "entity_protos": "SpawnMobDeer",
  905. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  906. "octaves": 1,
  907. "frequency": 0.1,
  908. "fractal_type": FractalType.FractalType_FBm,
  909. "threshold": 0.9989,
  910. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  911. "priority": 11,
  912. },
  913. { # Pigs
  914. "type": "BiomeEntityLayer",
  915. "entity_protos": "SpawnMobPig",
  916. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  917. "octaves": 1,
  918. "frequency": 0.1,
  919. "fractal_type": FractalType.FractalType_FBm,
  920. "threshold": 0.9992,
  921. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  922. "priority": 11,
  923. },
  924. # DECALS
  925. { # Bush Temperate group 1
  926. "type": "BiomeDecalLayer",
  927. "decal_id": [
  928. "BushTemperate1",
  929. "BushTemperate2",
  930. "BushTemperate3",
  931. "BushTemperate4",
  932. ],
  933. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  934. "octaves": 1,
  935. "frequency": 0.1,
  936. "fractal_type": FractalType.FractalType_FBm,
  937. "threshold": 0.96,
  938. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  939. "color": "#FFFFFFFF",
  940. },
  941. { # Bush Temperate group 2
  942. "type": "BiomeDecalLayer",
  943. "decal_id": [
  944. "BushTemperate5",
  945. "BushTemperate6",
  946. "BushTemperate7",
  947. "BushTemperate8",
  948. ],
  949. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  950. "octaves": 1,
  951. "frequency": 0.1,
  952. "fractal_type": FractalType.FractalType_FBm,
  953. "threshold": 0.96,
  954. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  955. "color": "#FFFFFFFF",
  956. },
  957. { # Bush Temperate group 3
  958. "type": "BiomeDecalLayer",
  959. "decal_id": ["BushTemperate9", "BushTemperate10", "BushTemperate11"],
  960. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  961. "octaves": 1,
  962. "frequency": 0.1,
  963. "fractal_type": FractalType.FractalType_FBm,
  964. "threshold": 0.96,
  965. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  966. "color": "#FFFFFFFF",
  967. },
  968. { # Bush Temperate group 4
  969. "type": "BiomeDecalLayer",
  970. "decal_id": [
  971. "BushTemperate12",
  972. "BushTemperate13",
  973. "BushTemperate14",
  974. "BushTemperate15",
  975. ],
  976. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  977. "octaves": 1,
  978. "frequency": 0.1,
  979. "fractal_type": FractalType.FractalType_FBm,
  980. "threshold": 0.96,
  981. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  982. "color": "#FFFFFFFF",
  983. },
  984. { # Bush Temperate group 5
  985. "type": "BiomeDecalLayer",
  986. "decal_id": ["BushTemperate16", "BushTemperate17", "BushTemperate18"],
  987. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  988. "octaves": 1,
  989. "frequency": 0.1,
  990. "fractal_type": FractalType.FractalType_FBm,
  991. "threshold": 0.96,
  992. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  993. "color": "#FFFFFFFF",
  994. },
  995. { # Bush Temperate group 6
  996. "type": "BiomeDecalLayer",
  997. "decal_id": [
  998. "BushTemperate19",
  999. "BushTemperate20",
  1000. "BushTemperate21",
  1001. "BushTemperate22",
  1002. ],
  1003. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1004. "octaves": 1,
  1005. "frequency": 0.1,
  1006. "fractal_type": FractalType.FractalType_FBm,
  1007. "threshold": 0.96,
  1008. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1009. "color": "#FFFFFFFF",
  1010. },
  1011. { # Bush Temperate group 7
  1012. "type": "BiomeDecalLayer",
  1013. "decal_id": ["BushTemperate23", "BushTemperate24", "BushTemperate25"],
  1014. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1015. "octaves": 1,
  1016. "frequency": 0.1,
  1017. "fractal_type": FractalType.FractalType_FBm,
  1018. "threshold": 0.96,
  1019. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1020. "color": "#FFFFFFFF",
  1021. },
  1022. { # Bush Temperate group 8
  1023. "type": "BiomeDecalLayer",
  1024. "decal_id": ["BushTemperate26", "BushTemperate27", "BushTemperate28"],
  1025. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1026. "octaves": 1,
  1027. "frequency": 0.1,
  1028. "fractal_type": FractalType.FractalType_FBm,
  1029. "threshold": 0.96,
  1030. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1031. "color": "#FFFFFFFF",
  1032. },
  1033. { # Bush Temperate group 9
  1034. "type": "BiomeDecalLayer",
  1035. "decal_id": [
  1036. "BushTemperate29",
  1037. "BushTemperate30",
  1038. "BushTemperate31",
  1039. "BushTemperate32",
  1040. ],
  1041. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1042. "octaves": 1,
  1043. "frequency": 0.1,
  1044. "fractal_type": FractalType.FractalType_FBm,
  1045. "threshold": 0.96,
  1046. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1047. "color": "#FFFFFFFF",
  1048. },
  1049. { # Bush Temperate group 10
  1050. "type": "BiomeDecalLayer",
  1051. "decal_id": [
  1052. "BushTemperate33",
  1053. "BushTemperate34",
  1054. "BushTemperate35",
  1055. "BushTemperate36",
  1056. ],
  1057. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1058. "octaves": 1,
  1059. "frequency": 0.1,
  1060. "fractal_type": FractalType.FractalType_FBm,
  1061. "threshold": 0.96,
  1062. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1063. "color": "#FFFFFFFF",
  1064. },
  1065. { # Bush Temperate group 11 - High grass
  1066. "type": "BiomeDecalLayer",
  1067. "decal_id": [
  1068. "BushTemperate37",
  1069. "BushTemperate38",
  1070. "BushTemperate39",
  1071. "BushTemperate40",
  1072. "BushTemperate41",
  1073. "BushTemperate42",
  1074. ],
  1075. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1076. "octaves": 1,
  1077. "frequency": 0.1,
  1078. "fractal_type": FractalType.FractalType_FBm,
  1079. "threshold": 0.96,
  1080. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1081. "color": "#FFFFFFFF",
  1082. },
  1083. ]
  1084. # -----------------------------------------------------------------------------
  1085. # Execution
  1086. # -----------------------------------------------------------------------------
  1087. start_time = time.time()
  1088. seed_base = random.randint(0, 1000000)
  1089. print(f"Generated seed: {seed_base}")
  1090. width, height = mapWidth, mapHeight
  1091. chunk_size = 16
  1092. biome_tile_layers = [layer for layer in MAP_CONFIG if layer["type"] == "BiomeTileLayer"]
  1093. biome_entity_layers = [
  1094. layer for layer in MAP_CONFIG if layer["type"] == "BiomeEntityLayer"
  1095. ]
  1096. script_dir = os.path.dirname(os.path.abspath(__file__))
  1097. output_dir = os.path.join(script_dir, "Resources", "Maps", "civ")
  1098. os.makedirs(output_dir, exist_ok=True)
  1099. tile_map = generate_tile_map(width, height, biome_tile_layers, seed_base)
  1100. # Applies erosion to lone sand tiles, overwritting it with surrounding tiles
  1101. tile_map = apply_iterative_erosion(
  1102. tile_map, TILEMAP_REVERSE["FloorSand"], min_neighbors=1
  1103. )
  1104. bordered_tile_map = add_border(tile_map, border_value=TILEMAP_REVERSE["FloorDirt"])
  1105. save_map_to_yaml(
  1106. bordered_tile_map,
  1107. MAP_CONFIG,
  1108. output_dir,
  1109. filename="nomads_classic.yml",
  1110. chunk_size=chunk_size,
  1111. seed_base=seed_base,
  1112. )
  1113. end_time = time.time()
  1114. total_time = end_time - start_time
  1115. print(f"Map generated and saved in {total_time:.2f} seconds!")