Files
Cinderwild/scripts/creatures/spawner.gd
Paul Bailey 67bf85523d M1d: creatures, hunting, weapons, food buffs and the hide cloak
- Rabbits, deer and boars with procedural meshes and animated legs, a
  state machine (idle/wander/flee/charge/dead) and heightmap steering that
  avoids water, cliffs, the world edge, trees and buildings
- Spawner keeps a habitat-based budget around the player (6/4/3)
- Melee needs a weapon or tool (club, spear, axe, pickaxe); box sweep hits
- Wooden spear (thrust or throw) and crude bow with draw-scaled arrows;
  projectiles stick in the world as saved pickups
- Carcasses: E to butcher for meat, hide and embedded arrows
- Boars charge and gore when hit; hurt flash and a brief healing pause
- Cooked meat gives Well fed (faster stamina and healing); buffs saved
- Hide cloak: wear for warmth, saved, goes in the grave on death
- Feathers as meadow pickups (appended, existing save ids unchanged)
- New test_creatures_combat suite; run_all.sh gets a per-suite time limit
2026-11-04 10:55:44 +00:00

108 lines
3.3 KiB
GDScript

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