TowerGame/TerrainGen.gd

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 []