Wolves with a den, fishing, and a mixed diet
Wolves - Den: a boulder cave at the foot of the rim, sited from the seed (away from the sea and spawn, mouth toward the valley, clearing in front) - Grace period: no wolves days 1-2; they move in on day 3 and howl; hunting from night 4; pack of 2, 3 from day 6, 4 from day 9; replaced by day - Wary pack hunters: circle and dart in one at a time; a lit campfire is a safe zone, a torch makes them hesitate, walls block bites (line of sight); hit one and the pack turns; hurt wolves flee; a lone wolf is timid; by day they defend the den; home at dawn - Glowing eyes, synthesised howls, can't sleep with wolves prowling - Wolf pelt and Wolf Cloak (+25 warmth) - Animals take a committed detour around obstacles Fishing - Fishing rod (workbench); cast a float onto the lake, 1.5 s to land a bite - Perch and rarer trout; faster bites at dawn/dusk and in rain; grill them - Visible fish near the shore that jump with splash rings Mixed diet - Foods belong to meat, fish or greens; each level runs down over ~1.5 days - One group low: no healing; two low: Malnourished (health drains); all three good: Balanced diet (heal x1.5); HUD diet bars; saved Tests: test_wolves and test_fishing_diet; run_all.sh covers 9 suites
This commit is contained in:
174
scripts/creatures/den.gd
Normal file
174
scripts/creatures/den.gd
Normal file
@@ -0,0 +1,174 @@
|
||||
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)
|
||||
var ex: Array[RID] = [terrain.get_node("TerrainBody").get_rid(), world.player.get_rid()]
|
||||
for step in range(52):
|
||||
var ang := deg_to_rad(start + step * 7.0)
|
||||
var dir := Vector2(cos(ang), sin(ang))
|
||||
# Walk inward from the rim until the den sits inside the walkable
|
||||
# area, a little way short of the wall.
|
||||
var r: float = terrain.play_radius + 10.0
|
||||
while r > terrain.play_radius - 40.0:
|
||||
var x := dir.x * r
|
||||
var z := dir.y * r
|
||||
if terrain.inside_boundary(x, z, 11.0):
|
||||
break
|
||||
r -= 1.0
|
||||
var c := Vector3(dir.x * r, 0, dir.y * r)
|
||||
if terrain.sea_weight(c.x, c.z) > 0.02 or Vector2(c.x, c.z).length() < 50.0:
|
||||
continue
|
||||
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(c.x + d.x * 4.0, c.z + d.y * 4.0))
|
||||
if hs.max() - hs.min() > 2.2 or hs.min() < terrain.water_level + 1.0:
|
||||
continue
|
||||
c.y = hs[4]
|
||||
# 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 - c.x, n.global_position.z - c.z).length() < HALF_SIZE:
|
||||
crowd += 1
|
||||
if crowd <= 5:
|
||||
return c
|
||||
return Vector3.INF
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user