Files
Emberwild/scripts/creatures/den.gd

188 lines
7.9 KiB
GDScript

extends Node3D
## The wolf den: a cave mouth of piled boulders at the foot of the mountain
## rim, opening toward the valley. Built from the same procedural rocks as the
## scenery, so it's part of the island and regenerates from the seed.
##
## The site is chosen deterministically: a flat, clear spot close to the rim,
## away from the sea and the spawn. The rocks' colliders are also on the
## building layer, so standing inside the cave counts as being under a roof
## (it's dry in there, if you dare).
const ProcMesh = preload("res://scripts/world/proc_mesh.gd")
const BONE := Color(0.88, 0.85, 0.76)
## [local position, radii] of each boulder. The interior is about 3.6 m
## wide, 4 m deep and 2 m high; +Z is the mouth.
const ROCKS := [
[Vector3(0, 1.2, -3.4), Vector3(3.2, 2.4, 1.6)], # back wall
[Vector3(-2.6, 1.1, -1.4), Vector3(1.4, 2.2, 2.4)], # left wall
[Vector3(-2.4, 0.9, 0.9), Vector3(1.3, 1.8, 1.6)],
[Vector3(2.6, 1.2, -1.5), Vector3(1.5, 2.3, 2.4)], # right wall
[Vector3(2.3, 0.8, 0.9), Vector3(1.2, 1.6, 1.5)],
[Vector3(0, 3.0, -1.2), Vector3(3.4, 0.9, 3.0)], # roof slab
[Vector3(0, 2.0, -5.6), Vector3(4.5, 3.4, 3.0)], # mound behind
[Vector3(-3.6, 1.6, -4.4), Vector3(2.5, 2.6, 2.2)],
[Vector3(3.7, 1.8, -4.3), Vector3(2.6, 2.8, 2.3)],
[Vector3(-2.5, 0.5, 2.6), Vector3(1.0, 0.8, 0.9)], # boulders at the mouth
[Vector3(2.7, 0.45, 2.4), Vector3(0.9, 0.7, 0.8)],
]
## Where the wolves lie (local), facing out of the mouth.
const SLOTS := [Vector3(-0.9, 0, -0.9), Vector3(1.0, 0, -1.3), Vector3(0.0, 0, -2.0), Vector3(-0.2, 0, 0.4)]
const HALF_SIZE := 6.5 ## clear area needed around the site
var terrain: Node3D
var yaw := 0.0 ## node rotation; local +Z points into the valley
var found := false
## Finds the site and builds the den. Returns false (and builds nothing) if
## no spot qualifies on this seed.
func place(world: Node3D, rock_mat: Material) -> bool:
terrain = world.terrain
var site := _find_site(world)
if site == Vector3.INF:
push_warning("[den] no site found for the wolf den on this seed")
return false
found = true
position = site
yaw = atan2(-site.x, -site.z) # mouth toward the centre of the valley
rotation.y = yaw
_clear_area(world.props)
_build(rock_mat)
return true
func _find_site(world: Node3D) -> Vector3:
var rng := RandomNumberGenerator.new()
rng.seed = terrain.noise_seed + 777
var start := rng.randf() * 360.0
var space := world.get_world_3d().direct_space_state
var box := BoxShape3D.new()
box.size = Vector3(HALF_SIZE * 2.0, 6.0, HALF_SIZE * 2.0)
# Pass 1 tries only the foot of the rim wall in each direction (the
# original search, so islands that already had a den keep it in the same
# place). Where the rim is steep all the way round (seed 777, seed 4),
# pass 2 steps further into the valley, a few metres at a time.
for pass_insets in [[0.0], [3.0, 6.0, 9.0, 12.0, 16.0, 20.0, 25.0, 30.0]]:
for step in range(52):
var ang := deg_to_rad(start + step * 7.0)
var dir := Vector2(cos(ang), sin(ang))
var r_foot := _rim_foot(dir)
for inset in pass_insets:
var c := _check_site(world, dir * (r_foot - inset))
if c != Vector3.INF:
return c
return Vector3.INF
## Walks inward from the rim until the den sits inside the walkable area, a
## little way short of the wall. Returns that distance from the centre.
func _rim_foot(dir: Vector2) -> float:
var r: float = terrain.play_radius + 10.0
while r > terrain.play_radius - 40.0:
if terrain.inside_boundary(dir.x * r, dir.y * r, 11.0):
break
r -= 1.0
return r
## The site at `p` (x, z) with its height, or INF if it's no good: near the
## sea or the spawn, too sloped, too low, or in thick woods.
func _check_site(world: Node3D, p: Vector2) -> Vector3:
if terrain.sea_weight(p.x, p.y) > 0.02 or p.length() < 50.0:
return Vector3.INF
var hs: Array[float] = []
for d in [Vector2(-1, -1), Vector2(1, -1), Vector2(-1, 1), Vector2(1, 1), Vector2.ZERO]:
hs.append(terrain.sample_height(p.x + d.x * 4.0, p.y + d.y * 4.0))
if hs.max() - hs.min() > 2.2 or hs.min() < terrain.water_level + 1.0 or terrain.near_stream(p.x, p.y, 7.0):
return Vector3.INF
# Trees and boulders in the way are cleared (_clear_area); just avoid
# the thickest woods. Counted by position, because their colliders
# were only just created and the physics space hasn't seen them yet.
var crowd := 0
for uid in world.props.resource_nodes:
var n: Node3D = world.props.resource_nodes[uid]
if Vector2(n.global_position.x - p.x, n.global_position.z - p.y).length() < HALF_SIZE:
crowd += 1
if crowd > 5:
return Vector3.INF
return Vector3(p.x, hs[4], p.y)
## Removes the trees, boulders, bushes and pickups the den is built over.
## Deterministic (the site comes from the seed), and the other scenery keeps
## its save ids because those come from creation order.
func _clear_area(props: Node3D) -> void:
var r := HALF_SIZE
# Also a clearing in front of the mouth, so you can see the cave from the valley.
var b := Basis(Vector3.UP, yaw)
var front: Vector3 = position + b * Vector3(0, 0, 9.0)
for dict in [props.resource_nodes, props.pickups, props.forage]:
for uid in dict.keys():
var n: Node3D = dict[uid]
if not is_instance_valid(n):
continue
var p2 := Vector2(n.global_position.x, n.global_position.z)
if p2.distance_to(Vector2(position.x, position.z)) < r or p2.distance_to(Vector2(front.x, front.z)) < 6.0:
if n.has_method("_set_instance"):
n._set_instance(Transform3D(Basis(), n.global_position + Vector3(0, -1000, 0)))
dict.erase(uid)
n.queue_free()
func _build(rock_mat: Material) -> void:
var body := StaticBody3D.new()
body.name = "Rocks"
body.collision_layer = 1 | 4 # solid, and a roof for shelter checks
add_child(body)
var b := Basis(Vector3.UP, yaw)
for i in range(ROCKS.size()):
var lp: Vector3 = ROCKS[i][0]
var radii: Vector3 = ROCKS[i][1]
# Sit each boulder on the ground under it (the site isn't perfectly flat).
var w: Vector3 = position + b * Vector3(lp.x, 0, lp.z)
var y: float = terrain.sample_height(w.x, w.z) - position.y + lp.y - 0.25
var mesh := ProcMesh.rock(terrain.noise_seed + 3100 + i, false)
var mi := MeshInstance3D.new()
mi.mesh = mesh
mi.material_override = rock_mat
var turn := Basis(Vector3.UP, float(i) * 1.7)
mi.transform = Transform3D(turn.scaled(radii), Vector3(lp.x, y, lp.z))
add_child(mi)
var hull := mesh.create_convex_shape(true, true) as ConvexPolygonShape3D
var pts := PackedVector3Array()
for p: Vector3 in hull.points:
pts.append(turn * (p * radii))
hull.points = pts
var cs := CollisionShape3D.new()
cs.shape = hull
cs.position = Vector3(lp.x, y, lp.z)
body.add_child(cs)
# Old bones on the cave floor.
var st := ProcMesh._begin()
var rng := RandomNumberGenerator.new()
rng.seed = terrain.noise_seed + 778
var n := FastNoiseLite.new()
n.seed = 778
for k in range(7):
var c := Vector3(rng.randf_range(-1.4, 1.4), 0.0, rng.randf_range(-2.4, 1.2))
var a := rng.randf() * TAU
var dir := Vector3(cos(a), 0, sin(a)) * rng.randf_range(0.15, 0.3)
ProcMesh._cylinder(st, c - dir + Vector3(0, 0.04, 0), c + dir + Vector3(0, 0.04, 0), 0.025, 0.02, 5, BONE, rng, -1.0, BONE)
ProcMesh._blob(st, c + dir + Vector3(0, 0.05, 0), Vector3.ONE * 0.045, n, 0.2, BONE, rng, 0)
var bones := MeshInstance3D.new()
bones.mesh = ProcMesh._commit(st)
bones.material_override = ItemDB.item_material()
add_child(bones) # (the den's origin is the cave floor)
## World position of resting slot `i`, on the ground.
func slot_pos(i: int) -> Vector3:
var lp: Vector3 = SLOTS[i % SLOTS.size()]
var w: Vector3 = position + Basis(Vector3.UP, yaw) * lp
w.y = terrain.sample_height(w.x, w.z)
return w
## Yaw a wolf uses to face out of the mouth (forward is -Z).
func facing_out_yaw() -> float:
var out := Basis(Vector3.UP, yaw) * Vector3(0, 0, 1)
return atan2(-out.x, -out.z)
func mouth_pos() -> Vector3:
var w: Vector3 = position + Basis(Vector3.UP, yaw) * Vector3(0, 0, 3.5)
w.y = terrain.sample_height(w.x, w.z)
return w