diff --git a/addons/PoissonDiscSampling/PoissonDiscSampling.gd b/addons/PoissonDiscSampling/PoissonDiscSampling.gd new file mode 100644 index 0000000..a47ea72 --- /dev/null +++ b/addons/PoissonDiscSampling/PoissonDiscSampling.gd @@ -0,0 +1,137 @@ +class_name PoissonDiscSampling + +var _radius: float +var _sample_region_shape +var _retries: int +var _start_pos: Vector2 +var _sample_region_rect: Rect2 +var _cell_size: float +var _rows: int +var _cols: int +var _cell_size_scaled: Vector2 +var _grid: Array = [] +var _points: Array = [] +var _spawn_points: Array = [] +var _transpose: Vector2 + +# radius - minimum distance between points +# sample_region_shape - takes any of the following: +# -a Rect2 for rectangular region +# -an array of Vector2 for polygon region +# -a Vector3 with x,y as the position and z as the radius of the circle +# retries - maximum number of attempts to look around a sample point, reduce this value to speed up generation +# start_pos - optional parameter specifying the starting point +# +# returns an Array of Vector2D with points in the order of their discovery +func generate_points(radius: float, sample_region_shape, retries: int, start_pos := Vector2(INF, INF)) -> Array: + _radius = radius + _sample_region_shape = sample_region_shape + _retries = retries + _start_pos = start_pos + _init_vars() + + while _spawn_points.size() > 0: + var spawn_index: int = randi() % _spawn_points.size() + var spawn_centre: Vector2 = _spawn_points[spawn_index] + var sample_accepted: bool = false + for i in retries: + var angle: float = 2 * PI * randf() + var sample: Vector2 = spawn_centre + Vector2(cos(angle), sin(angle)) * (radius + radius * randf()) + if _is_valid_sample(sample): + _grid[int((_transpose.x + sample.x) / _cell_size_scaled.x)][int((_transpose.y + sample.y) / _cell_size_scaled.y)] = _points.size() + _points.append(sample) + _spawn_points.append(sample) + sample_accepted = true + break + if not sample_accepted: + _spawn_points.remove(spawn_index) + return _points + + +func _is_valid_sample(sample: Vector2) -> bool: + if _is_point_in_sample_region(sample): + var cell := Vector2(int((_transpose.x + sample.x) / _cell_size_scaled.x), int((_transpose.y + sample.y) / _cell_size_scaled.y)) + var cell_start := Vector2(max(0, cell.x - 2), max(0, cell.y - 2)) + var cell_end := Vector2(min(cell.x + 2, _cols - 1), min(cell.y + 2, _rows - 1)) + + for i in range(cell_start.x, cell_end.x + 1): + for j in range(cell_start.y, cell_end.y + 1): + var search_index: int = _grid[i][j] + if search_index != -1: + var dist: float = _points[search_index].distance_to(sample) + if dist < _radius: + return false + return true + return false + + +func _is_point_in_sample_region(sample: Vector2) -> bool: + if _sample_region_rect.has_point(sample): + match typeof(_sample_region_shape): + TYPE_RECT2: + return true + TYPE_VECTOR2_ARRAY, TYPE_ARRAY: + if Geometry.is_point_in_polygon(sample, _sample_region_shape): + return true + TYPE_VECTOR3: + if Geometry.is_point_in_circle(sample, Vector2(_sample_region_shape.x, _sample_region_shape.y), _sample_region_shape.z): + return true + _: + return false + return false + +func _init_vars() -> void: + randomize() + + # identify the type of shape and it's bounding rectangle and starting point + match typeof(_sample_region_shape): + TYPE_RECT2: + _sample_region_rect = _sample_region_shape + if _start_pos.x == INF: + _start_pos.x = _sample_region_rect.position.x + _sample_region_rect.size.x * randf() + _start_pos.y = _sample_region_rect.position.y + _sample_region_rect.size.y * randf() + + TYPE_VECTOR2_ARRAY, TYPE_ARRAY: + var start: Vector2 = _sample_region_shape[0] + var end: Vector2 = _sample_region_shape[0] + for i in range(1, _sample_region_shape.size()): + start.x = min(start.x, _sample_region_shape[i].x) + start.y = min(start.y, _sample_region_shape[i].y) + end.x = max(end.x, _sample_region_shape[i].x) + end.y = max(end.y, _sample_region_shape[i].y) + _sample_region_rect = Rect2(start, end - start) + if _start_pos.x == INF: + var n: int = _sample_region_shape.size() + var i: int = randi() % n + _start_pos = _sample_region_shape[i] + (_sample_region_shape[(i + 1) % n] - _sample_region_shape[i]) * randf() + + TYPE_VECTOR3: + var x = _sample_region_shape.x + var y = _sample_region_shape.y + var r = _sample_region_shape.z + _sample_region_rect = Rect2(x - r, y - r, r * 2, r * 2) + if _start_pos.x == INF: + var angle: float = 2 * PI * randf() + _start_pos = Vector2(x, y) + Vector2(cos(angle), sin(angle)) * r * randf() + _: + _sample_region_shape = Rect2(0, 0, 0, 0) + push_error("Unrecognized shape!!! Please input a valid shape") + + _cell_size = _radius / sqrt(2) + _cols = max(floor(_sample_region_rect.size.x / _cell_size), 1) + _rows = max(floor(_sample_region_rect.size.y / _cell_size), 1) + # scale the cell size in each axis + _cell_size_scaled.x = _sample_region_rect.size.x / _cols + _cell_size_scaled.y = _sample_region_rect.size.y / _rows + # use tranpose to map points starting from origin to calculate grid position + _transpose = -_sample_region_rect.position + + _grid = [] + for i in _cols: + _grid.append([]) + for j in _rows: + _grid[i].append(-1) + + _points = [] + _spawn_points = [] + _spawn_points.append(_start_pos) diff --git a/addons/delaunator/Delaunator.gd b/addons/delaunator/Delaunator.gd new file mode 100644 index 0000000..7860fd2 --- /dev/null +++ b/addons/delaunator/Delaunator.gd @@ -0,0 +1,571 @@ +extends Reference + +class_name Delaunator + +const EPSILON = pow(2, -52) +const EDGE_STACK = [] + +var coords = [] # PoolRealArray. +var halfedges = [] # PoolIntArray. +var hull = [] # PoolIntArray. +var triangles = [] # PoolIntArray. +var triangles_len = 0 +var _cx +var _cy +var _dists = [] # PoolRealArray. +var _halfedges = [] # This array should be a PoolIntArray but we need to use the .slice() function on it. +var _hash_size +var _hull_hash = [] # PoolIntArray. +var _hull_next = [] # PoolIntArray. +var _hull_prev = [] # PoolIntArray. +var _hull_start +var _hull_tri = [] # PoolIntArray. +var _ids = [] # PoolIntArray. +var _triangles = [] # This array should be a PoolIntArray but we need to use the .slice() function on it. + +func _init(points): + if points.size() < 3: + push_error(ProjectSettings.get_setting("application/config/name") + " needs at least 3 points.") + return + + EDGE_STACK.resize(512) + + var n = points.size() + + coords.resize(n * 2) + + for i in n: + var point = points[i] + coords[2 * i] = point.x + coords[2 * i + 1] = point.z + + _constructor() + +func _constructor(): + var n = coords.size() >> 1 + + # Arrays that will store the triangulation graph. + var max_triangles = max(2 * n - 5, 0) + _triangles.resize(max_triangles * 3) + _halfedges.resize(max_triangles * 3) + + # Temporary arrays for tracking the edges of the advancing convex hull. + _hash_size = ceil(sqrt(n)) + _hull_prev.resize(n) # Edge to prev edge. + _hull_next.resize(n) # Edge to next edge. + _hull_tri.resize(n) # Edge to adjacent triangle. + + _hull_hash.resize(_hash_size) + for i in _hash_size: + _hull_hash[i] = -1 # angular edge hash + + # Temporary arrays for sorting points. + _ids.resize(n) + _dists.resize(n) + + update() + + +func update(): + var n = coords.size() >> 1 + + # Populate an array of point indices; calculate input data bbox. + var min_x = INF + var min_y = INF + var max_x = -INF + var max_y = -INF + + for i in n: + var x = coords[2 * i] + var y = coords[2 * i + 1] + if x < min_x: min_x = x + if y < min_y: min_y = y + if x > max_x: max_x = x + if y > max_y: max_y = y + _ids[i] = i + + var cx = (min_x + max_x) / 2 + var cy = (min_y + max_y) / 2 + + var min_dist = INF + var i0 = 0 + var i1 = 0 + var i2 = 0 + + # Pick a seed point close to the center. + for i in n: + var d = dist(cx, cy, coords[2 * i], coords[2 * i + 1]) + if (d < min_dist): + i0 = i + min_dist = d + var i0x = coords[2 * i0] + var i0y = coords[2 * i0 + 1] + + min_dist = INF + + # Find the point closest to the seed. + for i in n: + if i == i0: continue + var d = dist(i0x, i0y, coords[2 * i], coords[2 * i + 1]) + if (d < min_dist and d > 0): + i1 = i + min_dist = d + var i1x = coords[2 * i1] + var i1y = coords[2 * i1 + 1] + + var min_radius = INF + + # Find the third point which forms the smallest circumcircle with the first two. + for i in n: + if i == i0 or i == i1: continue + var r = circumradius(i0x, i0y, i1x, i1y, coords[2 * i], coords[2 * i + 1]) + if r < min_radius: + i2 = i + min_radius = r + var i2x = coords[2 * i2] + var i2y = coords[2 * i2 + 1] + + if min_radius == INF: + # Order collinear points by dx (or dy if all x are identical) + # and return the list as a hull. + for i in n: + var _dist_temp + + if coords[2 * i] - coords[0]: + _dist_temp = coords[2 * i] - coords[0] + elif coords[2 * i + 1] - coords[1]: + _dist_temp = coords[2 * i + 1] - coords[1] + else: + _dist_temp = 0 + + _dists[i] = _dist_temp + + quicksort(_ids, _dists, 0, n - 1) + var hull = [] + hull.resize(n) + var j = 0 + var d0 = -INF + + for i in n: + var id = _ids[i] + if _dists[id] > d0: + hull[j] = id + j += 1 + d0 = _dists[id] + hull = hull.slice(0, j - 1) + triangles = [] + halfedges = [] + + return + + # Swap the order of the seed points for counter-clockwise orientation. + if orient(i0x, i0y, i1x, i1y, i2x, i2y): + var i = i1 + var x = i1x + var y = i1y + i1 = i2 + i1x = i2x + i1y = i2y + i2 = i + i2x = x + i2y = y + + var center = circumcenter(i0x, i0y, i1x, i1y, i2x, i2y) + _cx = center[0] + _cy = center[1] + + for i in n: + _dists[i] = dist(coords[2 * i], coords[2 * i + 1], center[0], center[1]) + + # Sort the points by distance from the seed triangle circumcenter. + quicksort(_ids, _dists, 0, n - 1) + + # Set up the seed triangle as the starting hull. + _hull_start = i0 + var hull_size = 3 + + _hull_next[i0] = i1 + _hull_prev[i2] = i1 + _hull_next[i1] = i2 + _hull_prev[i0] = i2 + _hull_next[i2] = i0 + _hull_prev[i1] = i0 + + _hull_tri[i0] = 0 + _hull_tri[i1] = 1 + _hull_tri[i2] = 2 + + for i in _hull_hash.size(): + _hull_hash[i] = -1 + _hull_hash[_hash_key(i0x, i0y)] = i0 + _hull_hash[_hash_key(i1x, i1y)] = i1 + _hull_hash[_hash_key(i2x, i2y)] = i2 + +# triangles_len = 0 + _add_triangle(i0, i1, i2, -1, -1, -1) + + var xp = 0 + var yp = 0 + + for k in _ids.size(): + var i = _ids[k] + var x = coords[2 * i] + var y = coords[2 * i + 1] + + # Skip near-duplicate points. + if k > 0 and abs(x - xp) <= EPSILON and abs(y - yp) <= EPSILON: continue + + xp = x + yp = y + + # Skip seed triangle points. + if i == i0 or i == i1 or i == i2: continue + + # Find a visible edge on the convex hull using edge hash. + var start = 0 + var key = _hash_key(x, y) + + for j in _hash_size: + start = _hull_hash[fmod((key + j), _hash_size)] + if (start != -1 and start != _hull_next[start]): break + + start = _hull_prev[start] + var e = start + + while true: + var q = _hull_next[e] + if orient(x, y, coords[2 * e], coords[2 * e + 1], coords[2 * q], coords[2 * q + 1]): break + e = q + + if (e == start): + e = -1 + break + + if (e == -1): continue # Likely a near-duplicate point; Skip it. + + # Add the first triangle from the point. + var t = _add_triangle(e, i, _hull_next[e], -1, -1, _hull_tri[e]) + # Recursively flip triangles from the point until they satisfy the Delaunay condition. + _hull_tri[i] = _legalize(t + 2) + _hull_tri[e] = t # Keep track of boundary triangles on the hull. + hull_size += 1 + + # Walk forward through the hull, adding more triangles and flipping recursively. + n = _hull_next[e] + + while true: + var q = _hull_next[n] + if not orient(x, y, coords[2 * n], coords[2 * n + 1], coords[2 * q], coords[2 * q + 1]): break + t = _add_triangle(n, i, q, _hull_tri[i], -1, _hull_tri[n]) + _hull_tri[i] = _legalize(t + 2) + _hull_next[n] = n # Mark as removed. + hull_size -= 1 + n = q + + # Walk backward from the other side, adding more triangles and flipping. + if (e == start): + while true: + var q = _hull_prev[e] + if not orient(x, y, coords[2 * q], coords[2 * q + 1], coords[2 * e], coords[2 * e + 1]): break + t = _add_triangle(q, i, e, -1, _hull_tri[e], _hull_tri[q]) + _legalize(t + 2) + _hull_tri[q] = t + _hull_next[e] = e # Mark as removed. + hull_size -= 1 + e = q + + # Update the hull indices. + _hull_start = e + _hull_prev[i] = e + _hull_next[e] = i + _hull_prev[n] = i + _hull_next[i] = n + + # Save the two new edges in the hash table. + _hull_hash[_hash_key(x, y)] = i + _hull_hash[_hash_key(coords[2 * e], coords[2 * e + 1])] = e + + hull.resize(hull_size) + var e = _hull_start + for i in hull_size: + hull[i] = e + e = _hull_next[e] + + # Trim typed triangle mesh arrays. + triangles = _triangles.slice(0, triangles_len - 1) + halfedges = _halfedges.slice(0, triangles_len - 1) + + +func _hash_key(x, y): + return fmod(floor(pseudo_angle(x - _cx, y - _cy) * _hash_size), _hash_size) + + +func _legalize(a): + var i = 0 + var ar = 0 + + # Recursion eliminated with a fixed-size stack. + while true: + var b = _halfedges[a] + +# If the pair of triangles doesn't satisfy the Delaunay condition +# (p1 is inside the circumcircle of [p0, pl, pr]), flip them, +# then do the same check/flip recursively for the new pair of triangles +# +# pl pl +# /||\ / \ +# al/ || \bl al/ \a +# / || \ / \ +# / a||b \ flip /___ar___\ +# p0\ || /p1 => p0\---bl---/p1 +# \ || / \ / +# ar\ || /br b\ /br +# \||/ \ / +# pr pr + + var a0 = a - a % 3 + ar = a0 + (a + 2) % 3 + + if b == -1: # Convex hull edge. + if i == 0: break + i -= 1 + a = EDGE_STACK[i] + continue + + var b0 = b - b % 3 + var al = a0 + (a + 1) % 3 + var bl = b0 + (b + 2) % 3 + + var p0 = _triangles[ar] + var pr = _triangles[a] + var pl = _triangles[al] + var p1 = _triangles[bl] + + var illegal = in_circle( + coords[2 * p0], coords[2 * p0 + 1], + coords[2 * pr], coords[2 * pr + 1], + coords[2 * pl], coords[2 * pl + 1], + coords[2 * p1], coords[2 * p1 + 1] + ) + + if illegal: + _triangles[a] = p1 + _triangles[b] = p0 + + var hbl = _halfedges[bl] + + # Edge swapped on the other side of the hull (rare); Fix the halfedge reference. + if (hbl == -1): + var e = _hull_start + while true: + if _hull_tri[e] == bl: + _hull_tri[e] = a + break + + e = _hull_prev[e] + if e == _hull_start: break + + _link(a, hbl) + _link(b, _halfedges[ar]) + _link(ar, bl) + + var br = b0 + (b + 1) % 3 + + # Don't worry about hitting the cap: it can only happen on extremely degenerate input. + if i < EDGE_STACK.size(): + EDGE_STACK[i] = br + i += 1 + else: + if i == 0: break + i -= 1 + a = EDGE_STACK[i] + + return ar + + +func _link(a, b): + _halfedges[a] = b + if (b != -1): + _halfedges[b] = a + + +# Add a new triangle given vertex indices and adjacent half-edge ids. +func _add_triangle(i0, i1, i2, a, b, c): + var t = triangles_len + + _triangles[t] = i0 + _triangles[t + 1] = i1 + _triangles[t + 2] = i2 + + _link(t, a) + _link(t + 1, b) + _link(t + 2, c) + + triangles_len += 3 + + return t + + +# Monotonically increases with real angle, but doesn't need expensive trigonometry. +func pseudo_angle(dx, dy): + var p = dx / (abs(dx) + abs(dy)) + + if (dy > 0): + return (3 - p) / 4 # [0..1] + else: + return (1 + p) / 4 # [0..1] + + +func dist(ax, ay, bx, by): + var dx = ax - bx + var dy = ay - by + return dx * dx + dy * dy + + +# Return 2d orientation sign if we're confident in it through J. Shewchuk's error bound check. +func orient_if_sure(px, py, rx, ry, qx, qy): + var l = (ry - py) * (qx - px) + var r = (rx - px) * (qy - py) + + if (abs(l - r) >= 0.00000000000000033306690738754716 * abs(l + r)): + return l - r + else: + return 0 + + +# A more robust orientation test that's stable in a given triangle (to fix robustness issues). +func orient(rx, ry, qx, qy, px, py): + var _sign + + if orient_if_sure(px, py, rx, ry, qx, qy): + _sign = orient_if_sure(px, py, rx, ry, qx, qy) + elif orient_if_sure(rx, ry, qx, qy, px, py): + _sign = orient_if_sure(rx, ry, qx, qy, px, py) + elif orient_if_sure(qx, qy, px, py, rx, ry): + _sign = orient_if_sure(qx, qy, px, py, rx, ry) + + return false if _sign == null else _sign < 0 + + +func in_circle(ax, ay, bx, by, cx, cy, px, py): + var dx = ax - px + var dy = ay - py + var ex = bx - px + var ey = by - py + var fx = cx - px + var fy = cy - py + + var ap = dx * dx + dy * dy + var bp = ex * ex + ey * ey + var cp = fx * fx + fy * fy + + return dx * (ey * cp - bp * fy) -\ + dy * (ex * cp - bp * fx) +\ + ap * (ex * fy - ey * fx) < 0 + + +func circumradius(ax, ay, bx, by, cx, cy): + var dx = bx - ax + var dy = by - ay + var ex = cx - ax + var ey = cy - ay + + var bl = dx * dx + dy * dy + var cl = ex * ex + ey * ey + + # When you divide by 0 in Godot you get an error. + # It should return INF (positive or negative). + var d + if (dx * ey - dy * ex) == 0: + d = INF + elif (dx * ey - dy * ex) == -0: + d = -INF + else: + d = 0.5 / (dx * ey - dy * ex) + + var x = (ey * bl - dy * cl) * d + var y = (dx * cl - ex * bl) * d + + return x * x + y * y + + +func circumcenter(ax, ay, bx, by, cx, cy): + var dx = bx - ax + var dy = by - ay + var ex = cx - ax + var ey = cy - ay + + var bl = dx * dx + dy * dy + var cl = ex * ex + ey * ey + + # When you divide by 0 in Godot you get an error. + # It should return INF (positive or negative). + var d + if (dx * ey - dy * ex) == 0: + d = INF + elif (dx * ey - dy * ex) == -0: + d = -INF + else: + d = 0.5 / (dx * ey - dy * ex) + + var x = ax + (ey * bl - dy * cl) * d + var y = ay + (dx * cl - ex * bl) * d + + return [x, y] + + +func quicksort(ids, dists, left, right): + if right - left <= 20: + for i in range(left + 1, right + 1): + var temp = ids[i] + var temp_dist = dists[temp] + var j = i - 1 + while j >= left and dists[ids[j]] > temp_dist: + ids[j + 1] = ids[j] + j -= 1 + ids[j + 1] = temp + else: + var median = (left + right) >> 1 + var i = left + 1 + var j = right + swap(ids, median, i) + + if (dists[ids[left]] > dists[ids[right]]): + swap(ids, left, right) + + if (dists[ids[i]] > dists[ids[right]]): + swap(ids, i, right) + + if (dists[ids[left]] > dists[ids[i]]): + swap(ids, left, i) + + var temp = ids[i] + var temp_dist = dists[temp] + + while true: + while true: + i += 1 + if dists[ids[i]] >= temp_dist: break + + while true: + j -= 1 + if dists[ids[j]] <= temp_dist: break + + if j < i: break + swap(ids, i, j) + + ids[left + 1] = ids[j] + ids[j] = temp + + if right - i + 1 >= j - left: + quicksort(ids, dists, i, right) + quicksort(ids, dists, left, j - 1) + else: + quicksort(ids, dists, left, j - 1) + quicksort(ids, dists, i, right) + + +func swap(arr, i, j): + var tmp = arr[i] + arr[i] = arr[j] + arr[j] = tmp diff --git a/default_env.tres b/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..c98fbb6 Binary files /dev/null and b/icon.png differ diff --git a/icon.png.import b/icon.png.import new file mode 100644 index 0000000..a4c02e6 --- /dev/null +++ b/icon.png.import @@ -0,0 +1,35 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] + +[params] + +compress/mode=0 +compress/lossy_quality=0.7 +compress/hdr_mode=0 +compress/bptc_ldr=0 +compress/normal_map=0 +flags/repeat=0 +flags/filter=true +flags/mipmaps=false +flags/anisotropic=false +flags/srgb=2 +process/fix_alpha_border=true +process/premult_alpha=false +process/HDR_as_SRGB=false +process/invert_color=false +process/normal_map_invert_y=false +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..5fbb305 --- /dev/null +++ b/project.godot @@ -0,0 +1,49 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=4 + +_global_script_classes=[ { +"base": "Reference", +"class": "Delaunator", +"language": "GDScript", +"path": "res://addons/delaunator/Delaunator.gd" +}, { +"base": "Reference", +"class": "PoissonDiscSampling", +"language": "GDScript", +"path": "res://addons/PoissonDiscSampling/PoissonDiscSampling.gd" +}, { +"base": "Reference", +"class": "Terrain", +"language": "GDScript", +"path": "res://utils/terrain/Terrain.gd" +} ] +_global_script_class_icons={ +"Delaunator": "", +"PoissonDiscSampling": "", +"Terrain": "" +} + +[application] + +config/name="Societer" +run/main_scene="res://world/game.tscn" +config/icon="res://icon.png" + +[gui] + +common/drop_mouse_on_gui_input_disabled=true + +[physics] + +common/enable_pause_aware_picking=true + +[rendering] + +environment/default_environment="res://default_env.tres" diff --git a/ui/map/map.gd b/ui/map/map.gd new file mode 100644 index 0000000..b06d140 --- /dev/null +++ b/ui/map/map.gd @@ -0,0 +1,116 @@ +extends Node2D + +var terrain + +func create_map(): + var river = {"size": 3, "color": "blue"} + + terrain.get_edge(16).set_data("river", river) + + var triangle_idx = 5 + var triangle = terrain.get_triangle(triangle_idx) + + print("Triangle index : %d" % (triangle.get_index())) + + var edges = triangle.edges() + + print("Number of edges : %d" % (edges.size())) + print() + + for edge in edges: + print("Edge index : %d" % (edge.get_index())) + var start_point = edge.start() + var end_point = edge.end() + var start = start_point.point2d() + var end = end_point.point2d() + + print("Start point index : %d" % (start_point.get_index())) + print("End point index : %d" % (end_point.get_index())) + + print("Start point : %s" % (start)) + print("End point : %s" % (end)) + + if edge.has_key("river"): + print("Has river") + var a_river = edge.get_data("river") + print("River size : %d" % (a_river["size"])) + print("River color : %s" % (a_river["color"])) + + print() + print(terrain.get_point(5).point3d()) + + +func draw_triangles(): + for polygon in terrain.get_triangles_as_polygon(): + var color = Color(randf(), randf(), randf(), 1) + if polygon.size() > 2: + draw_polygon(polygon, PoolColorArray([color])) + +func draw_triangles_edges(color=Color("#000000")): + for line in terrain.get_edges_as_line(): + draw_line(line[0], line[1], color) + +func draw_voronoi_edges(color=Color("#000000")): + for line in terrain.get_voronoi_edges_as_line(): + draw_line(line[0], line[1], color) + +func draw_voronoi_cells_old(): + var seen = [] + for edge_idx in terrain.edges(): + var triangles = [] + var vertices = [] + var p = terrain._triangles[terrain.next_half_edge(edge_idx)] + if not seen.has(p): + seen.append(p) + var edges = terrain.edges_around_point(edge_idx) + for edge_around_idx in edges: + triangles.append(terrain.triangle_of_edge(edge_around_idx)) + for triangle in triangles: + vertices.append(terrain.triangle_center(triangle)) + + if triangles.size() > 2: + var color = Color(randf(), randf(), randf(), 1) + var voronoi_cell = PoolVector2Array() + for vertice in vertices: + voronoi_cell.append(Vector2(vertice.x, vertice.z)) + draw_polygon(voronoi_cell, PoolColorArray([color])) +func draw_voronoi_cells(): + for polygon in terrain.get_voronoi_cells_as_polygon(): + var color = Color(randf(), randf(), randf(), 1) + if polygon.size() > 2: + draw_polygon(polygon, PoolColorArray([color])) + +func draw_voronoi_cells_convex_hull(): + for point_idx in terrain.points(): + var triangles = [] + var vertices = [] + var incoming = terrain._points_to_half_edges.get(point_idx) + + if incoming == null: + triangles.append(0) + else: + var edges = terrain.edges_around_point(incoming) + for edge_idx in edges: + triangles.append(terrain.triangle_of_edge(edge_idx)) + + for triangle_idx in triangles: + vertices.append(terrain.triangle_center(triangle_idx)) + + if triangles.size() > 2: + var color = Color(randf(), randf(), randf(), 1) + var voronoi_cell = PoolVector2Array() + for vertice in vertices: + voronoi_cell.append(Vector2(vertice[0], vertice[1])) + draw_polygon(voronoi_cell, PoolColorArray([color])) + +func _draw(): + print("before drawing") + draw_triangles() +# draw_voronoi_cells() +# draw_triangles_edges() + # draw_voronoi_cells_convex_hull() +# draw_voronoi_edges(Color("#ff0000")) + +func _on_Game_world_loaded(game_terrain): + terrain = game_terrain + create_map() diff --git a/ui/map/map.tscn b/ui/map/map.tscn new file mode 100644 index 0000000..3ad22d3 --- /dev/null +++ b/ui/map/map.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://ui/map/map.gd" type="Script" id=1] + +[node name="Map" type="Node2D"] +script = ExtResource( 1 ) diff --git a/ui/ui.tscn b/ui/ui.tscn new file mode 100644 index 0000000..8fd1d01 --- /dev/null +++ b/ui/ui.tscn @@ -0,0 +1,7 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://ui/map/map.tscn" type="PackedScene" id=1] + +[node name="UI" type="Node2D"] + +[node name="Map" parent="." instance=ExtResource( 1 )] diff --git a/utils/terrain/Terrain.gd b/utils/terrain/Terrain.gd new file mode 100644 index 0000000..f2aecfc --- /dev/null +++ b/utils/terrain/Terrain.gd @@ -0,0 +1,332 @@ +extends Reference + +class_name Terrain + +class Triangle: + var _idx + var _terrain + + func _init(idx, terrain): + self._idx = idx + self._terrain = terrain + + func get_index(): + return _idx + + func has_key(key): + return _terrain._triangles_data[_idx].has(key) + + func set_data(key,value): + var data = _terrain._triangles_data[_idx] + data[key] = value + + func get_data(key): + var data = _terrain._triangles_data[_idx] + if data.has(key): + return data[key] + + func edges(): + return [Edge.new(3 * _idx, _terrain), Edge.new(3 * _idx + 1, _terrain), Edge.new(3 * _idx + 2, _terrain)] + + func points(): + var list_points = [] + for edge in edges(): + list_points.append(Point.new(_terrain._triangles[edge._idx], _terrain)) + return list_points + + func triangles_adjacent(): + var list_triangles = [] + for edge in edges(): + var opposite = Edge.new(_terrain._halfedges[edge._idx], _terrain) + if opposite._idx >= 0: + list_triangles.append(opposite.triangle()) + return list_triangles + + func center2d(): + var points = points() + return (points[0].point2d() + points[1].point2d() + points[2].point2d()) / 3.0 + + func center3d(): + var points = points() + return (points[0].point3d() + points[1].point3d() + points[2].point3d()) / 3.0 + + +class Point: + var _idx + var _terrain + + func _init(idx, terrain): + self._idx = idx + self._terrain = terrain + + func get_index(): + return _idx + + func has_key(key): + return _terrain._points_data[_idx].has(key) + + func set_data(key,value): + var data = _terrain._points_data[_idx] + data[key] = value + + func get_data(key): + var data = _terrain._points_data[_idx] + if data.has(key): + return data[key] + + func point3d(): + return _terrain._points[_idx] + + func point2d(): + var point3d:Vector3 = _terrain._points[_idx] + var point2d:Vector2 = Vector2(point3d.x, point3d.z) + return(point2d) + + func set_elevation(elevation:float): + _terrain._points[_idx].y = elevation + + func get_elevation(): + return(_terrain._points[_idx].y) + + func edges_around(): + var list_edges = [] + var incoming_edge = Edge.new(_idx, _terrain) + var outgoing_edge + while true: + list_edges.append(incoming_edge); + outgoing_edge = incoming_edge.next_half() + incoming_edge = Edge.new(_terrain._halfedges[outgoing_edge._idx], _terrain); + if not (incoming_edge._idx != -1 and incoming_edge._idx != _idx): + break + return list_edges + + func points_around(): + var list_points = [] + var incoming = _terrain._points_to_halfedges.get(_idx) + var incoming_edge = Point.new(incoming, _terrain) + var outgoing_edge + while true: + list_points.append(Point.new(_terrain._triangles[incoming_edge._idx], _terrain)); + outgoing_edge = incoming_edge.next_half() + incoming_edge = Edge.new(_terrain._halfedges[outgoing_edge._idx], _terrain); + if not (incoming_edge._idx != -1 and incoming_edge._idx != incoming): + break + return list_points + +class Edge: + var _idx + var _terrain + + func _init(idx, terrain): + self._idx = idx + self._terrain = terrain + + func get_index(): + return _idx + + func has_key(key): + return _terrain._edges_data[_idx].has(key) + + func set_data(key,value): + _terrain._edges_data[_idx][key] = value + + func get_data(key): + var data = _terrain._edges_data[_idx] + if data.has(key): + return data[key] + + func next_half(): + return Edge.new(_idx - 2 if _idx % 3 == 2 else _idx + 1, _terrain) + + func prev_half(): + return Edge.new(_idx + 2 if _idx % 3 == 0 else _idx -1, _terrain) + + func triangle(): + return Triangle.new(floor(_idx / 3), _terrain) + + func start(): + return Point.new(_terrain._triangles[_idx], _terrain) + + func end(): + return Point.new(_terrain._triangles[next_half()._idx], _terrain) + +const terrain_file = "user://terrain.save" + +var width: int +var height: int +var spacing: int +var _points = PoolVector3Array() +var _halfedges +var _triangles +var _points_to_halfedges = {} +var _data = {} +var _points_data = [] +var _edges_data = [] +var _triangles_data = [] +var _file = File.new() +var _debug = true + +""" +func general_type_of(obj): + var typ = typeof(obj) + var builtin_type_names = ["nil", "bool", "int", "real", "string", "vector2", "rect2", "vector3", "maxtrix32", "plane", "quat", "aabb", "matrix3", "transform", "color", "image", "nodepath", "rid", null, "inputevent", "dictionary", "array", "rawarray", "intarray", "realarray", "stringarray", "vector2array", "vector3array", "colorarray", "unknown"] + + if(typ == TYPE_OBJECT): + return obj.type_of() + else: + return builtin_type_names[typ] +""" + +func _print_debug(message): + if _debug: + print(message) + +func _init(width:int=1600, height:int=800, spacing:int=30, create=false): + if _file.file_exists(terrain_file) and not create: + _print_debug("loading...") + _load() + else: + _print_debug("Creating...") + var delaunay: Delaunator + self.width = width + self.height = height + self.spacing = spacing + _create_points() + delaunay = Delaunator.new(_points) + + _halfedges = PoolIntArray(delaunay.halfedges) + _triangles = PoolIntArray(delaunay.triangles) + + # Initialize _points_to_halfedges + for edge_idx in edges(): + var edge = get_edge(edge_idx) + var endpoint = _triangles[edge.next_half().get_index()] + if (! _points_to_halfedges.has(endpoint) or _halfedges[edge_idx] == -1): + _points_to_halfedges[endpoint] = edge_idx + + # Initialise _points_data + for point_idx in points(): + _points_data.append({}) + + # Initialise _edges_data + for edge_idx in edges(): + _edges_data.append({}) + + # Initialise _triangle_data + for triangle_idx in triangles(): + _triangles_data.append({}) + + _save() + +func _create_points(): + var rect = Rect2(Vector2(0, 0), Vector2(width, height)) + var poisson_disc_sampling: PoissonDiscSampling = PoissonDiscSampling.new() + var points2d = poisson_disc_sampling.generate_points(spacing, rect, 5) + _points.resize(points2d.size()) + for point_idx in points2d.size(): + _points[point_idx].x = points2d[point_idx].x + _points[point_idx].z = points2d[point_idx].y + +func get_triangles(): + return _triangles + +func get_halfedges(): + return _halfedges + +# return que les id ? +func get_points(): + return _points + +func get_point(idx): + return Point.new(idx, self) + +func get_edge(idx): + return Edge.new(idx, self) + +func get_triangle(idx): + return Triangle.new(idx, self) + +func triangles(): + return _triangles.size() / 3 + +func points(): + return _points.size() + +func edges(): + return _triangles.size() + +# Voronoi + +func centroid(points): + return Vector3((points[0].x + points[1].x + points[2].x) / 3.0, 0.0, (points[0].z + points[1].z + points[2].z) / 3.0) + + +func _save(): + _file.open(terrain_file, File.WRITE) + _file.store_var(width) + _file.store_var(height) + _file.store_var(spacing) + _file.store_var(_points) + _file.store_var(_halfedges) + _file.store_var(_triangles) + _file.store_var(_points_to_halfedges) + _file.store_var(_points_data) + _file.store_var(_edges_data) + _file.store_var(_triangles_data) + _file.close() + +func _load(): + _file.open(terrain_file, File.READ) + width = _file.get_var() + height = _file.get_var() + spacing = _file.get_var() + _points = _file.get_var() + _halfedges = _file.get_var() + _triangles = _file.get_var() + _points_to_halfedges = _file.get_var() + _points_data = _file.get_var() + _edges_data = _file.get_var() + _triangles_data = _file.get_var() + _file.close() + +func get_triangles_as_polygon(): + var list_polygon = [] + for triangle_idx in triangles(): + var polygon = [] + for point in get_triangle(triangle_idx).points(): + polygon.append(point.point2d()) + list_polygon.append(polygon) + return list_polygon + +func get_edges_as_line(): + var list_lines = [] + for edge_idx in edges(): + var line = [] + var edge = get_edge(edge_idx) + line.append(edge.start().point2d()) + line.append(edge.end().point2d()) + list_lines.append(line) + return list_lines + +func get_voronoi_edges_as_line(): + var list_lines = [] + for edge_idx in edges(): + var line = [] + var start_edge = get_edge(edge_idx) + var end_edge = get_edge(_halfedges[edge_idx]) + if (edge_idx < _halfedges[edge_idx]): + line.append(start_edge.triangle().center2d()) + line.append(end_edge.triangle().center2d()) + list_lines.append(line) + return list_lines + +func get_voronoi_cells_as_polygon(): + var list_polygon = [] + for point_idx in points(): + var point = get_point(point_idx) + var polygon = [] + for edge in point.edges_around(): + polygon.append(edge.triangle().center2d()) + list_polygon.append(polygon) + return(list_polygon) + diff --git a/world/game.gd b/world/game.gd new file mode 100644 index 0000000..02460cd --- /dev/null +++ b/world/game.gd @@ -0,0 +1,70 @@ +extends Spatial + +signal world_loaded + +export(int) var width = 2000 +export(int) var height = 2000 +export(int) var spacing = 20 +export(int, 1, 9) var octaves = 5 +export(int, 1, 30) var wavelength = 8 +export(int) var border_width = 200 +export(int) var terraces = 24 +export(int) var terrace_height = 5 +export(int) var mountain_height = 6 +export(int) var river_proba = 200 + +var rng = RandomNumberGenerator.new() +var noise = OpenSimplexNoise.new() + +var terrain + +func _ready(): + rng.randomize() + noise.seed = rng.randi() + noise.octaves = octaves + terrain = Terrain.new(width,height,spacing,true) + init_points_data() + print(terrain.get_point(3)) + emit_signal("world_loaded", terrain) + +func init_points_data(): + for index in terrain.get_points().size(): + terrain.get_point(index).set_elevation(find_elevation(terrain.get_point(index).point2d())) +# points_data.append({ +# "elevation": 0, +# "used": false, +# "water": false, +# "ocean": false, +# "coast": false, +# "mountain": false, +# "river": false +# }) + +func find_elevation(point): + var border = border_width + rng.randf_range(-20.0, 20.0) + var elevation = noise.get_noise_2d(point.x / wavelength, point.y / wavelength) + + if point.x < border: + elevation -= ((border - point.x) / border) / 2.0 + if point.y < border: + elevation -= (border - point.y) / border + if point.x > width - border: + elevation -= (border - (width - point.x)) / border + if point.y > height - border: + elevation -= (border - (height - point.y)) / border + + elevation = max(elevation, -1) + + if elevation > 0.1: + elevation = max(pow((elevation) * 1.2, 1.5), 0.1) + + elevation = min(elevation, 1) + + elevation = elevation * terraces + return elevation +# +# if points_data[point_id].elevation <= 0: +# points_data[point_id].water = true +# +# if points_data[point_id].elevation >= mountain_height: +# points_data[point_id].mountain = true diff --git a/world/game.tscn b/world/game.tscn new file mode 100644 index 0000000..04f54ff --- /dev/null +++ b/world/game.tscn @@ -0,0 +1,13 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://ui/ui.tscn" type="PackedScene" id=1] +[ext_resource path="res://world/game.gd" type="Script" id=2] + +[node name="Game" type="Spatial"] +script = ExtResource( 2 ) + +[node name="UI" parent="." instance=ExtResource( 1 )] + +[connection signal="world_loaded" from="." to="UI/Map" method="_on_Game_world_loaded"] + +[editable path="UI"]