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:
BIN
assets/audio/sfx/creatures/crab_click_0.ogg
Normal file
BIN
assets/audio/sfx/creatures/crab_click_0.ogg
Normal file
Binary file not shown.
BIN
assets/audio/sfx/creatures/crab_click_1.ogg
Normal file
BIN
assets/audio/sfx/creatures/crab_click_1.ogg
Normal file
Binary file not shown.
BIN
assets/audio/sfx/creatures/gull_cry_0.ogg
Normal file
BIN
assets/audio/sfx/creatures/gull_cry_0.ogg
Normal file
Binary file not shown.
BIN
assets/audio/sfx/creatures/gull_cry_1.ogg
Normal file
BIN
assets/audio/sfx/creatures/gull_cry_1.ogg
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/audio/voice/hint_crab.ogg
Normal file
BIN
assets/audio/voice/hint_crab.ogg
Normal file
Binary file not shown.
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"hint_cold": "91c5088e453fb0e6",
|
||||
"hint_cove": "126f0b5e977fbef7",
|
||||
"hint_cove": "f7e5c0283b31e1cc",
|
||||
"hint_crab": "283542496ecd180c",
|
||||
"hint_died": "e865f8a63622b3c3",
|
||||
"hint_diet": "3178b319c44e9fa0",
|
||||
"hint_freezing": "48b1e505119a0fc9",
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
{"id": "seam", "when": "seam", "hold": 3, "title": "The cracked seam",
|
||||
"text": "That antler pick bites rock iron only scratches. Out on the clifftop, near the edge, there's a seam of cracked rock, and a way down to the sea behind it if you dig. Take wood to prop it as you go, and lights: it's black as pitch in there."},
|
||||
{"id": "cove", "when": "worldlet:cove", "hold": 3, "title": "The Cove",
|
||||
"text": "You made it down. Sand, salt and sea fish. Fish off the end of the pier: mackerel bite close in, cod further out where it's deep. The sea cave takes you back up the tunnel, and home's waiting at the top."}
|
||||
"text": "You made it down. Sand, salt and sea fish. Fish off the end of the pier: mackerel bite close in, cod further out where it's deep. The sea cave takes you back up the tunnel, and home's waiting at the top. Mind the gulls: they'll have any food you leave on the sand, and fish off a smoking rack. And at dusk the crabs come up the beach: give them room, or they pinch."},
|
||||
{"id": "crab", "when": "has:crab_meat", "hold": 2, "title": "Crab",
|
||||
"text": "Two lots of crab meat cook into a good crab supper at a campfire. Raw, it's as risky as raw fish."}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -593,6 +593,39 @@
|
||||
},
|
||||
"keeps": 4
|
||||
},
|
||||
{
|
||||
"id": "crab_meat",
|
||||
"name": "Crab Meat",
|
||||
"category": "food",
|
||||
"stack": 20,
|
||||
"description": "Sweet white meat from a crab off the Cove's beach. Cook it at a campfire. (Fish)",
|
||||
"mesh": "crab_meat",
|
||||
"food": {
|
||||
"hunger": 4,
|
||||
"health": -2,
|
||||
"diet": {
|
||||
"fish": 8
|
||||
},
|
||||
"risk": 0.2
|
||||
},
|
||||
"keeps": 2
|
||||
},
|
||||
{
|
||||
"id": "cooked_crab",
|
||||
"name": "Cooked Crab",
|
||||
"category": "food",
|
||||
"stack": 20,
|
||||
"description": "Boiled pink in its shell. Worth the pinches. (Fish)",
|
||||
"mesh": "cooked_crab",
|
||||
"food": {
|
||||
"hunger": 20,
|
||||
"health": 10,
|
||||
"diet": {
|
||||
"fish": 32
|
||||
}
|
||||
},
|
||||
"keeps": 3
|
||||
},
|
||||
{
|
||||
"id": "grilled_cod",
|
||||
"name": "Grilled Cod",
|
||||
|
||||
@@ -259,6 +259,16 @@
|
||||
"mackerel": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "cooked_crab",
|
||||
"output": "cooked_crab",
|
||||
"count": 1,
|
||||
"station": "campfire",
|
||||
"category": "cooking",
|
||||
"inputs": {
|
||||
"crab_meat": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "grilled_cod",
|
||||
"output": "grilled_cod",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
],
|
||||
"climate": {"snowline": 42.0},
|
||||
"places": [{"type": "den"}, {"type": "pier", "at": "beach"}, {"type": "wreck"}],
|
||||
"wildlife": ["crabs", "gulls"],
|
||||
"next": [
|
||||
{"to": "valley", "by": "tunnel", "where": "sea_cave"},
|
||||
{"to": "highlands", "by": "sail", "where": "pier"}
|
||||
|
||||
@@ -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
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
||||
echo "Importing the project first (fresh copy)..."
|
||||
godot --headless --path . --import >/dev/null 2>&1
|
||||
fi
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_tunnel test_map test_terrain_edges test_boundary; do
|
||||
for t in test_survival_core test_survival_stats test_save_load test_weather_building test_creatures_combat test_wolves test_alpha test_bear test_thieves test_barrow test_boss test_den_sites test_fishing_diet test_food test_hazards test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_boss test_net_rules test_net_lands test_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies test_cove test_wreck test_shore test_tunnel test_map test_terrain_edges test_boundary; do
|
||||
extra=""
|
||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||
# A suite that waits on a world that failed to load would hang forever,
|
||||
|
||||
312
tests/test_shore.gd
Normal file
312
tests/test_shore.gd
Normal file
@@ -0,0 +1,312 @@
|
||||
extends SceneTree
|
||||
## 0.6.0 (docs/WORLDLETS_DESIGN.md §5): the Cove's own wildlife. Crabs come
|
||||
## up the beach at dusk (never in the Valley, never by day), keep to the
|
||||
## sand, pinch anyone who walks too close and let them go once they back
|
||||
## off; a dead one gives crab meat, which cooks; at dawn they go back into
|
||||
## the sea. Gulls are the Cove's crows: food left on the sand draws a gull
|
||||
## (inland it's still a crow), and so does fish on a smoking rack nobody's
|
||||
## watching; a gull snatches one and flies off, and drops it if you shoot it.
|
||||
## Run: godot --headless --path . --script res://tests/test_shore.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var S: Node
|
||||
var A: Node
|
||||
var DB: Node
|
||||
var _toasts: Array[String] = []
|
||||
var w: Node3D
|
||||
var player: CharacterBody3D
|
||||
var sh: Node
|
||||
var th: Node
|
||||
var beach := Vector3.ZERO ## a spot on the sand, just above the waterline
|
||||
|
||||
func check(cond: bool, what: String) -> void:
|
||||
print((" ok " if cond else " FAIL ") + what)
|
||||
if not cond:
|
||||
_fails += 1
|
||||
|
||||
func _initialize() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
func _run() -> void:
|
||||
S = root.get_node("Saves")
|
||||
A = root.get_node("Actions")
|
||||
DB = root.get_node("ItemDB")
|
||||
A.notify.connect(func(t): _toasts.append(t))
|
||||
S.root_dir = "user://test_shore_worlds"
|
||||
for x in S.list_worlds():
|
||||
S.delete_world(x.id)
|
||||
await _body()
|
||||
check(_finished, "test ran to the end (no script errors)")
|
||||
for x in S.list_worlds():
|
||||
S.delete_world(x.id)
|
||||
DirAccess.remove_absolute(S.root_dir)
|
||||
print("RESULT: %s (%d failures)" % ["PASS" if _fails == 0 else "FAIL", _fails])
|
||||
quit(1 if _fails > 0 else 0)
|
||||
|
||||
func _load() -> Node3D:
|
||||
var x: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(x)
|
||||
for _i in range(8):
|
||||
await physics_frame
|
||||
return x
|
||||
|
||||
func _set_time(hours: float) -> void:
|
||||
w.day_night.day = 3
|
||||
w.day_night.time = hours / 24.0
|
||||
w.day_night.apply(true)
|
||||
|
||||
func _ground(p: Vector3) -> Vector3:
|
||||
return Vector3(p.x, w.terrain.sample_height(p.x, p.z), p.z)
|
||||
|
||||
func _toast_has(s: String) -> bool:
|
||||
return _toasts.any(func(t): return String(t).contains(s))
|
||||
|
||||
## Runs the crabs (shore.gd and each crab) for up to `secs`.
|
||||
func _sim_crabs(secs: float, until: Callable = Callable()) -> float:
|
||||
var t := 0.0
|
||||
while t < secs:
|
||||
sh.step(0.1)
|
||||
for c in sh.get_children():
|
||||
if is_instance_valid(c) and not c.is_queued_for_deletion():
|
||||
c.set_physics_process(false)
|
||||
c.step(0.1)
|
||||
t += 0.1
|
||||
if until.is_valid() and until.call():
|
||||
break
|
||||
if int(t * 10) % 10 == 0:
|
||||
await physics_frame
|
||||
return t
|
||||
|
||||
## Runs the thieves for up to `secs`.
|
||||
func _sim_thieves(secs: float, until: Callable = Callable()) -> float:
|
||||
var t := 0.0
|
||||
while t < secs:
|
||||
th.step(0.1)
|
||||
for c in th.get_children():
|
||||
if is_instance_valid(c) and not c.is_queued_for_deletion():
|
||||
c.set_physics_process(false)
|
||||
c.step(0.1)
|
||||
t += 0.1
|
||||
if until.is_valid() and until.call():
|
||||
break
|
||||
if int(t * 10) % 10 == 0:
|
||||
await physics_frame
|
||||
return t
|
||||
|
||||
func _thief(kind: String) -> Node3D:
|
||||
for c in th.alive():
|
||||
if c.kind == kind:
|
||||
return c
|
||||
return null
|
||||
|
||||
func _clear_thieves() -> void:
|
||||
for c in th.get_children():
|
||||
c.queue_free()
|
||||
for d in w.all_dropped():
|
||||
d.queue_free()
|
||||
await physics_frame
|
||||
|
||||
## A spot on the sand just above the waterline, in the middle of the bay.
|
||||
func _find_beach() -> Vector3:
|
||||
var t: Node3D = w.terrain
|
||||
var a := deg_to_rad(float(t.beach_angle_deg))
|
||||
for off in [0.0, 0.25, -0.25, 0.5, -0.5]:
|
||||
for r in range(20, 140):
|
||||
var x := cos(a + off) * r
|
||||
var z := sin(a + off) * r
|
||||
var h: float = t.sample_height(x, z)
|
||||
if t.beach_weight(x, z) > 0.85 and h > t.water_level + 0.5 and h < t.water_level + 0.9:
|
||||
return Vector3(x, h, z)
|
||||
return Vector3.INF
|
||||
|
||||
func _body() -> void:
|
||||
print("the Valley")
|
||||
S.current_id = S.create_world("Valley", 1337)
|
||||
w = await _load()
|
||||
check(w.shore == null, "no crabs in the Valley")
|
||||
check(w.thieves != null and not w.thieves.gulls, "and no gulls")
|
||||
w.queue_free()
|
||||
for _i in range(3):
|
||||
await process_frame
|
||||
|
||||
S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"})
|
||||
w = await _load()
|
||||
player = w.player
|
||||
player.set_physics_process(false)
|
||||
player.survival.set_process(false)
|
||||
w.day_night.frozen = true
|
||||
w.weather.set_process(false)
|
||||
w.weather.force("clear")
|
||||
if w.wolves != null:
|
||||
w.wolves.enabled = false
|
||||
for c in w.creatures.alive():
|
||||
c.queue_free()
|
||||
sh = w.shore
|
||||
th = w.thieves
|
||||
check(sh != null and th.gulls, "the Cove has crabs and gulls")
|
||||
if sh == null:
|
||||
return
|
||||
sh.set_physics_process(false)
|
||||
th.enabled = false
|
||||
w.creatures.set_process(false) # (keeps rabbits out of the way; creatures.enabled stays true)
|
||||
beach = _find_beach()
|
||||
check(beach != Vector3.INF, "a spot on the beach (%s)" % [beach])
|
||||
if beach == Vector3.INF:
|
||||
return
|
||||
await _crabs()
|
||||
await _gulls()
|
||||
_net()
|
||||
_finished = true
|
||||
|
||||
func _crabs() -> void:
|
||||
print("crabs")
|
||||
_set_time(12.0)
|
||||
player.global_position = beach
|
||||
await _sim_crabs(10.0)
|
||||
check(sh.alive().is_empty(), "none by day")
|
||||
_set_time(18.0)
|
||||
await _sim_crabs(20.0, func(): return sh.alive().size() >= sh.BUDGET)
|
||||
var crabs: Array = sh.alive()
|
||||
check(crabs.size() == sh.BUDGET, "at dusk they come up the beach (%d)" % crabs.size())
|
||||
var t: Node3D = w.terrain
|
||||
check(crabs.all(func(c): return t.on_sand(c.global_position.x, c.global_position.z)),
|
||||
"on the sand at the waterline (%s)" % [crabs.map(func(c): return "%.2f" % (c.global_position.y - t.water_level))])
|
||||
check(crabs.all(func(c): return c.global_position.distance_to(beach) >= sh.NOT_WITHIN), "and not right beside you")
|
||||
print("keeping to the sand")
|
||||
player.global_position = beach
|
||||
await _sim_crabs(40.0)
|
||||
crabs = sh.alive()
|
||||
check(crabs.size() >= 3 and crabs.all(func(c): return t.water_level + 0.1 < c.global_position.y and c.global_position.y < t.water_level + t.SAND_TOP + 0.2),
|
||||
"40 s on, they're all still on the sand (%d)" % crabs.size())
|
||||
print("a pinch")
|
||||
for c in crabs:
|
||||
c.queue_free()
|
||||
await physics_frame
|
||||
var crab: Node3D = sh.spawn(beach + Vector3(1.6, 0, 0))
|
||||
crab.set_physics_process(false)
|
||||
player.global_position = _ground(beach)
|
||||
player.survival.health = player.survival.MAX
|
||||
var h0: float = player.survival.health
|
||||
sh.enabled = false # (just this one)
|
||||
await _sim_crabs(3.0)
|
||||
check(crab.state == "charge", "walk within a couple of metres and it comes at you")
|
||||
check(player.survival.health < h0 and player.survival.health > h0 - 20.0, "and pinches (%.0f -> %.0f)" % [h0, player.survival.health])
|
||||
check(crab.interact_prompt(player).contains("claws up"), "(\"%s\")" % crab.interact_prompt(player))
|
||||
player.global_position = _ground(crab.global_position + Vector3(9, 0, 0))
|
||||
await _sim_crabs(8.0)
|
||||
check(crab.state in ["wander", "idle"], "back off and it lets you go (%s)" % crab.state)
|
||||
var h1: float = player.survival.health
|
||||
await _sim_crabs(5.0)
|
||||
check(is_equal_approx(player.survival.health, h1), "no more pinches from over there")
|
||||
print("crab supper")
|
||||
crab.take_hit(20.0, player.global_position)
|
||||
check(crab.is_dead() and crab.interact_prompt(player).contains("Crack it open"), "a blow kills it: [E] Crack it open")
|
||||
player.global_position = _ground(crab.global_position + Vector3(0.8, 0, 0))
|
||||
var before: int = player.inventory.count("crab_meat")
|
||||
A.butcher(player, crab)
|
||||
var got: int = player.inventory.count("crab_meat") - before
|
||||
check(got >= 1 and got <= 2, "crab meat (%d)" % got)
|
||||
check(DB.get_item("crab_meat").has("food") and DB.get_item("cooked_crab").has("food"), "it's food, raw and cooked")
|
||||
var r: Dictionary = DB.recipes.get("cooked_crab", {})
|
||||
check(not r.is_empty() and String(r.get("station", "")) == "campfire" and int(Dictionary(r.inputs).get("crab_meat", 0)) == 2,
|
||||
"two of it cook into a Cooked Crab at a campfire")
|
||||
print("dawn")
|
||||
sh.enabled = true
|
||||
_set_time(19.0)
|
||||
await _sim_crabs(20.0, func(): return sh.alive().size() >= 3)
|
||||
check(sh.alive().size() >= 3, "(a few more come up)")
|
||||
_set_time(7.0)
|
||||
await _sim_crabs(2.0)
|
||||
check(sh.alive().all(func(c): return c.state == "home"), "at dawn they head back to the sea")
|
||||
await _sim_crabs(45.0, func(): return sh.get_children().filter(func(c): return not c.is_queued_for_deletion()).is_empty())
|
||||
check(sh.get_children().filter(func(c): return not c.is_queued_for_deletion()).is_empty(), "and are gone")
|
||||
await _sim_crabs(10.0)
|
||||
check(sh.alive().is_empty(), "none come up by day")
|
||||
sh.enabled = false
|
||||
|
||||
func _gulls() -> void:
|
||||
print("gulls on the sand")
|
||||
_set_time(12.0)
|
||||
_toasts.clear()
|
||||
var fish: Node3D = w.drop_item("mackerel", 3, Transform3D(Basis(), beach + Vector3(0, 0.15, 0)))
|
||||
player.global_position = _ground(beach + Vector3(-25, 0, 0))
|
||||
check(not th.on_sand(w.build.debug_find_site(w._spawn, 6.0)), "(the grass above the beach isn't sand)")
|
||||
await _sim_thieves(30.0, func(): return _thief("gull") != null or _thief("crow") != null)
|
||||
var gull := _thief("gull")
|
||||
check(gull != null and _thief("crow") == null, "food left on the sand draws a gull, not a crow")
|
||||
if gull == null:
|
||||
return
|
||||
await _sim_thieves(30.0, func(): return not gull.carry.is_empty())
|
||||
check(String(gull.carry.get("id", "")) == "mackerel" and int(fish.amount) == 2, "it snatches one mackerel (%d left)" % int(fish.amount))
|
||||
check(_toast_has("gull has snatched"), "and you hear about it (%s)" % [_toasts])
|
||||
check(gull.state == "fly_off", "then it's off with it")
|
||||
print("shooting one")
|
||||
await _sim_thieves(1.0)
|
||||
gull.take_hit(10.0, player.global_position)
|
||||
check(gull.is_dead(), "an arrow brings it down")
|
||||
var back: Array = w.all_dropped().filter(func(d): return d.item_id == "mackerel" and d != fish)
|
||||
check(back.size() == 1 and int(back[0].amount) == 1, "and it drops the mackerel")
|
||||
check(int(gull.butcher_drops().get("feather", 0)) >= 1, "(feathers)")
|
||||
await _clear_thieves()
|
||||
|
||||
print("inland")
|
||||
var site: Vector3 = w.build.debug_find_site(w._spawn, 6.0)
|
||||
if th.on_sand(site):
|
||||
site = _ground(Vector3(-40, 0, -10))
|
||||
var berries: Node3D = w.drop_item("berries", 3, Transform3D(Basis(), _ground(site) + Vector3(0, 0.15, 0)))
|
||||
player.global_position = _ground(site + Vector3(25, 0, 0))
|
||||
await _sim_thieves(30.0, func(): return not th.alive().is_empty())
|
||||
check(_thief("crow") != null and _thief("gull") == null, "food left inland still draws a crow")
|
||||
berries.queue_free()
|
||||
await _clear_thieves()
|
||||
|
||||
print("a smoking rack")
|
||||
var rp := _ground(site + Vector3(3, 0, 3))
|
||||
var rack: Node3D = w.spawn_piece(DB.get_item("smoking_rack"), Transform3D(Basis(), rp))
|
||||
rack.set_process(false)
|
||||
rack.inventory.add("mackerel", 1) # (one: not enough to smoke, so it stays raw)
|
||||
rack.inventory.add("smoked_fish", 2)
|
||||
player.global_position = _ground(rp + Vector3(6, 0, 0))
|
||||
await _sim_thieves(40.0)
|
||||
check(th.alive().is_empty(), "with you beside it, nothing comes")
|
||||
player.global_position = _ground(rp + Vector3(25, 0, 0))
|
||||
_toasts.clear()
|
||||
await _sim_thieves(30.0, func(): return _thief("gull") != null)
|
||||
gull = _thief("gull")
|
||||
check(gull != null, "left alone, a gull comes for the fish on it")
|
||||
if gull != null:
|
||||
await _sim_thieves(30.0, func(): return not gull.carry.is_empty())
|
||||
check(rack.inventory.count("mackerel") + rack.inventory.count("smoked_fish") == 2, "and takes one (%s)" % [gull.carry])
|
||||
check(_toast_has("smoking rack"), "(\"A gull's at the fish on your smoking rack!\")")
|
||||
await _clear_thieves()
|
||||
rack.inventory.remove("mackerel", 5)
|
||||
rack.inventory.remove("smoked_fish", 5)
|
||||
rack.inventory.add("wood", 2)
|
||||
await _sim_thieves(40.0)
|
||||
check(th.alive().is_empty(), "no fish on it, no gull")
|
||||
print("at night")
|
||||
_set_time(22.0)
|
||||
var meat: Node3D = w.drop_item("raw_meat", 2, Transform3D(Basis(), beach + Vector3(0, 0.15, 0)))
|
||||
player.global_position = _ground(beach + Vector3(25, 0, 0))
|
||||
await _sim_thieves(30.0, func(): return not th.alive().is_empty())
|
||||
check(_thief("fox") != null and _thief("gull") == null, "at night it's a fox, sand or not")
|
||||
meat.queue_free()
|
||||
await _clear_thieves()
|
||||
|
||||
func _net() -> void:
|
||||
print("online")
|
||||
var CN: Node = root.get_node("CreatureNet")
|
||||
for k in [["crab", "home"], ["crab", "charge"], ["gull", "peck"]]:
|
||||
var c: Node3D = (sh.CrabScript if k[0] == "crab" else th.GullScript).new()
|
||||
c.setup(k[0], w, beach, 5)
|
||||
c.set_physics_process(false)
|
||||
w.add_child(c)
|
||||
c.set_meta("cid", 42)
|
||||
c.state = k[1]
|
||||
var buf := PackedByteArray()
|
||||
buf.resize(CN.ENTRY)
|
||||
CN._encode(buf, 0, c)
|
||||
var e: Array = CN._decode(buf, 0)
|
||||
check(e[1] == k[0] and e[4] == k[1], "a %s goes over the network (%s, %s)" % [k[0], e[1], e[4]])
|
||||
c.free()
|
||||
1
tests/test_shore.gd.uid
Normal file
1
tests/test_shore.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cydvl5o72vanm
|
||||
@@ -616,6 +616,29 @@ def make_voices():
|
||||
_encode(os.path.join(d, "fox_yip_%d.ogg" % i),
|
||||
[voice(200 + i, 0.18, lambda q, k=i: 800 + 80 * k + 500 * sin(math.pi * q), _swell(0.05, 0.5),
|
||||
[(1100, 3, 1.0), (2600, 4, 0.4)], breath=0.2, rasp=0.2, jitter=0.02)], SR_FX, 5)
|
||||
# 0.6.0, the Cove: a gull's "kee-ow" (and a laughing "ha-ha-ha"), and a
|
||||
# crab's claws clicking.
|
||||
_encode(os.path.join(d, "gull_cry_0.ogg"),
|
||||
[voice(210, 0.55, lambda q: 1350 - 500 * q ** 1.4 + 120 * sin(math.pi * min(1, q * 3)), _swell(0.06, 0.55),
|
||||
[(1700, 4, 1.0), (3300, 5, 0.5)], breath=0.25, rasp=0.25, rasp_hz=70, jitter=0.03)], SR_FX, 5)
|
||||
hs = [0.0, 0.14, 0.28, 0.42]
|
||||
def genv(q, ps=hs):
|
||||
t = q * 0.6
|
||||
return max(max(0.0, min(1, (t - p) / 0.015)) * max(0.0, 1 - (t - p) / 0.11) if t >= p else 0.0 for p in ps)
|
||||
_encode(os.path.join(d, "gull_cry_1.ogg"),
|
||||
[voice(211, 0.6, lambda q: 1150 - 150 * q, genv, [(1600, 4, 1.0), (3100, 5, 0.5)],
|
||||
breath=0.3, rasp=0.3, rasp_hz=70, jitter=0.03)], SR_FX, 5)
|
||||
for i in range(2):
|
||||
rng = random.Random(220 + i)
|
||||
cb = fx_buf(0.3)
|
||||
for t in [0.0, 0.07, 0.11, 0.2][: 2 + i * 2]:
|
||||
i0 = int(t * SR_FX)
|
||||
f = rng.uniform(2600, 3600)
|
||||
for j in range(int(0.012 * SR_FX)):
|
||||
if i0 + j < len(cb):
|
||||
cb[i0 + j] += sin(TAU * f * j / SR_FX) * exp(-j / 60) * 0.9
|
||||
add_noise(cb, SR_FX, t, 0.004, 0.7, lp=0.95, hp=0.4, env=lambda q: 1 - q, rng=rng)
|
||||
_encode(os.path.join(d, "crab_click_%d.ogg" % i), [cb], SR_FX, 5)
|
||||
|
||||
|
||||
def make_sfx():
|
||||
|
||||
Reference in New Issue
Block a user