mapGeneration.py 45 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232
  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. """
  324. Generates the main map and grid entities, including tile chunks, decals, and atmospheric data.
  325. Divides the tile map into chunks, encodes each chunk for storage, and constructs the main map entity (UID 1) and grid entity (UID 2) with relevant components such as lighting, physics, weather, decals, and atmosphere. Decals are grouped by ID and indexed, and atmospheric data is generated per chunk. Returns a dictionary containing the main entities and their components.
  326. """
  327. if decals_by_id is None:
  328. decals_by_id = {}
  329. h, w = tile_map.shape
  330. chunks = {}
  331. for cy in range(0, h, chunk_size):
  332. for cx in range(0, w, chunk_size):
  333. chunk_key = f"{cx//chunk_size},{cy//chunk_size}"
  334. chunk_tiles = tile_map[cy : cy + chunk_size, cx : cx + chunk_size]
  335. if chunk_tiles.shape[0] < chunk_size or chunk_tiles.shape[1] < chunk_size:
  336. full_chunk = np.zeros((chunk_size, chunk_size), dtype=np.int32)
  337. full_chunk[: chunk_tiles.shape[0], : chunk_tiles.shape[1]] = chunk_tiles
  338. chunk_tiles = full_chunk
  339. chunks[chunk_key] = {
  340. "ind": f"{cx//chunk_size},{cy//chunk_size}",
  341. "tiles": encode_tiles(chunk_tiles),
  342. "version": 6,
  343. }
  344. atmosphere_chunk_size = 4
  345. atmosphere_tiles = generate_atmosphere_tiles(w, h, atmosphere_chunk_size)
  346. # Decals generation
  347. decal_nodes = []
  348. global_index = 0
  349. for decal_id, decals in decals_by_id.items():
  350. if decals:
  351. node_decals = {}
  352. for decal in decals:
  353. node_decals[str(global_index)] = decal["position"]
  354. global_index += 1
  355. node = {
  356. "node": {"color": decals[0]["color"], "id": decal_id},
  357. "decals": node_decals,
  358. }
  359. decal_nodes.append(node)
  360. print(f"Total decal nodes generated: {len(decal_nodes)}")
  361. print(f"Total decals: {global_index}")
  362. main = {
  363. "proto": "",
  364. "entities": [
  365. {
  366. "uid": 1,
  367. "components": [
  368. {"type": "MetaData", "name": "Map Entity"},
  369. {"type": "Transform"},
  370. {"type": "LightCycle"},
  371. {"type": "MapLight", "ambientLightColor": "#D8B059FF"},
  372. {"type": "Map", "mapPaused": True},
  373. {"type": "PhysicsMap"},
  374. {"type": "GridTree"},
  375. {"type": "MovedGrids"},
  376. {"type": "Broadphase"},
  377. {"type": "OccluderTree"},
  378. {"type": "CivFactions"},
  379. ],
  380. },
  381. {
  382. "uid": 2,
  383. "components": [
  384. {"type": "MetaData", "name": "grid"},
  385. {"type": "Transform", "parent": 1, "pos": "0,0"},
  386. {"type": "MapGrid", "chunks": chunks},
  387. {"type": "Broadphase"},
  388. {
  389. "type": "Physics",
  390. "angularDamping": 0.05,
  391. "bodyStatus": "InAir",
  392. "bodyType": "Dynamic",
  393. "fixedRotation": True,
  394. "linearDamping": 0.05,
  395. },
  396. {"type": "Fixtures", "fixtures": {}},
  397. {"type": "OccluderTree"},
  398. {"type": "SpreaderGrid"},
  399. {"type": "Shuttle"},
  400. {"type": "SunShadow"},
  401. {"type": "SunShadowCycle"},
  402. {"type": "GridPathfinding"},
  403. {
  404. "type": "Gravity",
  405. "gravityShakeSound": {
  406. "!type:SoundPathSpecifier": {
  407. "path": "/Audio/Effects/alert.ogg"
  408. }
  409. },
  410. "inherent": True,
  411. "enabled": True,
  412. },
  413. {"type": "BecomesStation", "id": "Nomads"},
  414. {"type": "Weather"},
  415. {
  416. "type": "WeatherNomads",
  417. "enabledWeathers": [
  418. "Rain",
  419. "Storm",
  420. "SnowfallLight",
  421. "SnowfallMedium",
  422. "SnowfallHeavy",
  423. ],
  424. "minSeasonMinutes": 30,
  425. "maxSeasonMinutes": 45,
  426. "minPrecipitationDurationMinutes": 5,
  427. "maxPrecipitationDurationMinutes": 10,
  428. },
  429. {
  430. "type": "DecalGrid",
  431. "chunkCollection": {"version": 2, "nodes": decal_nodes},
  432. },
  433. {
  434. "type": "GridAtmosphere",
  435. "version": 2,
  436. "data": {
  437. "tiles": atmosphere_tiles,
  438. "uniqueMixes": unique_mixes,
  439. "chunkSize": atmosphere_chunk_size,
  440. },
  441. },
  442. {"type": "GasTileOverlay"},
  443. {"type": "RadiationGridResistance"},
  444. ],
  445. },
  446. ],
  447. }
  448. return main
  449. def generate_all_entities(tile_map, chunk_size=16, biome_layers=None, seed_base=None):
  450. """Combines tiles, entities and decals."""
  451. entities = []
  452. if biome_layers is None:
  453. biome_layers = []
  454. biome_tile_layers = [
  455. layer for layer in biome_layers if layer["type"] == "BiomeTileLayer"
  456. ]
  457. biome_entity_layers = [
  458. layer for layer in biome_layers if layer["type"] == "BiomeEntityLayer"
  459. ]
  460. biome_decal_layers = [
  461. layer for layer in biome_layers if layer["type"] == "BiomeDecalLayer"
  462. ]
  463. dynamic_groups = generate_dynamic_entities(tile_map, biome_entity_layers, seed_base)
  464. decals_by_chunk = generate_decals(
  465. tile_map, biome_decal_layers, seed_base, chunk_size
  466. )
  467. main_entities = generate_main_entities(tile_map, chunk_size, decals_by_chunk)
  468. entities.append(main_entities)
  469. entities.extend(dynamic_groups)
  470. spawn_points = generate_spawn_points(tile_map)
  471. entities.extend(spawn_points)
  472. return entities
  473. # -----------------------------------------------------------------------------
  474. # Save YAML
  475. # -----------------------------------------------------------------------------
  476. def represent_sound_path_specifier(dumper, data):
  477. """Customised representation for the SoundPathSpecifier in the YAML."""
  478. for key, value in data.items():
  479. if isinstance(key, str) and key.startswith("!type:"):
  480. tag = key
  481. if isinstance(value, dict) and "path" in value:
  482. return dumper.represent_mapping(tag, value)
  483. return dumper.represent_dict(data)
  484. def save_map_to_yaml(
  485. tile_map,
  486. biome_layers,
  487. output_dir,
  488. filename="output.yml",
  489. chunk_size=16,
  490. seed_base=None,
  491. ):
  492. """Saves the generated map in a YAML file in the specified folder."""
  493. all_entities = generate_all_entities(tile_map, chunk_size, biome_layers, seed_base)
  494. count = sum(len(group.get("entities", [])) for group in all_entities)
  495. map_data = {
  496. "meta": {
  497. "format": 7,
  498. "category": "Map",
  499. "engineVersion": "249.0.0",
  500. "forkId": "",
  501. "forkVersion": "",
  502. "time": "03/23/2025 18:21:23",
  503. "entityCount": count,
  504. },
  505. "maps": [1],
  506. "grids": [2],
  507. "orphans": [],
  508. "nullspace": [],
  509. "tilemap": TILEMAP,
  510. "entities": all_entities,
  511. }
  512. yaml.add_representer(dict, represent_sound_path_specifier)
  513. output_path = os.path.join(output_dir, filename)
  514. with open(output_path, "w") as outfile:
  515. yaml.dump(map_data, outfile, default_flow_style=False, sort_keys=False)
  516. import numpy as np
  517. from collections import defaultdict
  518. def apply_erosion(tile_map, tile_type, min_neighbors=3):
  519. h, w = tile_map.shape
  520. new_map = tile_map.copy()
  521. for y in range(1, h - 1):
  522. for x in range(1, w - 1):
  523. if tile_map[y, x] == tile_type:
  524. neighbors = 0
  525. neighbor_types = []
  526. for dy in [-1, 0, 1]:
  527. for dx in [-1, 0, 1]:
  528. if dy == 0 and dx == 0:
  529. continue
  530. neighbor_y = y + dy
  531. neighbor_x = x + dx
  532. if 0 <= neighbor_y < h and 0 <= neighbor_x < w:
  533. nt = tile_map[neighbor_y, neighbor_x]
  534. neighbor_types.append(nt)
  535. if nt == tile_type:
  536. neighbors += 1
  537. if neighbors < min_neighbors:
  538. counts = defaultdict(int)
  539. for nt in neighbor_types:
  540. counts[nt] += 1
  541. if counts:
  542. max_count = max(counts.values())
  543. candidates = [k for k, v in counts.items() if v == max_count]
  544. majority_type = candidates[0] # Defines majority_type here
  545. new_map[y, x] = majority_type
  546. return new_map
  547. def count_isolated_tiles(tile_map, tile_type, min_neighbors=3):
  548. h, w = tile_map.shape
  549. isolated = 0
  550. for y in range(1, h - 1):
  551. for x in range(1, w - 1):
  552. if tile_map[y, x] == tile_type:
  553. neighbors = sum(
  554. 1
  555. for dy in [-1, 0, 1]
  556. for dx in [-1, 0, 1]
  557. if not (dy == 0 and dx == 0)
  558. and 0 <= y + dy < h
  559. and 0 <= x + dx < w
  560. and tile_map[y + dy, x + dx] == tile_type
  561. )
  562. if neighbors < min_neighbors:
  563. isolated += 1
  564. return isolated
  565. def apply_iterative_erosion(tile_map, tile_type, min_neighbors=3, max_iterations=10):
  566. """Applies erosion interactively untill there are no more tiles with the declared min neighbors"""
  567. iteration = 0
  568. while iteration < max_iterations:
  569. isolated_before = count_isolated_tiles(tile_map, tile_type, min_neighbors)
  570. tile_map = apply_erosion(tile_map, tile_type, min_neighbors)
  571. isolated_after = count_isolated_tiles(tile_map, tile_type, min_neighbors)
  572. if isolated_after == isolated_before or isolated_after == 0:
  573. break
  574. iteration += 1
  575. return tile_map
  576. # -----------------------------------------------------------------------------
  577. # Spawn Point Generation
  578. # -----------------------------------------------------------------------------
  579. def generate_spawn_points(tile_map, num_points_per_corner=1):
  580. """Generates 4 SpawnPointNomads and 4 SpawnPointLatejoin, one on each corner, on FloorPlanetGrass."""
  581. h, w = tile_map.shape
  582. spawn_positions = set()
  583. nomads_entities = []
  584. latejoin_entities = []
  585. corners = ["top_left", "top_right", "bottom_left", "bottom_right"]
  586. astro_grass_id = TILEMAP_REVERSE["FloorPlanetGrass"]
  587. directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
  588. for corner in corners:
  589. found = False
  590. initial_size = 15 # Initial size to search for positions
  591. while not found and initial_size <= min(w, h) // 2:
  592. x_min, x_max, y_min, y_max = get_corner_region(corner, w, h, initial_size)
  593. candidates = []
  594. # Searchs for AstroTileGrass in the initial size in the corners
  595. for y in range(y_min, y_max + 1):
  596. for x in range(x_min, x_max + 1):
  597. if (
  598. tile_map[y, x] == astro_grass_id
  599. and (x, y) not in spawn_positions
  600. ):
  601. # Verifies adjacent valid tiles
  602. adjacent = []
  603. for dx, dy in directions:
  604. nx, ny = x + dx, y + dy
  605. if (
  606. 0 <= nx < w
  607. and 0 <= ny < h
  608. and tile_map[ny, nx] == astro_grass_id
  609. and (nx, ny) not in spawn_positions
  610. ):
  611. adjacent.append((nx, ny))
  612. if adjacent:
  613. candidates.append((x, y, adjacent))
  614. if candidates:
  615. x, y, adjacent = random.choice(candidates)
  616. adj_x, adj_y = random.choice(adjacent)
  617. if random.random() < 0.5:
  618. nomads_pos = (x, y)
  619. latejoin_pos = (adj_x, adj_y)
  620. else:
  621. nomads_pos = (adj_x, adj_y)
  622. latejoin_pos = (x, y)
  623. nomads_entities.append(
  624. {
  625. "uid": next_uid(),
  626. "components": [
  627. {
  628. "type": "Transform",
  629. "parent": 2,
  630. "pos": f"{nomads_pos[0]},{nomads_pos[1]}",
  631. }
  632. ],
  633. }
  634. )
  635. latejoin_entities.append(
  636. {
  637. "uid": next_uid(),
  638. "components": [
  639. {
  640. "type": "Transform",
  641. "parent": 2,
  642. "pos": f"{latejoin_pos[0]},{latejoin_pos[1]}",
  643. }
  644. ],
  645. }
  646. )
  647. spawn_positions.add(nomads_pos)
  648. spawn_positions.add(latejoin_pos)
  649. found = True
  650. else:
  651. initial_size += 1
  652. if not found:
  653. print(
  654. f"Possible to find an available position at the corner for spawn points {corner}"
  655. )
  656. print("SpawnPointNomads positions:")
  657. for ent in nomads_entities:
  658. pos = ent["components"][0]["pos"]
  659. print(pos)
  660. print("SpawnPointLatejoin positions:")
  661. for ent in latejoin_entities:
  662. pos = ent["components"][0]["pos"]
  663. print(pos)
  664. # Retorna as entidades no formato correto para o YAML
  665. return [
  666. {"proto": "SpawnPointNomads", "entities": nomads_entities},
  667. {"proto": "SpawnPointLatejoin", "entities": latejoin_entities},
  668. ]
  669. def get_corner_region(corner, w, h, initial_size):
  670. """Defines a region to search in the map's corners."""
  671. if corner == "top_left":
  672. x_min = 1
  673. x_max = min(initial_size, w - 2)
  674. y_min = 1
  675. y_max = min(initial_size, h - 2)
  676. elif corner == "top_right":
  677. x_min = max(w - 1 - initial_size, 1)
  678. x_max = w - 2
  679. y_min = 1
  680. y_max = min(initial_size, h - 2)
  681. elif corner == "bottom_left":
  682. x_min = 1
  683. x_max = min(initial_size, w - 2)
  684. y_min = max(h - 1 - initial_size, 1)
  685. y_max = h - 2
  686. elif corner == "bottom_right":
  687. x_min = max(w - 1 - initial_size, 1)
  688. x_max = w - 2
  689. y_min = max(h - 1 - initial_size, 1)
  690. y_max = h - 2
  691. else:
  692. raise ValueError("Invalid corner")
  693. return x_min, x_max, y_min, y_max
  694. # -----------------------------------------------------------------------------
  695. # Configuração do Mapa (MAP_CONFIG)
  696. # -----------------------------------------------------------------------------
  697. MAP_CONFIG = [
  698. { # Rock dirt formations
  699. "type": "BiomeTileLayer",
  700. "tile_type": "FloorDirtRock",
  701. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  702. "octaves": 2,
  703. "frequency": 0.01,
  704. "fractal_type": FractalType.FractalType_None,
  705. "threshold": -1.0,
  706. "overwrite": True,
  707. },
  708. { # Sprinkled dirt around the map
  709. "type": "BiomeTileLayer",
  710. "tile_type": "FloorDirt",
  711. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  712. "octaves": 10,
  713. "frequency": 0.3,
  714. "fractal_type": FractalType.FractalType_FBm,
  715. "threshold": 0.825,
  716. "overwrite": True,
  717. "dontOverwrite": ["FloorSand", "FloorDirtRock"],
  718. "priority": 10,
  719. },
  720. {
  721. "type": "BiomeTileLayer",
  722. "tile_type": "FloorPlanetGrass",
  723. "noise_type": NoiseType.NoiseType_Perlin,
  724. "octaves": 3,
  725. "frequency": 0.02,
  726. "fractal_type": FractalType.FractalType_None,
  727. "threshold": 0.4,
  728. "overwrite": True,
  729. },
  730. { # Boulders for flints
  731. "type": "BiomeEntityLayer",
  732. "entity_protos": "FloraRockSolid",
  733. "noise_type": NoiseType.NoiseType_OpenSimplex2S,
  734. "octaves": 6,
  735. "frequency": 0.3,
  736. "fractal_type": FractalType.FractalType_FBm,
  737. "threshold": 0.815,
  738. "tile_condition": lambda tile: tile
  739. in [
  740. TILEMAP_REVERSE["FloorPlanetGrass"],
  741. TILEMAP_REVERSE["FloorDirt"],
  742. TILEMAP_REVERSE["FloorDirtRock"],
  743. ],
  744. "priority": 1,
  745. },
  746. { # Rocks
  747. "type": "BiomeEntityLayer",
  748. "entity_protos": "WallRock",
  749. "noise_type": NoiseType.NoiseType_Cellular,
  750. "cellular_distance_function": CellularDistanceFunction.CellularDistanceFunction_Hybrid,
  751. "cellular_return_type": CellularReturnType.CellularReturnType_CellValue,
  752. "cellular_jitter": 1.070,
  753. "octaves": 2,
  754. "frequency": 0.015,
  755. "fractal_type": FractalType.FractalType_FBm,
  756. "threshold": 0.30,
  757. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorDirtRock"],
  758. "priority": 2,
  759. },
  760. { # Wild crops
  761. "type": "BiomeEntityLayer",
  762. "entity_protos": [
  763. "WildPlantPotato",
  764. "WildPlantCorn",
  765. "WildPlantRice",
  766. "WildPlantWheat",
  767. "WildPlantHemp",
  768. "WildPlantPoppy",
  769. "WildPlantAloe",
  770. "WildPlantYarrow",
  771. "WildPlantElderflower",
  772. "WildPlantMilkThistle",
  773. "WildPlantComfrey",
  774. ],
  775. "noise_type": NoiseType.NoiseType_OpenSimplex2S,
  776. "octaves": 6,
  777. "frequency": 0.3,
  778. "fractal_type": FractalType.FractalType_FBm,
  779. "threshold": 0.78,
  780. "tile_condition": lambda tile: tile in [TILEMAP_REVERSE["FloorPlanetGrass"]],
  781. "priority": 1,
  782. },
  783. { # Rivers
  784. "type": "BiomeEntityLayer",
  785. "entity_protos": "FloorWaterEntity",
  786. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  787. "octaves": 1,
  788. "fractal_lacunarity": 1.50,
  789. "frequency": 0.003,
  790. "fractal_type": FractalType.FractalType_Ridged,
  791. "threshold": 0.95,
  792. "tile_condition": lambda tile: True,
  793. "priority": 10,
  794. "seed_key": "river_noise",
  795. },
  796. { # Deep River Water (in the middle)
  797. "type": "BiomeEntityLayer",
  798. "entity_protos": "FloorWaterDeepEntity", # The deep water entity
  799. "noise_type": NoiseType.NoiseType_OpenSimplex2, # Same noise type as river
  800. "octaves": 1, # Same octaves as river
  801. "fractal_lacunarity": 1.50, # Same lacunarity as river
  802. "frequency": 0.003, # Same frequency as river
  803. "fractal_type": FractalType.FractalType_Ridged, # Same fractal type as river
  804. "threshold": 0.975, # HIGHER threshold than river (adjust if needed)
  805. "tile_condition": lambda tile: True, # Place wherever noise is high enough
  806. "priority": 11, # HIGHER priority than river (to overwrite)
  807. "seed_key": "river_noise", # MUST use the same noise seed as river
  808. },
  809. { # River sand
  810. "type": "BiomeTileLayer",
  811. "tile_type": "FloorSand",
  812. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  813. "octaves": 1,
  814. "frequency": 0.003, # Same as the river
  815. "fractal_type": FractalType.FractalType_Ridged,
  816. "threshold": 0.935, # Larger than the river
  817. "overwrite": True,
  818. "seed_key": "river_noise",
  819. },
  820. { # Additional River Sand with More Curves
  821. "type": "BiomeTileLayer",
  822. "tile_type": "FloorSand",
  823. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  824. "octaves": 1,
  825. "frequency": 0.003,
  826. "fractal_type": FractalType.FractalType_Ridged,
  827. "threshold": 0.92, # Slightly lower than the original
  828. "overwrite": True,
  829. "seed_key": "river_noise", # Same as the original to follow its path
  830. "modulation": {
  831. "noise_type": NoiseType.NoiseType_Perlin, # Different noise for variation
  832. "frequency": 0.01, # Controls the scale of the variation
  833. "threshold_min": 0.43, # Lower bound where sand starts appearing
  834. "threshold_max": 0.55, # Upper bound for a smooth transition
  835. },
  836. },
  837. { # Trees
  838. "type": "BiomeEntityLayer",
  839. "entity_protos": "TreeTemperate",
  840. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  841. "octaves": 1,
  842. "frequency": 0.5,
  843. "fractal_type": FractalType.FractalType_FBm,
  844. "threshold": 0.9,
  845. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  846. "priority": 0,
  847. },
  848. ####### PREDATORS
  849. { # Wolves
  850. "type": "BiomeEntityLayer",
  851. "entity_protos": "SpawnMobGreyWolf",
  852. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  853. "octaves": 1,
  854. "frequency": 0.1,
  855. "fractal_type": FractalType.FractalType_FBm,
  856. "threshold": 0.9981,
  857. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  858. "priority": 11,
  859. },
  860. { # Bears
  861. "type": "BiomeEntityLayer",
  862. "entity_protos": "SpawnMobBear",
  863. "noise_type": NoiseType.NoiseType_Perlin,
  864. "octaves": 1,
  865. "frequency": 0.300,
  866. "fractal_type": FractalType.FractalType_FBm,
  867. "threshold": 0.958,
  868. "tile_condition": lambda tile: tile
  869. in [TILEMAP_REVERSE["FloorPlanetGrass"], TILEMAP_REVERSE["FloorDirtRock"]],
  870. "priority": 1,
  871. },
  872. { # Sabertooth
  873. "type": "BiomeEntityLayer",
  874. "entity_protos": "SpawnMobSabertooth",
  875. "noise_type": NoiseType.NoiseType_Perlin,
  876. "octaves": 1,
  877. "frequency": 0.300,
  878. "fractal_type": FractalType.FractalType_FBm,
  879. "threshold": 0.96882,
  880. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  881. "priority": 11,
  882. },
  883. ####### Preys
  884. { # Rabbits
  885. "type": "BiomeEntityLayer",
  886. "entity_protos": "SpawnMobRabbit",
  887. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  888. "octaves": 1,
  889. "frequency": 0.1,
  890. "fractal_type": FractalType.FractalType_FBm,
  891. "threshold": 0.9989,
  892. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  893. "priority": 11,
  894. },
  895. { # Cows
  896. "type": "BiomeEntityLayer",
  897. "entity_protos": "SpawnMobCow",
  898. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  899. "octaves": 1,
  900. "frequency": 0.1,
  901. "fractal_type": FractalType.FractalType_FBm,
  902. "threshold": 0.9989,
  903. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  904. "priority": 11,
  905. },
  906. { # Goats
  907. "type": "BiomeEntityLayer",
  908. "entity_protos": "SpawnMobGoat",
  909. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  910. "octaves": 1,
  911. "frequency": 0.1,
  912. "fractal_type": FractalType.FractalType_FBm,
  913. "threshold": 0.9989,
  914. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  915. "priority": 11,
  916. },
  917. { # Chicken
  918. "type": "BiomeEntityLayer",
  919. "entity_protos": "SpawnMobChicken",
  920. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  921. "octaves": 1,
  922. "frequency": 0.1,
  923. "fractal_type": FractalType.FractalType_FBm,
  924. "threshold": 0.9989,
  925. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  926. "priority": 11,
  927. },
  928. { # Deers
  929. "type": "BiomeEntityLayer",
  930. "entity_protos": "SpawnMobDeer",
  931. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  932. "octaves": 1,
  933. "frequency": 0.1,
  934. "fractal_type": FractalType.FractalType_FBm,
  935. "threshold": 0.9989,
  936. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  937. "priority": 11,
  938. },
  939. { # Pigs
  940. "type": "BiomeEntityLayer",
  941. "entity_protos": "SpawnMobPig",
  942. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  943. "octaves": 1,
  944. "frequency": 0.1,
  945. "fractal_type": FractalType.FractalType_FBm,
  946. "threshold": 0.9992,
  947. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  948. "priority": 11,
  949. },
  950. # DECALS
  951. { # Bush Temperate group 1
  952. "type": "BiomeDecalLayer",
  953. "decal_id": [
  954. "BushTemperate1",
  955. "BushTemperate2",
  956. "BushTemperate3",
  957. "BushTemperate4",
  958. ],
  959. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  960. "octaves": 1,
  961. "frequency": 0.1,
  962. "fractal_type": FractalType.FractalType_FBm,
  963. "threshold": 0.96,
  964. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  965. "color": "#FFFFFFFF",
  966. },
  967. { # Bush Temperate group 2
  968. "type": "BiomeDecalLayer",
  969. "decal_id": [
  970. "BushTemperate5",
  971. "BushTemperate6",
  972. "BushTemperate7",
  973. "BushTemperate8",
  974. ],
  975. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  976. "octaves": 1,
  977. "frequency": 0.1,
  978. "fractal_type": FractalType.FractalType_FBm,
  979. "threshold": 0.96,
  980. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  981. "color": "#FFFFFFFF",
  982. },
  983. { # Bush Temperate group 3
  984. "type": "BiomeDecalLayer",
  985. "decal_id": ["BushTemperate9", "BushTemperate10", "BushTemperate11"],
  986. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  987. "octaves": 1,
  988. "frequency": 0.1,
  989. "fractal_type": FractalType.FractalType_FBm,
  990. "threshold": 0.96,
  991. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  992. "color": "#FFFFFFFF",
  993. },
  994. { # Bush Temperate group 4
  995. "type": "BiomeDecalLayer",
  996. "decal_id": [
  997. "BushTemperate12",
  998. "BushTemperate13",
  999. "BushTemperate14",
  1000. "BushTemperate15",
  1001. ],
  1002. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1003. "octaves": 1,
  1004. "frequency": 0.1,
  1005. "fractal_type": FractalType.FractalType_FBm,
  1006. "threshold": 0.96,
  1007. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1008. "color": "#FFFFFFFF",
  1009. },
  1010. { # Bush Temperate group 5
  1011. "type": "BiomeDecalLayer",
  1012. "decal_id": ["BushTemperate16", "BushTemperate17", "BushTemperate18"],
  1013. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1014. "octaves": 1,
  1015. "frequency": 0.1,
  1016. "fractal_type": FractalType.FractalType_FBm,
  1017. "threshold": 0.96,
  1018. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1019. "color": "#FFFFFFFF",
  1020. },
  1021. { # Bush Temperate group 6
  1022. "type": "BiomeDecalLayer",
  1023. "decal_id": [
  1024. "BushTemperate19",
  1025. "BushTemperate20",
  1026. "BushTemperate21",
  1027. "BushTemperate22",
  1028. ],
  1029. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1030. "octaves": 1,
  1031. "frequency": 0.1,
  1032. "fractal_type": FractalType.FractalType_FBm,
  1033. "threshold": 0.96,
  1034. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1035. "color": "#FFFFFFFF",
  1036. },
  1037. { # Bush Temperate group 7
  1038. "type": "BiomeDecalLayer",
  1039. "decal_id": ["BushTemperate23", "BushTemperate24", "BushTemperate25"],
  1040. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1041. "octaves": 1,
  1042. "frequency": 0.1,
  1043. "fractal_type": FractalType.FractalType_FBm,
  1044. "threshold": 0.96,
  1045. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1046. "color": "#FFFFFFFF",
  1047. },
  1048. { # Bush Temperate group 8
  1049. "type": "BiomeDecalLayer",
  1050. "decal_id": ["BushTemperate26", "BushTemperate27", "BushTemperate28"],
  1051. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1052. "octaves": 1,
  1053. "frequency": 0.1,
  1054. "fractal_type": FractalType.FractalType_FBm,
  1055. "threshold": 0.96,
  1056. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1057. "color": "#FFFFFFFF",
  1058. },
  1059. { # Bush Temperate group 9
  1060. "type": "BiomeDecalLayer",
  1061. "decal_id": [
  1062. "BushTemperate29",
  1063. "BushTemperate30",
  1064. "BushTemperate31",
  1065. "BushTemperate32",
  1066. ],
  1067. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1068. "octaves": 1,
  1069. "frequency": 0.1,
  1070. "fractal_type": FractalType.FractalType_FBm,
  1071. "threshold": 0.96,
  1072. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1073. "color": "#FFFFFFFF",
  1074. },
  1075. { # Bush Temperate group 10
  1076. "type": "BiomeDecalLayer",
  1077. "decal_id": [
  1078. "BushTemperate33",
  1079. "BushTemperate34",
  1080. "BushTemperate35",
  1081. "BushTemperate36",
  1082. ],
  1083. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1084. "octaves": 1,
  1085. "frequency": 0.1,
  1086. "fractal_type": FractalType.FractalType_FBm,
  1087. "threshold": 0.96,
  1088. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1089. "color": "#FFFFFFFF",
  1090. },
  1091. { # Bush Temperate group 11 - High grass
  1092. "type": "BiomeDecalLayer",
  1093. "decal_id": [
  1094. "BushTemperate37",
  1095. "BushTemperate38",
  1096. "BushTemperate39",
  1097. "BushTemperate40",
  1098. "BushTemperate41",
  1099. "BushTemperate42",
  1100. ],
  1101. "noise_type": NoiseType.NoiseType_OpenSimplex2,
  1102. "octaves": 1,
  1103. "frequency": 0.1,
  1104. "fractal_type": FractalType.FractalType_FBm,
  1105. "threshold": 0.96,
  1106. "tile_condition": lambda tile: tile == TILEMAP_REVERSE["FloorPlanetGrass"],
  1107. "color": "#FFFFFFFF",
  1108. },
  1109. ]
  1110. # -----------------------------------------------------------------------------
  1111. # Execution
  1112. # -----------------------------------------------------------------------------
  1113. start_time = time.time()
  1114. seed_base = random.randint(0, 1000000)
  1115. print(f"Generated seed: {seed_base}")
  1116. width, height = mapWidth, mapHeight
  1117. chunk_size = 16
  1118. biome_tile_layers = [layer for layer in MAP_CONFIG if layer["type"] == "BiomeTileLayer"]
  1119. biome_entity_layers = [
  1120. layer for layer in MAP_CONFIG if layer["type"] == "BiomeEntityLayer"
  1121. ]
  1122. script_dir = os.path.dirname(os.path.abspath(__file__))
  1123. output_dir = os.path.join(script_dir, "Resources", "Maps", "civ")
  1124. os.makedirs(output_dir, exist_ok=True)
  1125. tile_map = generate_tile_map(width, height, biome_tile_layers, seed_base)
  1126. # Applies erosion to lone sand tiles, overwritting it with surrounding tiles
  1127. tile_map = apply_iterative_erosion(
  1128. tile_map, TILEMAP_REVERSE["FloorSand"], min_neighbors=1
  1129. )
  1130. bordered_tile_map = add_border(tile_map, border_value=TILEMAP_REVERSE["FloorDirt"])
  1131. save_map_to_yaml(
  1132. bordered_tile_map,
  1133. MAP_CONFIG,
  1134. output_dir,
  1135. filename="nomads_classic.yml",
  1136. chunk_size=chunk_size,
  1137. seed_base=seed_base,
  1138. )
  1139. end_time = time.time()
  1140. total_time = end_time - start_time
  1141. print(f"Map generated and saved in {total_time:.2f} seconds!")