301 lines
11 KiB
GDScript
301 lines
11 KiB
GDScript
@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 []
|