W3: the Cove (bay, pond, pier for sea fishing), mackerel and cod (cod only in deep water), non-start worldlets offered for testing when run from source

This commit is contained in:
Paul Bailey
2026-11-06 17:08:42 +00:00
parent 9b88a10609
commit 793b214786
21 changed files with 527 additions and 14 deletions

View File

@@ -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_host test_tutorial test_tutorial_full test_hints test_updates test_audio test_items_more test_worldlets test_worldlet_pick test_water_bodies 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_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_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,

View File

@@ -30,6 +30,9 @@ func _run() -> void:
print("a beach (testbay)")
S.current_id = S.create_world("Bay", 1337, {"worldlet": "testbay"})
await _escape_all(BAY_DIRECTIONS, 90.0)
print("the Cove (all round)")
S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"})
await _escape_all(DIRECTIONS, -1.0)
for w in S.list_worlds():
S.delete_world(w.id)
DirAccess.remove_absolute(S.root_dir)

175
tests/test_cove.gd Normal file
View File

@@ -0,0 +1,175 @@
extends SceneTree
## W3 (docs/WORLDLETS_DESIGN.md §5): the Cove. A world made there has its
## bay, a pond and a pier; you walk from the sand onto the pier, cast off its
## end into the sea and catch a mackerel; cod only bite in deeper water; the
## sea fish cook like the lake's. The Cove isn't a start (you reach it on
## the journey), and its ways on are back to the Valley and on by sail.
## Run: godot --headless --path . --script res://tests/test_cove.gd
var _fails := 0
var _finished := false
var A: Node
var S: Node
var DB: Node
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:
A = root.get_node("Actions")
S = root.get_node("Saves")
DB = root.get_node("ItemDB")
S.root_dir = "user://test_cove_worlds"
for w in S.list_worlds():
S.delete_world(w.id)
await _body()
check(_finished, "test ran to the end (no script errors)")
for w in S.list_worlds():
S.delete_world(w.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 _body() -> void:
var T: Script = load("res://scripts/world/terrain.gd")
print("the Cove")
var def: Dictionary = T.load_worldlet("cove")
check(def.get("name", "") == "Cove" and not bool(def.get("start", true)), "a worldlet, not a start (reached on the journey)")
S.current_id = S.create_world("Cove", 1337, {"worldlet": "cove"})
var world: Node3D = load("res://scenes/world.tscn").instantiate()
root.add_child(world)
for _i in range(8):
await physics_frame
var t: Node3D = world.terrain
var player: CharacterBody3D = world.player
world.day_night.frozen = true
world.weather.force("clear")
world.creatures.enabled = false
world.day_night.time = 0.5
world.day_night.apply(true)
player.survival.set_process(false)
check(world.worldlet_id() == "cove", "a world made in the Cove is the Cove")
var bodies: Array = world.water_bodies().map(func(b): return b.type)
check(bodies.has("sea") and bodies.has("lake"), "with the sea and a pond (%s)" % [bodies])
check(world.crossings().any(func(c): return c.to == "valley") and world.crossings().any(func(c): return c.by == "sail"),
"its ways on: back to the Valley, and on by sail")
check(world._spawn.y > t.water_level + 1.0, "you start on dry land")
check(world.den != null and world.barrow != null and world.stones != null, "the den, the barrow and the stones found places")
check(t.get_node_or_null("Water") == null, "one water surface (the bay is the sea)")
print("the pier")
var pier: Node3D = world.pier
check(pier != null and pier.is_in_group("pier"), "a pier")
if pier == null:
return
var len: float = pier.start.distance_to(pier.end)
check(len > 20.0, "running out over the bay (%.0f m)" % len)
var at_end: Dictionary = world.water_at(pier.end.x, pier.end.z)
check(at_end.get("type", "") == "sea" and float(at_end.depth) > 1.0, "into the sea (%.1f m deep at the end)" % float(at_end.get("depth", 0.0)))
check(absf(t.sample_height(pier.start.x, pier.start.z) - pier.start.y) < 0.3, "starting at the sand, so you walk straight on")
# Walk on from the sand: the deck holds you up above the water.
var on: Vector3 = pier.start - pier.dir * 1.5
on.y = t.sample_height(on.x, on.z) + 0.2
player.global_position = on
player.velocity = Vector3.ZERO
player.rotation = Vector3(0.0, atan2(-pier.dir.x, -pier.dir.z), 0.0)
Input.action_press("move_forward")
for _i in range(int(4.0 * Engine.physics_ticks_per_second)):
await physics_frame
Input.action_release("move_forward")
var p: Vector3 = player.global_position
var along: float = (p - pier.start).dot(pier.dir)
check(along > 10.0 and p.y > t.water_level + 0.5, "walk on from the beach and along it, dry (%.0f m out, %.1f m above the water)" % [along, p.y - t.water_level])
check(not world._in_current, "(no current up on the deck)")
print("sea fishing")
player.set_physics_process(false)
player.global_position = pier.end - pier.dir * 0.8
player.inventory.add("fishing_rod", 1)
_hold(player, "fishing_rod")
var out: Vector3 = pier.end + pier.dir * 8.0
out.y = t.water_level
var got := {}
var bobs := 0
for _try in range(25):
player.survival.stamina = 100.0
if not _cast(world, player, out):
break
var b := _bob_until(world, "floating", 5.0)
if b == null or b.state != "floating":
break
bobs += 1
if bobs == 1: check(b.water_type == "sea", "the float lands on the sea (%.1f m deep)" % b.water_depth)
b = _bob_until(world, "biting", 60.0)
if b == null or b.state != "biting":
break
var fish: String = b.fish
var before: int = player.inventory.count(fish)
if A.reel(player, world) and player.inventory.count(fish) == before + 1:
got[fish] = int(got.get(fish, 0)) + 1
if got.has("mackerel") and _try >= 6:
break
check(got.has("mackerel"), "you catch a mackerel off the pier (%s)" % [got])
check(got.keys().all(func(k): return k in ["mackerel", "cod"]), "(sea fish only: no perch in the sea)")
print("cod like it deep")
var bob: Node3D = load("res://scripts/fishing/bobber.gd").new()
bob.table = DB.water_fishing("sea")
bob.world = world
bob.rng.seed = 5
var shallow := {}
var deep := {}
for i in range(200):
bob.water_depth = 1.5
shallow[bob._pick_fish()] = true
bob.water_depth = 3.0
deep[bob._pick_fish()] = true
check(not shallow.has("cod") and shallow.has("mackerel"), "in shallow sea only mackerel bite (%s)" % [shallow.keys()])
check(deep.has("cod") and deep.has("mackerel"), "cast out deep and cod bite too")
bob.free()
print("cooking them")
for fish in ["mackerel", "cod"]:
var r: Dictionary = DB.recipes.get("grilled_" + fish, {})
check(r.get("station", "") == "campfire" and r.get("inputs", {}).has(fish) and not DB.get_item("grilled_" + fish).is_empty(),
"grilled %s, at a campfire" % fish)
check(DB.recipes.get("salted_" + fish, {}).get("output", "") == "salted_fish", "and %s salts into salted fish" % fish)
check(not DB.get_item("mackerel").get("food", {}).is_empty() and DB.get_item("cod").get("food", {}).get("diet", {}).has("fish"),
"the raw fish count as fish in your diet")
world.queue_free()
await process_frame
_finished = true
func _cast(world: Node3D, player: Node3D, toward: Vector3) -> bool:
var origin: Vector3 = player.global_position + Vector3(0, 1.6, 0)
return A.cast_line(player, origin, toward - origin, world)
## Steps the float until it reaches `want` (or `secs` pass). Returns it.
func _bob_until(world: Node3D, want: String, secs: float) -> Node3D:
var tt := 0.0
while tt < secs:
var b: Node3D = world.bobber
if b == null or not is_instance_valid(b):
return null
if b.state == want:
return b
b.set_physics_process(false)
b.step(0.05)
tt += 0.05
return world.bobber
func _hold(player: Node, id: String) -> void:
var inv: Inventory = player.inventory
for i in range(inv.size()):
var s = inv.get_slot(i)
if s != null and s.id == id:
var tmp = inv.get_slot(0)
inv.set_slot(0, s)
inv.set_slot(i, tmp)
break
player.select(0)

1
tests/test_cove.gd.uid Normal file
View File

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

View File

@@ -118,7 +118,8 @@ func _body(world: Node3D) -> void:
for f in lt.get("fish", []):
ids.append(f.id)
check(ids == ["perch", "trout"] and lt.min_depth == 0.5, "the lake's fish come from data/fish.json (%s)" % [ids])
check(DB.water_fishing("sea").is_empty(), "this island has no sea fish defined (the cliffs are in the way)")
var st: Dictionary = DB.water_fishing("sea")
check(st.get("fish", []).map(func(f): return f.id) == ["mackerel", "cod"], "the sea's fish: mackerel and cod (caught in the Cove; tests/test_cove.gd)")
print("casting")
sv.stamina = 100.0

View File

@@ -21,7 +21,7 @@ func _initialize() -> void:
## Each worldlet's edges (W2): the Valley, and a test worldlet with two
## ponds and a beach (tests/worldlets/testbay.json).
const WORLDLETS := ["valley", "testbay"]
const WORLDLETS := ["valley", "cove", "testbay"]
func _all() -> void:
var failed := 0

View File

@@ -40,7 +40,7 @@ func _body(S: Node, Net: Node) -> void:
var TS: Script = load("res://scripts/ui/title_screen.gd")
print("the worldlets to start in")
var starts: Array = T.starting_worldlets()
check(starts.size() == 1 and String(starts[0].id) == "valley", "only the Valley (%s)" % [starts.map(func(w): return w.id)])
check(starts.size() == 1 and String(starts[0].id) == "valley", "only the Valley (%s; the Cove is reached on the journey)" % [starts.map(func(w): return w.id)])
check(T.worldlets().all(func(w): return w.has("start")), "every worldlet says whether it's a start")
check(T.worldlet_name("valley") == "Valley", "the Valley's name, for the menus")
check(T.worldlet_name("nowhere") == "nowhere", "(an unknown one: its id)")
@@ -51,9 +51,14 @@ func _body(S: Node, Net: Node) -> void:
root.add_child(title)
for _i in range(3):
await process_frame
var release: Array = TS.offered_worldlets(false)
check(release.size() == 1 and String(release[0].id) == "valley", "a release build offers only the Valley")
var dev: Array = TS.offered_worldlets(true)
check(String(dev[0].id) == "valley" and dev.any(func(w): return w.id == "cove" and String(w.name) == "Cove (testing)"),
"running from the source, the Cove too, marked for testing (%s)" % [dev.map(func(w): return w.name)])
var pick: OptionButton = title._worldlet_pick
check(pick != null and pick.item_count == 1 and pick.get_item_text(0) == "Valley", "a \"Where\" picker with the Valley in it")
check(pick.disabled, "which sits still with only one choice")
check(pick != null and pick.item_count == dev.size() and pick.get_item_text(0) == "Valley", "a \"Where\" picker, on the Valley first")
check(not pick.disabled, "(with more than one to choose from)")
check(TS.picked_worldlet(pick) == "valley", "and gives the Valley")
var id: String = S.create_world("Pick test", 4242, {"worldlet": TS.picked_worldlet(pick)})
var meta: Dictionary = S.list_worlds().filter(func(w): return w.id == id)[0] if not id.is_empty() else {}
@@ -69,7 +74,7 @@ func _body(S: Node, Net: Node) -> void:
check(hp != null and hp.get_item_text(hp.selected) == "Valley", "a \"Where\" picker too, on the Valley")
title._show("host")
await process_frame
check(hp.disabled and TS.picked_worldlet(hp) == "valley", "a new island: the Valley (the only start)")
check(not hp.disabled and TS.picked_worldlet(hp) == "valley", "a new island: the Valley to start with, free to change")
# Once the island exists it keeps its worldlet: the picker shows that one.
DirAccess.make_dir_recursive_absolute(DIR + "/server/worlds/server-world")
var f := FileAccess.open(DIR + "/server/worlds/server-world/meta.json", FileAccess.WRITE)