188 lines
7.9 KiB
GDScript
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
|