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)") # A day and a half in the Cove before going home. w.day_night.advance(w.day_night.day_length * 1.5) var cove_day: int = w.day_night.day var cove_time: float = w.day_night.time _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") check(w.day_night.day == cove_day and absf(w.day_night.time - cove_time) < 0.01, "one clock for every land: home is on the Cove's day and hour (day %d), not the one you left it" % w.day_night.day) 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