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(around: Vector3 = Vector3.INF) -> void: for kind in BUDGET: for i in range(BUDGET[kind]): _spawn_near(kind, 22.0, RING_MAX, around) ## A player joined a server: some wildlife around them straight away, up to ## the island's budget. func populate_for(pos: Vector3) -> void: if not enabled: return var n: int = world.players().size() for kind in BUDGET: var near := alive(kind).filter(func(c): return c.global_position.distance_to(pos) < RING_MAX).size() for i in range(mini(BUDGET[kind] - near, budget(kind, n) - alive(kind).size())): _spawn_near(kind, 22.0, RING_MAX, pos) 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 # Around every player (on a server there can be several, wherever they # are): an animal goes once it's far from all of them. var everyone: Array = world.players() if everyone.is_empty(): return for c in get_children(): var near := false for p in everyone: near = near or c.global_position.distance_to(p.global_position) <= DESPAWN if not near: 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, everyone.size()): var who: Node3D = everyone[0] if everyone.size() == 1 else everyone[_rng.randi() % everyone.size()] if _spawn_near(kind, RING_MIN, RING_MAX, who.global_position) != null: _cooldown[kind] = RESPAWN_DELAY * 0.25 ## How many of a kind the island holds: the budget per player, up to twice ## it however many are on (MULTIPLAYER_PLAN.md ยง11). static func budget(kind: String, players: int) -> int: return BUDGET[kind] * clampi(players, 1, 2) 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, around: Vector3 = Vector3.INF) -> Node3D: var pp: Vector3 = around if around != Vector3.INF else 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