J1: the cliff tunnel (seam, six propped stretches, dark, lights), travel Valley <-> Cove through the sea cave, a save file per worldlet; New World has no Where (--dev-worldlets to try others)
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_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
|
||||
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_tunnel 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,
|
||||
|
||||
259
tests/test_tunnel.gd
Normal file
259
tests/test_tunnel.gd
Normal file
@@ -0,0 +1,259 @@
|
||||
extends SceneTree
|
||||
## J1 (docs/WORLDLETS_DESIGN.md §10): the cliff tunnel, in single-player. Dig
|
||||
## the seam on the Valley clifftop with the antler pickaxe (a lesser pick
|
||||
## won't do), go down, dig six stretches (each propped before the next),
|
||||
## light it, walk down it, out through the sea cave to the Cove, and back up
|
||||
## the same tunnel to your chest at home. Each worldlet keeps its own save.
|
||||
## Run: godot --headless --path . --script res://tests/test_tunnel.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var A: Node
|
||||
var S: Node
|
||||
var DB: Node
|
||||
var _toasts: Array[String] = []
|
||||
var _went := ""
|
||||
|
||||
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")
|
||||
A.notify.connect(func(t): _toasts.append(t))
|
||||
S.root_dir = "user://test_tunnel_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 _load_world() -> Node3D:
|
||||
var w: Node3D = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(w)
|
||||
for _i in range(8):
|
||||
await physics_frame
|
||||
w.creatures.enabled = false
|
||||
w.weather.force("clear")
|
||||
w.player.survival.set_process(false)
|
||||
w.travelled.connect(func(to): _went = to)
|
||||
return w
|
||||
|
||||
func _drop_world(w: Node3D) -> void:
|
||||
w.queue_free()
|
||||
for _i in range(3):
|
||||
await process_frame
|
||||
|
||||
## Swings at `face` until it gives (or `limit` blows). Returns the blows.
|
||||
func _dig(w: Node3D, face: Node3D, limit: int = 40) -> int:
|
||||
var n := 0
|
||||
while is_instance_valid(face) and not face.depleted and n < limit:
|
||||
w.player.survival.stamina = 100.0
|
||||
w.player.global_position = face.global_position - _toward(w, face) * 1.6 - Vector3(0, 1.0, 0)
|
||||
if not A.harvest_hit(w.player, face, face.global_position):
|
||||
break
|
||||
n += 1
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
return n
|
||||
|
||||
func _toward(w: Node3D, face: Node3D) -> Vector3:
|
||||
var d: Vector3 = face.global_position - w.player.global_position
|
||||
d.y = 0.0
|
||||
return d.normalized() if d.length() > 0.01 else Vector3.FORWARD
|
||||
|
||||
func _hold(player: Node, id: String) -> void:
|
||||
var inv = 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)
|
||||
|
||||
func _body() -> void:
|
||||
S.current_id = S.create_world("Journey", 1337)
|
||||
var w: Node3D = await _load_world()
|
||||
var t: Node3D = w.tunnel
|
||||
var player: CharacterBody3D = w.player
|
||||
print("the seam")
|
||||
check(t != null, "the Valley has a cliff tunnel to dig (to the %s)" % (t.to if t else "-"))
|
||||
if t == null:
|
||||
return
|
||||
var ter: Node3D = w.terrain
|
||||
check(ter.inside_boundary(t.seam_at.x, t.seam_at.z, 1.0) and not ter.inside_boundary(t.seam_at.x, t.seam_at.z, 14.0)
|
||||
and ter.sea_weight(t.seam_at.x, t.seam_at.z) > 0.5, "a seam on the clifftop, near the edge")
|
||||
var seam: Node3D = t._face
|
||||
check(seam != null and seam.stage == 0, "a cracked face to dig")
|
||||
# Your hut: a chest by the start, to come back to.
|
||||
var chest: Node3D = w.spawn_piece(DB.get_item("chest"), Transform3D(Basis(), w._spawn + Vector3(3, 0, 0)))
|
||||
var chest_at: Vector3 = chest.global_position
|
||||
player.set_physics_process(false)
|
||||
player.inventory.add("stone_pickaxe", 1)
|
||||
_hold(player, "stone_pickaxe")
|
||||
_toasts.clear()
|
||||
await _dig(w, seam, 3)
|
||||
check(not t.opened and _toasts.has("Too hard: you need a better pick"), "a stone pick won't touch it (%s)" % [_toasts])
|
||||
player.inventory.add("antler_pickaxe", 1)
|
||||
_hold(player, "antler_pickaxe")
|
||||
var blows := await _dig(w, seam)
|
||||
check(t.opened, "the antler pickaxe opens it (%d blows)" % blows)
|
||||
check(player.inventory.count("stone") > 0, "(and knocks loose some stone)")
|
||||
|
||||
print("down into the dark")
|
||||
t.go_in(player)
|
||||
for _i in range(40):
|
||||
await physics_frame
|
||||
check(w.underground_at(player.global_position), "inside the tunnel")
|
||||
check(w.day_night.underground > 0.95 and w.day_night.env.ambient_light_energy < 0.05 and not w.day_night.sun.visible,
|
||||
"pitch dark: no sun, no sky (ambient %.3f)" % w.day_night.env.ambient_light_energy)
|
||||
player.survival.step(0.3)
|
||||
check(player.survival.roofed and player.survival.sheltered, "and out of the weather")
|
||||
check(player.water_level == -INF, "(and no lake water down here, though it's far below the lake)")
|
||||
|
||||
print("digging down")
|
||||
var face1: Node3D = t._face
|
||||
check(face1 != null and face1.stage == 1, "the first face, at the top")
|
||||
blows = await _dig(w, face1)
|
||||
check(t.dug == 1, "dug the first stretch (%d blows)" % blows)
|
||||
var face2: Node3D = t._face
|
||||
_toasts.clear()
|
||||
await _dig(w, face2, 2)
|
||||
check(t.dug == 1 and _toasts.any(func(s): return s.begins_with("It's loose")), "the next won't take a blow until it's propped")
|
||||
player.inventory.remove("wood", player.inventory.count("wood"))
|
||||
check(not t.set_prop(player, 1), "a prop needs wood")
|
||||
player.inventory.add("wood", 20)
|
||||
var wood0: int = player.inventory.count("wood")
|
||||
check(t.set_prop(player, 1) and t.props[1], "set a timber prop (2 wood)")
|
||||
await process_frame
|
||||
for k in range(2, t.STRETCHES + 1):
|
||||
await _dig(w, t._face)
|
||||
if k < t.STRETCHES:
|
||||
t.set_prop(player, k)
|
||||
await process_frame
|
||||
check(t.dug == t.STRETCHES, "dug all %d stretches down" % t.STRETCHES)
|
||||
check(wood0 - player.inventory.count("wood") == 2 * (t.STRETCHES - 1), "propping it all took %d wood" % (wood0 - player.inventory.count("wood")))
|
||||
check(t.bottom.y < t.top.y - 20.0, "%.0f m down" % (t.top.y - t.bottom.y))
|
||||
check(_toasts.any(func(s): return s.begins_with("You break through into a sea cave")), "and broke through into a sea cave")
|
||||
for _i in range(4):
|
||||
await physics_frame
|
||||
|
||||
print("walking it")
|
||||
player.set_physics_process(true)
|
||||
var stuck := ""
|
||||
for k in range(1, t.STRETCHES + 1):
|
||||
var m: Vector3 = t._mouth(k) + Vector3(0, 0.2, 0)
|
||||
var dir := signf(t._landing_x(k) - t._landing_x(k - 1))
|
||||
player.global_position = m - Vector3(dir * 0.6, 0, 0)
|
||||
player.velocity = Vector3.ZERO
|
||||
player.rotation = Vector3(0.0, atan2(-dir, 0.0), 0.0)
|
||||
Input.action_press("move_forward")
|
||||
for _i in range(int(2.6 * Engine.physics_ticks_per_second)):
|
||||
await physics_frame
|
||||
Input.action_release("move_forward")
|
||||
var p: Vector3 = player.global_position
|
||||
var want_y: float = t.ORIGIN.y + t._floor_y(k)
|
||||
if absf(p.y - want_y) > 0.6 or not w.underground_at(p):
|
||||
stuck += " stretch %d ended at (%.1f, %.1f, %.1f), floor %.1f;" % [k, p.x, p.y, p.z, want_y]
|
||||
check(stuck.is_empty(), "walk down every stretch to the landing below%s" % stuck)
|
||||
stuck = ""
|
||||
for k in range(t.STRETCHES, 0, -1):
|
||||
var dir := signf(t._landing_x(k - 1) - t._landing_x(k)) # (back up: towards the landing above)
|
||||
var foot: Vector3 = t.ORIGIN + Vector3(t._landing_x(k) + dir * (t.LAND * 0.5 - 0.6), t._floor_y(k) + 0.2, t._lane_z(k))
|
||||
player.global_position = foot
|
||||
player.velocity = Vector3.ZERO
|
||||
player.rotation = Vector3(0.0, atan2(-dir, 0.0), 0.0)
|
||||
Input.action_press("move_forward")
|
||||
for _i in range(int(3.4 * Engine.physics_ticks_per_second)):
|
||||
await physics_frame
|
||||
Input.action_release("move_forward")
|
||||
var p: Vector3 = player.global_position
|
||||
var want_y: float = t.ORIGIN.y + t._floor_y(k - 1)
|
||||
if absf(p.y - want_y) > 0.6:
|
||||
stuck += " stretch %d ended at (%.1f, %.1f, %.1f), floor %.1f;" % [k, p.x, p.y, p.z, want_y]
|
||||
check(stuck.is_empty(), "and back up every stretch to the landing above%s" % stuck)
|
||||
|
||||
print("light")
|
||||
var floor_at: Vector3 = t._mouth(3) + Vector3(0.0, 0.0, 0.6)
|
||||
floor_at.x += 0.8 * signf(t._landing_x(2) - t._landing_x(3))
|
||||
var xf := Transform3D(Basis(), floor_at)
|
||||
check(w.placement_valid(DB.get_item("torch_stand"), xf), "a torch stand can stand on the tunnel floor")
|
||||
player.set_physics_process(false)
|
||||
player.global_position = floor_at + Vector3(1.0, 0, 0)
|
||||
player.inventory.add("torch_stand", 1)
|
||||
var slot := -1
|
||||
for i in range(player.inventory.size()):
|
||||
var s = player.inventory.get_slot(i)
|
||||
if s != null and s.id == "torch_stand":
|
||||
slot = i
|
||||
check(A.place(player, slot, xf, w), "set one down")
|
||||
var lights: int = w.all_pieces().filter(func(p): return p.item_id == "torch_stand").size()
|
||||
check(lights == 1, "(it's a piece like any other, so it stays: %d)" % lights)
|
||||
|
||||
print("saved with the Valley")
|
||||
w.save_now()
|
||||
await _drop_world(w)
|
||||
w = await _load_world()
|
||||
t = w.tunnel
|
||||
check(t.opened and t.dug == t.STRETCHES and t.props.count(true) == t.STRETCHES - 1, "the tunnel, dug and propped, after a reload")
|
||||
check(w.all_pieces().any(func(p): return p.item_id == "torch_stand" and w.underground_at(p.global_position)), "the torch stand still down there")
|
||||
player = w.player
|
||||
|
||||
print("out to the Cove")
|
||||
var stone0: int = player.inventory.count("stone")
|
||||
var day0: int = w.day_night.day
|
||||
player.set_physics_process(true)
|
||||
player.global_position = t.cave_mouth + Vector3(4.0, 0.2, 0.0)
|
||||
player.rotation = Vector3(0.0, atan2(1.0, 0.0), 0.0) # (facing -X, out of the cave)
|
||||
_went = ""
|
||||
Input.action_press("move_forward")
|
||||
for _i in range(int(3.0 * Engine.physics_ticks_per_second)):
|
||||
await physics_frame
|
||||
if not _went.is_empty():
|
||||
break
|
||||
Input.action_release("move_forward")
|
||||
check(_went == "cove", "walking out of the sea cave's mouth goes on to the Cove")
|
||||
check(S.worldlet_of(S.current_id) == "cove" and S.has_state(S.current_id, "cove"), "(the save is in the Cove now, with a file of its own)")
|
||||
check(S.has_state(S.current_id, "valley"), "(and the Valley's is kept)")
|
||||
await _drop_world(w)
|
||||
w = await _load_world()
|
||||
player = w.player
|
||||
check(w.worldlet_id() == "cove", "the Cove")
|
||||
check(w.sea_cave != null and player.global_position.distance_to(w.sea_cave.front) < 1.5, "arriving at the sea cave on its shore")
|
||||
check(player.inventory.count("stone") == stone0 and player.inventory.count("antler_pickaxe") == 1, "with your bag as it was")
|
||||
check(w.day_night.day == day0, "on the same day")
|
||||
check(w.tunnel == null, "(the Cove has no tunnel of its own: it's the Valley's)")
|
||||
|
||||
print("back up home")
|
||||
check(w.sea_cave.open(), "the sea cave leads back up (the tunnel's dug)")
|
||||
_went = ""
|
||||
w.sea_cave.interact(player)
|
||||
check(_went == "valley", "into the cave: back to the Valley")
|
||||
await _drop_world(w)
|
||||
w = await _load_world()
|
||||
t = w.tunnel
|
||||
player = w.player
|
||||
check(w.worldlet_id() == "valley" and w.underground_at(player.global_position) and player.global_position.distance_to(t.bottom) < 2.0,
|
||||
"arriving at the bottom of the tunnel")
|
||||
t.go_out(player)
|
||||
await physics_frame
|
||||
check(not w.underground_at(player.global_position) and player.global_position.distance_to(t.seam_out) < 1.0, "climb out at the top, on the clifftop")
|
||||
check(w.all_pieces().any(func(p): return p.item_id == "chest" and p.global_position.distance_to(chest_at) < 0.1), "and your chest is waiting at home")
|
||||
await _drop_world(w)
|
||||
|
||||
print("a world that never left home")
|
||||
var id2: String = S.create_world("Homebody", 42)
|
||||
check(S.state_path(id2, "valley").ends_with("/world.json"), "keeps its world.json (older saves just work)")
|
||||
_finished = true
|
||||
1
tests/test_tunnel.gd.uid
Normal file
1
tests/test_tunnel.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dgyh7ihkmdkxl
|
||||
@@ -52,14 +52,14 @@ func _body(S: Node, Net: Node) -> void:
|
||||
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")
|
||||
check(release.size() == 1 and String(release[0].id) == "valley", "normally only the Valley is offered")
|
||||
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)])
|
||||
"with --dev-worldlets, the Cove too, marked for testing (%s)" % [dev.map(func(w): return w.name)])
|
||||
check(not TS.dev_worldlets(), "(this test runs without --dev-worldlets)")
|
||||
var pick: OptionButton = title._worldlet_pick
|
||||
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")
|
||||
check(pick == null, "New World has no \"Where\": the journey always begins in the Valley")
|
||||
check(TS.picked_worldlet(pick) == "valley", "so a new world is 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 {}
|
||||
check(String(meta.get("worldlet", "")) == "valley", "a world made from it remembers the Valley (%s)" % meta.get("worldlet", "-"))
|
||||
@@ -74,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(not hp.disabled and TS.picked_worldlet(hp) == "valley", "a new island: the Valley to start with, free to change")
|
||||
check(hp.disabled and TS.picked_worldlet(hp) == "valley", "a new island: the Valley (the only start; One land mode will widen it)")
|
||||
# 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)
|
||||
|
||||
Reference in New Issue
Block a user