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:
113
scripts/creatures/crab.gd
Normal file
113
scripts/creatures/crab.gd
Normal 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)
|
||||
1
scripts/creatures/crab.gd.uid
Normal file
1
scripts/creatures/crab.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://kqumi2x6lfai
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
56
scripts/creatures/gull.gd
Normal 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 = {}
|
||||
1
scripts/creatures/gull.gd.uid
Normal file
1
scripts/creatures/gull.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://draw1wvqi3cif
|
||||
106
scripts/creatures/shore.gd
Normal file
106
scripts/creatures/shore.gd
Normal 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
|
||||
1
scripts/creatures/shore.gd.uid
Normal file
1
scripts/creatures/shore.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b6eeco4cs028q
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user