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:
@@ -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
|
||||
Reference in New Issue
Block a user