The Cove's shore life: crabs up the beach from dusk till dawn (keep to the sand, pinch if you get close, crab meat to cook), gulls after food on the sand and fish on a smoking rack; online too

This commit is contained in:
Paul Bailey
2026-11-07 10:23:49 +00:00
parent 5bcfa23710
commit 9709d2c424
30 changed files with 878 additions and 11 deletions

View File

@@ -87,6 +87,8 @@ const SOUNDS := {
"guard_rattle": {"set": [C + "guard_rattle_%d.ogg", 2], "db": -2.0, "pitch": [0.85, 1.1], "reach": 25.0},
"crow_caw": {"set": [C + "crow_caw_%d.ogg", 2], "db": -4.0, "pitch": [0.92, 1.1], "reach": 40.0},
"fox_yip": {"set": [C + "fox_yip_%d.ogg", 2], "db": -4.0, "pitch": [0.92, 1.12], "reach": 30.0},
"gull_cry": {"set": [C + "gull_cry_%d.ogg", 2], "db": -5.0, "pitch": [0.9, 1.12], "reach": 50.0},
"crab_click": {"set": [C + "crab_click_%d.ogg", 2], "db": -6.0, "pitch": [0.85, 1.15], "reach": 14.0},
}
const BUSES := ["Music", "SFX", "Ambience", "Voice"]
const DEFAULT_VOLUMES := {"Master": 0.85, "Music": 0.6, "SFX": 0.9, "Ambience": 0.7, "Voice": 1.0}

113
scripts/creatures/crab.gd Normal file
View File

@@ -0,0 +1,113 @@
extends "res://scripts/creatures/creature.gd"
## A crab (0.6.0, the Cove only): shore.gd brings them up out of the sea
## onto the beach at dusk. They scuttle about the sand sideways and keep to
## it. Walk within NIP of one and it comes at you, claws up, and pinches
## (not much, but it adds up with three of them about). Leave it NIP_GIVE_UP
## behind and it forgets you. Hit one hard enough and it runs for it. At
## dawn they all go back into the sea. A dead one gives crab meat.
const NIP := 2.2 ## this close and it comes for you
const NIP_GIVE_UP := 6.0 ## this far and it lets you go
const HOME_FOR := 40.0 ## at dawn: this long to reach the water before it simply goes
func interact_prompt(p: Node) -> String:
if is_dead():
return "Crab [E] Crack it open"
match state:
"charge": return "Crab (claws up!)"
"home": return "Crab (heading back to the sea)"
return "Crab (keep your distance, or it'll pinch)"
## Daytime: back to the sea.
func _daytime() -> bool:
var h: float = world.day_night.hours()
return h >= 6.5 and h < 17.5
## Which way the sea is: straight out from the middle of the bay.
func _seaward() -> Vector3:
var a := deg_to_rad(float(terrain.beach_angle_deg))
return Vector3(cos(a), 0.0, sin(a))
func _think(dt: float) -> void:
if state != "home" and state != "flee" and _daytime():
state = "home"
_state_t = HOME_FOR
var pp: Vector3 = player.global_position
var d := global_position.distance_to(pp)
match state:
"home":
var to := _seaward()
steer(atan2(-to.x, -to.z))
_state_t -= dt
if _state_t <= 0.0 or global_position.y < terrain.water_level - 0.25:
queue_free() # (into the waves)
return
"idle", "wander":
if d < NIP:
state = "charge"
_aggro = def.aggro
_attack_cd = 0.5 # (a moment to see it coming)
return
"charge":
if d < NIP_GIVE_UP:
_aggro = def.aggro # (it keeps on at you while you're close)
else:
_aggro = 0.0
super._think(dt)
func _desired_speed() -> float:
if state == "home":
return def.walk * 1.6
return super._desired_speed()
## Keeps to the sand (and out of the water) until it's going home.
func _blocked(dir: Vector3) -> bool:
var probe := global_position + dir * (0.8 + speed * 0.15)
if not terrain.inside_boundary(probe.x, probe.z, 4.0):
return true
if state == "home":
return false # (straight down the beach and into the sea)
if not terrain.on_sand(probe.x, probe.z):
return true
return super._blocked(dir)
## Hit: it fights back while it can, then runs.
func _react(from: Vector3) -> void:
if hp > def.hp * 0.35:
state = "charge"
_aggro = def.aggro
_attack_cd = 0.3
else:
_start_flee(from)
_state_t = 5.0
## Sideways, legs a-flicker; the claws come up when it's after you.
func _animate(dt: float) -> void:
_visual.rotation.y = PI * 0.5 # (it walks sideways)
if is_dead():
return
var k := clampf(speed / maxf(def.run, 0.01), 0.0, 1.0)
_phase += dt * (6.0 + speed * 14.0)
for i in range(_legs.size()):
var l: Node3D = _legs[i]
var s := -1.0 if l.position.x < 0.0 else 1.0
l.rotation.z = s * (1.0 + sin(_phase + i * 1.9) * (0.08 + 0.3 * k))
l.rotation.x = cos(_phase + i * 1.9) * 0.25 * k
var up := 0.12 if state == "charge" or (puppet and speed > 1.5) else 0.0
_visual.rotation.x = lerpf(_visual.rotation.x, -up, minf(dt * 6.0, 1.0))
_visual.position.y = absf(sin(_phase * 0.5)) * 0.01 * k
## A crab doesn't topple onto its side: it ends up on its back.
func _die(_from: Vector3) -> void:
state = "dead"
speed = 0.0
hp = 0.0
_flash = 0.0
_set_material(_mat)
remove_from_group("creature")
add_to_group("carcass")
collision_layer = 2
_visual.rotation = Vector3(0.0, PI * 0.5, PI)
_visual.position.y = 0.1
if not puppet:
died.emit(self)

View File

@@ -0,0 +1 @@
uid://kqumi2x6lfai

View File

@@ -54,6 +54,16 @@ const KINDS := {
"fox": {"name": "Fox", "hp": 25.0, "walk": 1.5, "run": 7.6, "turn": 7.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"drops": {"raw_meat": [1, 1], "hide": [1, 1]}},
# 0.6.0, the Cove only: crabs come up the beach at dusk and pinch anyone
# who walks too close (crab.gd, shore.gd); gulls are the Cove's crows,
# after food left on the sand and fish on a smoking rack (gull.gd).
"crab": {"name": "Crab", "hp": 14.0, "walk": 0.9, "run": 2.6, "turn": 7.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 20.0,
"damage": 5.0, "attack_range": 1.7, "attack_cd": 1.3, "aggro": 5.0,
"drops": {"crab_meat": [1, 2]}},
"gull": {"name": "Gull", "hp": 8.0, "walk": 1.0, "run": 11.0, "turn": 8.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
"drops": {"feather": [1, 3]}},
# The barrow's skeletal guards (guard.gd, barrow.gd). Slow (you can walk
# away from one) but they hit hard; bone, so blunt weapons do +50%.
"guard": {"name": "Barrow Guard", "hp": 80.0, "walk": 1.4, "run": 3.4, "turn": 4.0,
@@ -172,7 +182,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
cs.position = Vector3(0, parts.height * 0.5, 0)
if not k in ["rabbit", "guard", "drowned"]: # (rabbits are round, guards stand upright)
# Long bodies: lay the capsule along the spine.
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3, "old_stag": 3.3, "spirit_wolf": 1.25}.get(k, 1.4)
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3, "gull": 0.4, "crab": 0.45, "old_stag": 3.3, "spirit_wolf": 1.25}.get(k, 1.4)
cs.rotation.x = PI * 0.5
cs.position = Vector3(0, parts.height * 0.55, 0)
add_child(cs)
@@ -525,6 +535,8 @@ const VOICES := {
"drowned": {"alert": "guard_rattle", "angry": "guard_rattle", "attack": "guard_rattle"},
"crow": {"alert": "crow_caw", "hurt": "crow_caw", "idle": "crow_caw"},
"fox": {"alert": "fox_yip", "hurt": "fox_yip"},
"gull": {"alert": "gull_cry", "hurt": "gull_cry", "idle": "gull_cry"},
"crab": {"alert": "crab_click", "attack": "crab_click"},
}
## States a creature is in when it's after someone (music.gd's list too).
const ANGRY := ["charge", "stalk", "lunge", "bash", "stomp", "telegraph", "warn", "raid", "summon"]

View File

@@ -22,6 +22,8 @@ static func build(kind: String) -> Dictionary:
"bear": d = _bear()
"fox": d = _scaled(_wolf_parts(Color(0.78, 0.4, 0.15), Color(0.92, 0.88, 0.8), 281), 0.6)
"crow": d = _crow()
"gull": d = _gull()
"crab": d = _crab()
"guard": d = _guard()
"drowned": d = _drowned()
"old_stag": d = _scaled(_stag(), 2.1)
@@ -208,6 +210,61 @@ static func _crow() -> Dictionary:
"hips": [Vector3(-0.06, 0.19, -0.01), Vector3(0.06, 0.19, -0.01)],
"radius": 0.12, "height": 0.3}
## Gull: a crow's build, bigger, white with grey wings and a yellow beak.
static func _gull() -> Dictionary:
var white := Color(0.93, 0.93, 0.9)
var grey := Color(0.6, 0.63, 0.67)
var tip := Color(0.12, 0.12, 0.13)
var beak := Color(0.95, 0.75, 0.2)
var st := P._begin()
var r := _rng(295)
var n := _noise(295, 2.0)
P._blob(st, Vector3(0, 0.2, 0), Vector3(0.09, 0.085, 0.18), n, 0.08, white, r, 1)
P._blob(st, Vector3(0, 0.25, 0.04), Vector3(0.075, 0.05, 0.14), n, 0.06, grey, r, 1)
P._blob(st, Vector3(0, 0.28, -0.16), Vector3(0.06, 0.06, 0.065), n, 0.08, white, r, 1)
P._cylinder(st, Vector3(0, 0.275, -0.21), Vector3(0, 0.255, -0.3), 0.017, 0.004, 4, beak, r)
P._cylinder(st, Vector3(0, 0.22, 0.15), Vector3(0, 0.2, 0.3), 0.035, 0.05, 4, tip, r)
for s in [-1.0, 1.0]:
P._cylinder(st, Vector3(s * 0.03, 0.13, 0.0), Vector3(s * 0.035, 0.0, 0.02), 0.008, 0.007, 3, beak, r)
P._blob(st, Vector3(s * 0.04, 0.295, -0.19), Vector3.ONE * 0.009, n, 0.0, Color(0.1, 0.1, 0.1), r, 0)
var wst := P._begin()
P._blob(wst, Vector3(0, -0.2, 0.03), Vector3(0.014, 0.21, 0.09), n, 0.04, grey, r, 0)
P._blob(wst, Vector3(0, -0.38, 0.05), Vector3(0.012, 0.05, 0.06), n, 0.0, tip, r, 0)
return {"body": P._commit(st), "leg": P._commit(wst),
"hips": [Vector3(-0.08, 0.25, -0.01), Vector3(0.08, 0.25, -0.01)],
"radius": 0.15, "height": 0.38}
## Crab: a wide, flat red-brown shell facing -Z, two big claws in front and
## eyes on stalks; six thin legs splayed out to the sides (crab.gd turns the
## whole crab side-on to walk, and flickers the legs).
static func _crab() -> Dictionary:
var shell := Color(0.66, 0.22, 0.12)
var dark := Color(0.42, 0.13, 0.08)
var pale := Color(0.9, 0.72, 0.55)
var st := P._begin()
var r := _rng(297)
var n := _noise(297, 2.4)
P._blob(st, Vector3(0, 0.11, 0), Vector3(0.2, 0.06, 0.14), n, 0.1, shell, r, 1)
P._blob(st, Vector3(0, 0.08, 0.01), Vector3(0.17, 0.035, 0.12), n, 0.05, pale, r, 1)
for s in [-1.0, 1.0]:
# Arm, then the claw: a fat pincer and a thin finger.
P._cylinder(st, Vector3(s * 0.12, 0.1, -0.09), Vector3(s * 0.17, 0.13, -0.2), 0.025, 0.02, 4, shell, r)
P._blob(st, Vector3(s * 0.17, 0.14, -0.26), Vector3(0.05, 0.04, 0.07), n, 0.1, shell, r, 1)
P._cylinder(st, Vector3(s * 0.17, 0.15, -0.31), Vector3(s * 0.15, 0.16, -0.37), 0.02, 0.006, 4, dark, r)
P._cylinder(st, Vector3(s * 0.19, 0.12, -0.31), Vector3(s * 0.18, 0.12, -0.36), 0.014, 0.004, 4, dark, r)
# Eyes on stalks.
P._cylinder(st, Vector3(s * 0.04, 0.15, -0.11), Vector3(s * 0.05, 0.21, -0.13), 0.008, 0.006, 3, shell, r)
P._blob(st, Vector3(s * 0.05, 0.22, -0.13), Vector3.ONE * 0.014, n, 0.0, Color(0.05, 0.05, 0.05), r, 0)
var lst := P._begin()
P._cylinder(lst, Vector3(0, 0, 0), Vector3(0, -0.11, 0.0), 0.014, 0.01, 3, shell, r)
P._cylinder(lst, Vector3(0, -0.11, 0.0), Vector3(0, -0.2, 0.0), 0.01, 0.003, 3, dark, r)
var hips: Array = []
for s in [-1.0, 1.0]:
for z in [-0.05, 0.03, 0.1]:
hips.append(Vector3(s * 0.17, 0.1, z))
return {"body": P._commit(st), "leg": P._commit(lst), "hips": hips,
"radius": 0.2, "height": 0.25}
## Barrow guard: an upright skeleton facing -Z, in a rusted helm, an old
## sword in its right hand and a round shield on its left arm. Two legs
## (creature.gd walks them like a pair of a quadruped's).

56
scripts/creatures/gull.gd Normal file
View File

@@ -0,0 +1,56 @@
extends "res://scripts/creatures/crow.gd"
## A gull (0.6.0, the Cove only): the Cove's crow, sent by thieves.gd by
## day. It comes for food left on the sand and for fish on a smoking rack
## that nobody's watching. Bolder and quicker than a crow: it lands, snatches
## one and is off with it, and another soon follows. Shoot it down while
## it's carrying and it drops what it took. Feathers.
const SNATCH_AFTER := 1.2 ## seconds on the ground before it grabs one
const GULL_SCARE := 6.0 ## (bolder than a crow)
var carry: Dictionary = {} ## {id, count, extra} once it has something
func display_name() -> String:
return "Gull"
func interact_prompt(_p: Node) -> String:
if is_dead():
return "Gull [E] Pluck"
if not carry.is_empty():
return "Gull (off with your %s!)" % ItemDB.item_name(String(carry.id)).to_lower()
return {"peck": "Gull (after your food!)", "fly": "Gull (circling your food)"}.get(state, "Gull")
func _think(dt: float) -> void:
match state:
"fly":
if not thieves.gull_still_there(food) or thieves.watched(thieves.spot(food), GULL_SCARE):
_leave()
return
_goal = thieves.spot(food)
var ground: float = terrain.sample_height(global_position.x, global_position.z)
if Vector2(_goal.x - global_position.x, _goal.z - global_position.z).length() < 0.35 and global_position.y - ground < 0.15:
state = "peck"
_state_t = SNATCH_AFTER
thieves.gull_landed(self, food)
"peck":
if not thieves.gull_still_there(food) or thieves.watched(global_position, GULL_SCARE):
_leave()
return
_state_t -= dt
if _state_t <= 0.0:
_peck_anim = 0.4
carry = thieves.snatch(self, food)
_leave()
"fly_off":
if thieves.out_of_sight(global_position):
thieves.gone(self)
_:
_leave()
## Shot down with something in its beak: it drops it where it falls.
func _die(from: Vector3) -> void:
super._die(from)
if not carry.is_empty() and not puppet:
var at := global_position + Vector3(0, 0.3, 0)
world.drop_item(String(carry.id), int(carry.count), Transform3D(Basis(), at), carry.get("extra", {}))
carry = {}

View File

@@ -0,0 +1 @@
uid://draw1wvqi3cif

106
scripts/creatures/shore.gd Normal file
View File

@@ -0,0 +1,106 @@
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

View File

@@ -0,0 +1 @@
uid://b6eeco4cs028q

View File

@@ -9,10 +9,16 @@ extends Node3D
## fox.gd). Food in a chest is never touched (that's what chests are for);
## the bear (bear_range.gd) is another matter.
##
## In a land with gulls (the Cove, 0.6.0: its worldlet's "wildlife"), food
## left on the sand draws a gull by day instead of a crow (gull.gd), and so
## does fish on a smoking rack that nobody's watching: a gull snatches one
## and flies off with it.
##
## On a server this runs for everyone (thieves are wildlife: creature_net.gd).
const CrowScript = preload("res://scripts/creatures/crow.gd")
const FoxScript = preload("res://scripts/creatures/fox.gd")
const GullScript = preload("res://scripts/creatures/gull.gd")
const CHECK_EVERY := 5.0
const UNWATCHED := 10.0
@@ -25,6 +31,7 @@ signal stolen(thief: Node, id: String, count: int)
var world: Node3D
var enabled := true
var gulls := false ## this land has gulls (the Cove)
var _acc := 0.0
var _unwatched: Dictionary = {} ## pickup instance id -> seconds left alone
var _rng := RandomNumberGenerator.new()
@@ -32,6 +39,7 @@ var _rng := RandomNumberGenerator.new()
func setup(w: Node3D) -> void:
world = w
_rng.seed = w.terrain.noise_seed + 997
gulls = w.has_wildlife("gulls")
func _physics_process(delta: float) -> void:
if enabled:
@@ -85,14 +93,28 @@ func step(dt: float) -> void:
_unwatched[k] = float(_unwatched.get(k, 0.0)) + since
if float(_unwatched[k]) >= NOTICE_AFTER and not _targeted(d) and alive().size() < MAX_THIEVES:
send(d)
if gulls and not night():
for r in racks():
if not gull_still_there(r):
continue
var k: int = r.get_instance_id()
seen[k] = true
if watched(r.global_position, UNWATCHED):
_unwatched.erase(k)
continue
_unwatched[k] = float(_unwatched.get(k, 0.0)) + since
if float(_unwatched[k]) >= NOTICE_AFTER and not _targeted(r) and alive().size() < MAX_THIEVES:
send(r)
for k in _unwatched.keys():
if not seen.has(k):
_unwatched.erase(k)
## Sends a thief after `food`: a fox at dusk and night, otherwise a crow.
## Sends a thief after `food`: a fox at dusk and night, otherwise a crow (a
## gull on the sand, or at a smoking rack, where there are gulls).
func send(food: Node3D) -> Node3D:
var fox := night()
var fp: Vector3 = food.global_position
var gull := not fox and gulls and (food.is_in_group("processor") or on_sand(food.global_position))
var fp: Vector3 = spot(food)
var from := Vector3.INF
for _i in range(12):
var a := _rng.randf() * TAU
@@ -104,8 +126,8 @@ func send(food: Node3D) -> Node3D:
break
if from == Vector3.INF:
return null
var t: AnimatableBody3D = (FoxScript if fox else CrowScript).new()
t.setup("fox" if fox else "crow", world, from, _rng.randi())
var t: AnimatableBody3D = (FoxScript if fox else (GullScript if gull else CrowScript)).new()
t.setup("fox" if fox else ("gull" if gull else "crow"), world, from, _rng.randi())
t.thieves = self
t.food = food
t.state = "sneak" if fox else "fly"
@@ -148,3 +170,63 @@ func grab(fox: Node, food: Node3D) -> Dictionary:
## A thief left with what it took (or empty-handed).
func gone(t: Node) -> void:
t.queue_free()
# --- gulls (the Cove) ----------------------------------------------------------------
func on_sand(pos: Vector3) -> bool:
return world.terrain.on_sand(pos.x, pos.z)
static func is_fish(id: String) -> bool:
return Dictionary(ItemDB.get_item(id).get("food", {})).get("diet", {}).has("fish")
## This land's smoking racks.
func racks() -> Array:
return get_tree().get_nodes_in_group("processor").filter(func(r): return is_instance_valid(r) and not r.is_queued_for_deletion() \
and String(r.item_id) == "smoking_rack" and world.is_ancestor_of(r))
## Fish on a rack: the first id there is, or "".
func _rack_fish(r: Node3D) -> String:
for st in r.inventory.slots:
if st != null and is_fish(String(st.id)):
return String(st.id)
return ""
## Whether a gull's target is still worth it: food on the ground, or a rack
## with fish on it.
func gull_still_there(t: Node3D) -> bool:
if t == null or not is_instance_valid(t) or t.is_queued_for_deletion():
return false
if t.is_in_group("processor"):
return not _rack_fish(t).is_empty()
return still_there(t)
## Where a thief lands for `t`: on food, or on the ground beside a rack.
func spot(t: Node3D) -> Vector3:
if t.is_in_group("processor"):
var p: Vector3 = t.global_position + t.global_basis.z * 0.8
return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z)
return t.global_position
func gull_landed(_gull: Node, t: Node3D) -> void:
if t.is_in_group("processor"):
_tell(t.global_position, "A gull's at the fish on your smoking rack!")
else:
_tell(t.global_position, "A gull's landed by your %s!" % ItemDB.item_name(t.item_id).to_lower())
## A gull takes one and flies off with it. Returns what it's carrying.
func snatch(gull: Node, t: Node3D) -> Dictionary:
if not gull_still_there(t):
return {}
var c := {}
if t.is_in_group("processor"):
var id := _rack_fish(t)
t.inventory.remove(id, 1)
c = {"id": id, "count": 1, "extra": {}}
else:
c = {"id": String(t.item_id), "count": 1, "extra": t.extra.duplicate()}
t.amount = int(t.amount) - 1
if int(t.amount) <= 0:
world.take_dropped(t)
_tell(t.global_position, "A gull has snatched your %s!" % ItemDB.item_name(String(c.id)).to_lower())
stolen.emit(gull, String(c.id), 1)
return c

View File

@@ -84,6 +84,8 @@ static func build(key: String) -> ArrayMesh:
"mackerel": m = _fish(MACKEREL, 0.2, false)
"cod": m = _fish(COD, 0.3, false)
"grilled_mackerel": m = _fish(MACKEREL, 0.2, true)
"crab_meat": m = _crab_meat()
"cooked_crab": m = _cooked_crab()
"grilled_cod": m = _fish(COD, 0.3, true)
"fiber_plant": m = _fiber_plant()
"iron_nugget": m = _pebble(61, RUST, Vector3(0.07, 0.05, 0.06), 0.6)
@@ -1179,6 +1181,34 @@ static func _barrow_key() -> ArrayMesh:
# --- P4 food keeping ---------------------------------------------------------------------
## A crust of white salt crystals.
## A few chunks of white crab meat in a cracked bit of shell.
static func _crab_meat() -> ArrayMesh:
var st := P._begin()
var r := _rng(341)
var n := FastNoiseLite.new()
n.seed = 341
n.frequency = 2.6
P._blob(st, Vector3(0, 0.03, 0), Vector3(0.11, 0.03, 0.08), n, 0.2, Color(0.62, 0.22, 0.12), r, 1)
for i in range(4):
var c := Vector3(r.randf_range(-0.05, 0.05), 0.06, r.randf_range(-0.035, 0.035))
P._blob(st, c, Vector3(0.035, 0.022, 0.025), n, 0.25, Color(0.95, 0.9, 0.84).lerp(Color(0.95, 0.7, 0.62), r.randf() * 0.4), r, 0)
return P._commit(st)
## A whole crab, boiled pink-red, legs tucked.
static func _cooked_crab() -> ArrayMesh:
var st := P._begin()
var r := _rng(343)
var n := FastNoiseLite.new()
n.seed = 343
n.frequency = 2.4
var shell := Color(0.9, 0.35, 0.22)
P._blob(st, Vector3(0, 0.05, 0), Vector3(0.12, 0.04, 0.09), n, 0.1, shell, r, 1)
for sx in [-1.0, 1.0]:
P._blob(st, Vector3(sx * 0.1, 0.04, -0.1), Vector3(0.03, 0.025, 0.045), n, 0.1, shell, r, 0)
for z in [-0.03, 0.01, 0.05]:
P._cylinder(st, Vector3(sx * 0.1, 0.03, z), Vector3(sx * 0.15, 0.01, z + 0.02), 0.008, 0.004, 3, P._mul(shell, 0.85), r)
return P._commit(st)
static func _salt() -> ArrayMesh:
var st := P._begin()
var r := _rng(331)

View File

@@ -21,6 +21,8 @@ const Creature = preload("res://scripts/creatures/creature.gd")
const BearScript = preload("res://scripts/creatures/bear.gd")
const CrowScript = preload("res://scripts/creatures/crow.gd")
const GuardScript = preload("res://scripts/creatures/guard.gd")
const GullScript = preload("res://scripts/creatures/gull.gd")
const CrabScript = preload("res://scripts/creatures/crab.gd")
const StagScript = preload("res://scripts/creatures/old_stag.gd") # (rears and lowers its antlers) # (falls apart in its own way)
const Bobber = preload("res://scripts/fishing/bobber.gd")
const Projectile = preload("res://scripts/creatures/projectile.gd")
@@ -99,7 +101,7 @@ func _server_tick(w: Node3D, delta: float) -> void:
const PER_PACKET := 40
const ENTRY := 23
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf", "drowned"]
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf", "drowned", "crab", "gull"]
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash",
"warn", "raid", "eat", "fly", "fly_off", "peck", "sneak", "carry", "circle", "telegraph", "stunned", "summon", "stomp"]
@@ -198,7 +200,8 @@ func _s2c_life(packed: PackedByteArray, fl: Array) -> void:
var c: Node3D = got if is_instance_valid(got) else null
if c == null:
# (Kinds with their own look: the bear rears up, a crow flaps.)
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript, "drowned": GuardScript, "old_stag": StagScript}.get(kind, Creature)
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript, "drowned": GuardScript, "old_stag": StagScript,
"gull": GullScript, "crab": CrabScript}.get(kind, Creature)
c = script.new()
c.puppet = true
c.setup(kind, world, e[2], cid)

View File

@@ -533,7 +533,7 @@ func _run(p: Node3D, what: String, a: Array) -> void:
for m in w.get_tree().get_nodes_in_group("processor"):
m.step(float(a[0]) if a.size() > 0 and (a[0] is float or a[0] is int) else 60.0)
"debug_spawn":
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf", "drowned"]) and _v(a, 1).is_finite():
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf", "drowned", "crab", "gull"]) and _v(a, 1).is_finite():
w.creatures.spawn(_s(a, 0), _v(a, 1))
"debug_boss":
# Tests: ["hold", secs] stands the Old Stag still (stunned);

View File

@@ -768,6 +768,14 @@ func sea_weight(x: float, z: float) -> float:
_ensure_noise()
return _sea_w(x, z)
## Whether (x, z) is on the sand (0.6.0): the beach side of the rim, low
## enough for the sandy shoreline (terrain.gdshader paints sand below
## water_level + SAND_TOP), or under the water off it.
const SAND_TOP := 1.2
func on_sand(x: float, z: float) -> bool:
_ensure_noise()
return _beach_w(x, z) > 0.5 and sample_height(x, z) < water_level + SAND_TOP
## How strongly (x, z) lies on the beach side of the rim (0..1; W2).
func beach_weight(x: float, z: float) -> float:
_ensure_noise()

View File

@@ -74,6 +74,7 @@ var bobber: Node3D ## bobber.gd: the cast fishing float, if any
var wolves: Node3D ## wolf_pack.gd (null if this seed has no den site)
var bears: Node3D ## bear_range.gd: the island's bear (null online as a client)
var thieves: Node3D ## thieves.gd: crows and foxes after food left out
var shore: Node3D ## shore.gd: the Cove's crabs (null in a land without them)
var hazards: Node3D ## hazards.gd: lightning on trees, hard-winter ice (P4)
var stones: Node3D ## standing_stones.gd: the altar and the Old Stag (P5)
var barrow: Node3D ## barrow.gd: the burial mound and its guards (scenery; guards where creatures run)
@@ -513,6 +514,12 @@ func _setup_bear() -> void:
thieves.enabled = not "--no-creatures" in OS.get_cmdline_user_args()
if barrow != null and barrow.found and thieves.enabled:
barrow.start()
if has_wildlife("crabs"):
shore = preload("res://scripts/creatures/shore.gd").new()
shore.name = "Shore"
add_child(shore)
shore.setup(self)
shore.enabled = thieves.enabled
if not has_place("bear"):
return # (bears only in the lands that list one: the Valley)
bears = preload("res://scripts/creatures/bear_range.gd").new()
@@ -1314,6 +1321,11 @@ func has_place(kind: String) -> bool:
return kind in DEFAULT_PLACES
return places.any(func(p): return p is Dictionary and String(p.get("type", "")) == kind)
## Whether this land has wildlife `kind` beyond the usual ("crabs",
## "gulls"): its worldlet's "wildlife" list (0.6.0; the Cove's).
func has_wildlife(kind: String) -> bool:
return kind in Array(terrain.worldlet.get("wildlife", []))
## This worldlet's id ("valley") and its whole definition.
func worldlet_id() -> String:
return terrain.worldlet_id