@tool extends GridMap class_name TerrainGen signal map_changed() @export_tool_button("Generate", "Reload") var action: Callable = generate @export_tool_button("Test") var test_action: Callable = gen_test @export_group("Map", "map_") ## Dimension for the map @export var map_region: Rect2i = Rect2i(0, 0, 200, 200) ## Maximum height of the map @export var map_max_height: int = 16 ## Chance of a ramp appearing at any spot they can @export_range(0.0, 1.0) var map_ramp_chance: float = 0.1 ## Texture used to place tiles in map @export var map_texture: Texture2D ## Texture for 'biomes' @export var map_biome_texture: Texture2D ## Seed used for random placement within generation @export var map_seed: String = "69420" var map_image: Image var map_biome_image: Image # y orientation 0, 10, 16, 22 ## Dictionary of tiles [br] ## [br] ## Add using: [code]TileItem.new(tile_id, tile_orientation, tile_chance)[/code] var tile_dictionary: Dictionary[String, Array] = { "invalid_tile": [TileItem.new(GridMap.INVALID_CELL_ITEM)], ## used mainly for checking if there is nothing in a position "basalt_block": [TileItem.new(0)], "basalt_floor": [TileItem.new(1, 0, 70), TileItem.new(14, 0, 30)], "basalt_ramp_n": [TileItem.new(2)], "basalt_ramp_e": [TileItem.new(3)], "basalt_ramp_s": [TileItem.new(4)], "basalt_ramp_w": [TileItem.new(5)], "basalt_inner_ne": [TileItem.new(6)], "basalt_inner_se": [TileItem.new(7)], "basalt_inner_sw": [TileItem.new(8)], "basalt_inner_nw": [TileItem.new(9)], "basalt_outer_ne": [TileItem.new(10)], "basalt_outer_se": [TileItem.new(11)], "basalt_outer_sw": [TileItem.new(12)], "basalt_outer_nw": [TileItem.new(13)], "grass_block": [TileItem.new(15)], "grass_floor": [TileItem.new(16)], "grass_ramp_n": [TileItem.new(17, 0, 1)], "grass_ramp_e": [TileItem.new(17, 22, 1)], "grass_ramp_s": [TileItem.new(17, 10, 1)], "grass_ramp_w": [TileItem.new(17, 16, 1)], "grass_inner_ne": [TileItem.new(18, 0, 1)], "grass_inner_se": [TileItem.new(18, 22, 1)], "grass_inner_sw": [TileItem.new(18, 10, 1)], "grass_inner_nw": [TileItem.new(18, 16, 1)], "grass_outer_ne": [TileItem.new(19, 0, 1)], "grass_outer_se": [TileItem.new(19, 22, 1)], "grass_outer_sw": [TileItem.new(19, 10, 1)], "grass_outer_nw": [TileItem.new(19, 16, 1)], } ## Stores html codes for colors to choose the tile, used as a prefix for the tile_dictionary ## format of rrggbb | we can use alpha but requires very small change var biome_dictionary: Dictionary[String, String] = { "000000": "basalt", "ffffff": "grass", "00ff00": "grass", "0000ff": "water", } ## Info for a potential tile in the map gen class TileItem: var id: int = -1 ## id of the tile in the MeshLibrary var orientation: int = 0 ## magical orientation number 0-23 var chance: int = 0 ## chance it will be pulled, not a percentage can be literally any positive number func _init(tile_id: int, tile_orientation: int = 0, tile_chance: int = 1) -> void: self.id = tile_id self.orientation = clamp(tile_orientation, 0, 23) self.chance = tile_chance if tile_chance > 0 else 0 ## Object holding all surrounding heights class Heights: var c: int ## center height of point (the one you pass into get_heights()) var l: int ## lowest height of all the points var n: int ## north height var ne: int ## north-east height var e: int ## east height var se: int ## south-east height var s: int ## south height var sw: int ## south-west height var w: int ## west height var nw: int ## north-west height func gen_test() -> void: clear() seed(map_seed.hash()) var data = [ "basalt_block", "basalt_floor", "basalt_ramp_n", "basalt_ramp_e", "basalt_ramp_s", "basalt_ramp_w", "basalt_inner_ne", "basalt_inner_se", "basalt_inner_sw", "basalt_inner_nw", "basalt_outer_ne", "basalt_outer_se", "basalt_outer_sw", "basalt_outer_nw", "grass_block", "grass_floor", "grass_ramp_n", "grass_ramp_e", "grass_ramp_s", "grass_ramp_w", "grass_inner_ne", "grass_inner_se", "grass_inner_sw", "grass_inner_nw", "grass_outer_ne", "grass_outer_se", "grass_outer_sw", "grass_outer_nw", ] var x: int = 0 for item: String in data: place_tile(Vector3i(x, map_max_height / 2, 0), item) x += 1 #endfor ## Generates the map, this is a relatively [b]expensive[/b] function so be careful :> func generate() -> void: if map_texture == null: return if map_biome_texture == null: return map_image = map_texture.get_image() map_biome_image = map_biome_texture.get_image() clear() seed(map_seed.hash()) for x: int in range(map_region.position.x, map_region.position.x + map_region.size.x): for y: int in range(map_region.position.y, map_region.position.y + map_region.size.y): var points: Heights = get_heights(x, y) var point_3d: Vector3i = Vector3i(x, points.c, y) # get biome var biome: String = get_biome(x, y) # floor place_tile(point_3d, "%s_floor" % [biome]) # block in cliffs for i: int in range(points.l - 1, points.c): place_tile(Vector3i(x, i, y), "%s_block" % [biome]) var ramp_roll: float = randf() if ramp_roll > map_ramp_chance: continue # cardinal direction ramps if points.n - 1 == points.c: place_tile(point_3d, "%s_block" % [biome]) place_tile(point_3d + Vector3i(0, 1, 0), "%s_ramp_n" % [biome]) elif points.e - 1 == points.c: place_tile(point_3d, "%s_block" % [biome]) place_tile(point_3d + Vector3i(0, 1, 0), "%s_ramp_e" % [biome]) elif points.s - 1 == points.c: place_tile(point_3d, "%s_block" % [biome]) place_tile(point_3d + Vector3i(0, 1, 0), "%s_ramp_s" % [biome]) elif points.w - 1 == points.c: place_tile(point_3d, "%s_block" % [biome]) place_tile(point_3d + Vector3i(0, 1, 0), "%s_ramp_w" % [biome]) # replace cardinal ramps w/ inner ramps if points.n - 1 == points.c && points.e - 1 == points.c: place_tile(point_3d + Vector3i(0, 1, 0), "%s_inner_ne" % [biome]) if points.s - 1 == points.c && points.e - 1 == points.c: place_tile(point_3d + Vector3i(0, 1, 0), "%s_inner_se" % [biome]) if points.s - 1 == points.c && points.w - 1 == points.c: place_tile(point_3d + Vector3i(0, 1, 0), "%s_inner_sw" % [biome]) if points.n - 1 == points.c && points.w - 1 == points.c: place_tile(point_3d + Vector3i(0, 1, 0), "%s_inner_nw" % [biome]) #endfor for x: int in range(map_region.position.x, map_region.position.x + map_region.size.x): for y: int in range(map_region.position.y, map_region.position.y + map_region.size.y): var point_3d: Vector3i = Vector3i(x, get_height(x, y), y) var biome: String = get_biome(x, y) if !has_tile(point_3d + Vector3i(0, 1, 0), "invalid_tile"): continue # add outer ramps by checking nearby tiles if has_tile(point_3d + Vector3i(-1, 1, 0), "%s_ramp_n" % [biome]): if has_tile(point_3d + Vector3i(0, 1, 1), "%s_ramp_e" % [biome]): place_tile(point_3d + Vector3i(0, 1, 0), "%s_outer_ne" % [biome]) place_tile(point_3d + Vector3i(0, 0, 0), "%s_block" % [biome]) if has_tile(point_3d + Vector3i(-1, 1, 0), "%s_ramp_s" % [biome]): if has_tile(point_3d + Vector3i(0, 1, -1), "%s_ramp_e" % [biome]): place_tile(point_3d + Vector3i(0, 1, 0), "%s_outer_se" % [biome]) place_tile(point_3d + Vector3i(0, 0, 0), "%s_block" % [biome]) if has_tile(point_3d + Vector3i(1, 1, 0), "%s_ramp_n" % [biome]): if has_tile(point_3d + Vector3i(0, 1, 1), "%s_ramp_w" % [biome]): place_tile(point_3d + Vector3i(0, 1, 0), "%s_outer_nw" % [biome]) place_tile(point_3d + Vector3i(0, 0, 0), "%s_block" % [biome]) if has_tile(point_3d + Vector3i(1, 1, 0), "%s_ramp_s" % [biome]): if has_tile(point_3d + Vector3i(0, 1, -1), "%s_ramp_w" % [biome]): place_tile(point_3d + Vector3i(0, 1, 0), "%s_outer_sw" % [biome]) place_tile(point_3d + Vector3i(0, 0, 0), "%s_block" % [biome]) #endfor map_changed.emit() ## Using the [member map_texture] returns the height of that point on the map func get_height(x: int, y: int) -> int: var fx: float = clamp((x - map_region.position.x) as float / map_region.size.x as float, 0.0, 1.0) var fy: float = clamp((y - map_region.position.y) as float / map_region.size.y as float, 0.0, 1.0) var px: int = floori(fx * (map_image.get_width() - 1)) var py: int = floori(fy * (map_image.get_height() - 1)) #print("x: %s, fx: %s, px: %s" % [x, fx, px]) var col: Color = map_image.get_pixel(px, py) var val: float = col.get_luminance() var height: int = floori(((val + 1)/2.0) * map_max_height) return height ## Using the [member map_biome_texture] returns the biome using the [member biome_dictionary] as a lookup func get_biome(x: int, y: int) -> String: var fx: float = clamp((x - map_region.position.x) as float / map_region.size.x as float, 0.0, 1.0) var fy: float = clamp((y - map_region.position.y) as float / map_region.size.y as float, 0.0, 1.0) var px: int = floori(fx * (map_biome_image.get_width() - 1)) var py: int = floori(fy * (map_biome_image.get_height() - 1)) var col: Color = map_biome_image.get_pixel(px, py) var col_html: String = col.to_html(false) return biome_dictionary.get(col_html, "ERROR") ## Given a point on the map, returns an object containing the heights of all ## surrounding points func get_heights(x: int, y: int) -> Heights: var h: Heights = Heights.new() h.n = get_height(x, y + 1) h.ne = get_height(x - 1, y + 1) h.e = get_height(x - 1, y) h.se = get_height(x - 1, y - 1) h.s = get_height(x, y - 1) h.sw = get_height(x + 1, y - 1) h.w = get_height(x + 1, y) h.nw = get_height(x + 1, y + 1) h.c = get_height(x, y) h.l = min(h.n, h.ne, h.e, h.se, h.s, h.sw, h.w, h.nw, h.c - 1) return h ## Returns whether a specific tile exists at a specific point func has_tile(pos: Vector3i, tile_name: String) -> bool: var tile_list: Array[TileItem] = [] var tile_res: Array = tile_dictionary.get(tile_name, []) tile_list.assign(tile_res) var tile_id: int = get_cell_item(pos) return tile_list.reduce(func (acc: bool, tile: TileItem) -> bool: return acc || tile.id == tile_id, false) ## When given a tile name, it uses the [member tile_dictionary] lookup to roll a tile based on chance func get_tile(tile_name: String) -> TileItem: var tile_list: Array[TileItem] = [] var tile_res: Array = tile_dictionary.get(tile_name, []) tile_list.assign(tile_res) if len(tile_list) == 1: return tile_list[0] var roll_max: int = tile_list.reduce(func(acc: int, t: TileItem) -> bool: return acc + t.chance, 0) var roll: int = randi_range(0, roll_max) var roll_total: int = 0 for tile in tile_list: if roll_total + tile.chance >= roll: #print("%d : %d -> [%d]" % [roll_max, roll, tile.id]) return tile roll_total += tile.chance #endfor assert(false, "Should be unreachable") return TileItem.new(-1) ## Places a tile in the map given a tile name, will do lookup and roll the tile ## before placing func place_tile(tile_position: Vector3i, tile_name: String) -> void: var tile: TileItem = get_tile(tile_name) set_cell_item(tile_position, tile.id, tile.orientation) ## Function stub for getting tiles in a specific sphere, is going to be used ## for doing ground deformation post processing ## @experimental func get_tiles_in_sphere(_pos: Vector3, _size: float) -> Array[Vector3]: return []