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