107 lines
3.5 KiB
GDScript
107 lines
3.5 KiB
GDScript
extends Node3D
|
|
## The shore's own wildlife (0.6.0, a land whose worldlet lists "crabs" in
|
|
## its "wildlife": the Cove). From dusk till dawn crabs come up out of the
|
|
## sea onto the beach, a few around each player who's down on the sand, at
|
|
## the waterline and out of the way. At dawn they go back in (crab.gd).
|
|
## Not saved: like the other wildlife they simply carry on around you.
|
|
##
|
|
## On a server this runs for everyone, and players see the crabs through
|
|
## creature_net.gd.
|
|
|
|
const CrabScript = preload("res://scripts/creatures/crab.gd")
|
|
|
|
const BUDGET := 5 ## per player on the beach, up to twice it
|
|
const CHECK_EVERY := 2.0
|
|
const RING_MIN := 14.0
|
|
const RING_MAX := 45.0
|
|
const NOT_WITHIN := 10.0 ## never comes up right beside anyone
|
|
const DESPAWN := 80.0
|
|
const BEACH_NEAR := 40.0 ## a player this close to the sand counts as on the beach
|
|
|
|
var world: Node3D
|
|
var terrain: Node3D
|
|
var enabled := true
|
|
var _rng := RandomNumberGenerator.new()
|
|
var _acc := 0.0
|
|
var _serial := 0
|
|
|
|
func setup(w: Node3D) -> void:
|
|
world = w
|
|
terrain = w.terrain
|
|
_rng.seed = terrain.noise_seed + 1201
|
|
|
|
func _physics_process(delta: float) -> void:
|
|
step(delta)
|
|
|
|
## Dusk till dawn.
|
|
func night() -> bool:
|
|
var h: float = world.day_night.hours()
|
|
return h >= 17.5 or h < 6.5
|
|
|
|
## Living crabs.
|
|
func alive() -> Array:
|
|
return get_children().filter(func(c): return is_instance_valid(c) and not c.is_queued_for_deletion() and not c.is_dead())
|
|
|
|
func step(dt: float) -> void:
|
|
if not enabled or world.creatures == null or not world.creatures.enabled:
|
|
return
|
|
_acc += dt
|
|
if _acc < CHECK_EVERY:
|
|
return
|
|
_acc = 0.0
|
|
var everyone: Array = world.players()
|
|
for c in get_children():
|
|
if not everyone.any(func(p): return p.global_position.distance_to(c.global_position) <= DESPAWN):
|
|
c.queue_free()
|
|
if not night() or everyone.is_empty():
|
|
return
|
|
var on_beach := everyone.filter(func(p): return _near_sand(p.global_position))
|
|
if on_beach.is_empty():
|
|
return
|
|
if alive().size() < BUDGET * clampi(on_beach.size(), 1, 2):
|
|
var who: Node3D = on_beach[_rng.randi() % on_beach.size()]
|
|
spawn_near(who.global_position) # (one at a time: they trickle up out of the sea)
|
|
|
|
func _near_sand(p: Vector3) -> bool:
|
|
for a in [0.0, 1.0, 2.0, 3.0, 4.0, 5.0]:
|
|
var q := p + Vector3(cos(a), 0.0, sin(a)) * BEACH_NEAR * 0.5
|
|
if terrain.on_sand(p.x, p.z) or terrain.on_sand(q.x, q.z):
|
|
return true
|
|
return false
|
|
|
|
## A spot on the sand at the waterline, RING_MIN..RING_MAX from `around`
|
|
## and not right beside anyone, or Vector3.INF.
|
|
func find_spot(around: Vector3) -> Vector3:
|
|
for _try in range(40):
|
|
var a := _rng.randf() * TAU
|
|
var r := _rng.randf_range(RING_MIN, RING_MAX)
|
|
var x := around.x + cos(a) * r
|
|
var z := around.z + sin(a) * r
|
|
if not terrain.inside_boundary(x, z, 6.0) or terrain.beach_weight(x, z) < 0.75:
|
|
continue
|
|
var h: float = terrain.sample_height(x, z)
|
|
if h < terrain.water_level + 0.25 or h > terrain.water_level + 0.9:
|
|
continue
|
|
var p := Vector3(x, h, z)
|
|
if world.players().any(func(pl): return pl.global_position.distance_to(p) < NOT_WITHIN):
|
|
continue
|
|
return p
|
|
return Vector3.INF
|
|
|
|
func spawn_near(around: Vector3) -> Node3D:
|
|
var p := find_spot(around)
|
|
if p == Vector3.INF:
|
|
return null
|
|
return spawn(p)
|
|
|
|
func spawn(pos: Vector3) -> Node3D:
|
|
var c: AnimatableBody3D = CrabScript.new()
|
|
_serial += 1
|
|
c.setup("crab", world, pos, _rng.randi() + _serial)
|
|
add_child(c)
|
|
c.global_position = Vector3(pos.x, terrain.sample_height(pos.x, pos.z), pos.z)
|
|
c.state = "wander"
|
|
c.yaw = _rng.randf() * TAU
|
|
c.target_yaw = c.yaw
|
|
return c
|