extends Node3D ## Keeps a few animals around the player, within a budget per kind. ## ## Animals spawn in a ring 30-70 m out, out of the way, and despawn once ## they're more than 100 m away. Each kind has its own habitat: rabbits in ## meadows, deer at forest edges, boars deep in the woods. Animals aren't ## saved: like Valheim, the wildlife simply carries on around you, and a ## loaded world repopulates within a few seconds. const CreatureScript = preload("res://scripts/creatures/creature.gd") const BUDGET := {"rabbit": 6, "deer": 4, "boar": 3} const RING_MIN := 30.0 const RING_MAX := 70.0 const DESPAWN := 100.0 const RESPAWN_DELAY := 25.0 ## per kind, after one dies or wanders off const CHECK_EVERY := 2.0 var world: Node3D var terrain: Node3D var props: Node3D var enabled := true var _rng := RandomNumberGenerator.new() var _check := 0.0 var _cooldown := {"rabbit": 0.0, "deer": 0.0, "boar": 0.0} var _serial := 0 func setup(w: Node3D) -> void: world = w terrain = w.terrain props = w.props _rng.seed = terrain.noise_seed + 901 func alive(kind: String = "") -> Array: var out := [] for c in get_children(): if c.is_in_group("creature") and (kind.is_empty() or c.kind == kind): out.append(c) return out ## Fills the budget straight away (world start), closer in than usual so ## there's something to see, but not on top of the player. func populate() -> void: for kind in BUDGET: for i in range(BUDGET[kind]): _spawn_near(kind, 22.0, RING_MAX) func _process(delta: float) -> void: if not enabled: return for k in _cooldown: _cooldown[k] = maxf(_cooldown[k] - delta, 0.0) _check -= delta if _check > 0.0: return _check = CHECK_EVERY var pp: Vector3 = world.player.global_position for c in get_children(): if c.global_position.distance_to(pp) > DESPAWN: if c.is_in_group("creature"): _cooldown[c.kind] = RESPAWN_DELAY * 0.3 c.queue_free() for kind in BUDGET: if _cooldown[kind] > 0.0: continue if alive(kind).size() < BUDGET[kind]: if _spawn_near(kind, RING_MIN, RING_MAX) != null: _cooldown[kind] = RESPAWN_DELAY * 0.25 func _on_died(c: Node) -> void: _cooldown[c.kind] = RESPAWN_DELAY ## Habitat preference, 0..1, at a spot (forest density from the scenery). func _habitat(kind: String, x: float, z: float) -> float: var f: float = props._forest(x, z) match kind: "rabbit": return 1.0 - smoothstep(0.2, 0.6, f) "deer": return 1.0 - absf(f - 0.45) * 1.6 _: return smoothstep(0.35, 0.75, f) func _spawn_near(kind: String, rmin: float, rmax: float) -> Node3D: var pp: Vector3 = world.player.global_position for _try in range(40): var a := _rng.randf() * TAU var r := _rng.randf_range(rmin, rmax) var x := pp.x + cos(a) * r var z := pp.z + sin(a) * r if not terrain.inside_boundary(x, z, 10.0): continue var h: float = terrain.sample_height(x, z) if h < terrain.water_level + 0.6 or h > terrain.snow_height - 4.0: continue if terrain.sample_normal(x, z).y < 0.85: continue if _rng.randf() > 0.15 + _habitat(kind, x, z) * 0.85: continue return spawn(kind, Vector3(x, h, z)) return null func spawn(kind: String, pos: Vector3) -> Node3D: var c: AnimatableBody3D = CreatureScript.new() _serial += 1 c.setup(kind, world, pos, _rng.randi() + _serial) add_child(c) c.global_position = Vector3(pos.x, terrain.sample_height(pos.x, pos.z), pos.z) c.died.connect(_on_died) return c