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

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