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: 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