Files
Emberwild/tests/test_tutorial.gd

225 lines
8.8 KiB
GDScript

extends SceneTree
## T1 (docs/TUTORIAL_DESIGN.md §7): the Guide, data/tutorial.json, Actions'
## events, the guide panel and the marker. Plays the first chapter by
## calling Actions, the way a player would, and checks every step ticks in
## order; a step done early ticks as soon as it comes up; progress is saved.
## Run: godot --headless --path . --script res://tests/test_tutorial.gd
var _fails := 0
var _finished := false
var A: Node
var S: Node
var DB: Node
var G: Node
var world: Node3D
var player: CharacterBody3D
var ticked: Array = []
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")
G = root.get_node("Guide")
G.step_done.connect(func(_i, s): ticked.append(String(s.id)))
S.root_dir = "user://test_worlds"
for w in S.list_worlds():
S.delete_world(w.id)
S.current_id = S.create_world("Tutorial", 1337, {"tutorial": true})
await _load()
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() -> void:
world = load("res://scenes/world.tscn").instantiate()
root.add_child(world)
for _i in range(5):
await physics_frame
world.creatures.enabled = false
for c in world.creatures.alive():
c.queue_free()
if world.wolves != null:
world.wolves.enabled = false
player = world.player
player.set_physics_process(false)
player.survival.set_process(false)
func _reload() -> void:
check(world.save_now(), "saved")
world.queue_free()
await process_frame
await _load()
## The guide panel. (Headless runs have no HUD, so the test makes one, as
## the HUD does.)
var _pn: Control = null
func _panel() -> Control:
if _pn == null or not is_instance_valid(_pn):
_pn = load("res://scripts/ui/guide.gd").new()
root.add_child(_pn)
_pn.setup(player)
return _pn
## Stands next to the nearest `id` pickup and picks it up. Returns false if
## there's none.
func _take(id: String) -> bool:
var best: Node3D = null
var best_d := INF
for pk in world.props.pickups.values():
if is_instance_valid(pk) and not pk.taken and pk.item_id == id:
var d: float = pk.global_position.distance_to(player.global_position)
if d < best_d:
best_d = d
best = pk
if best == null:
return false
player.global_position = best.global_position + Vector3(0.8, 0.0, 0)
return A.pickup(player, best)
func _nearest_node(kind: String) -> Node3D:
var best: Node3D = null
var best_d := INF
for n in world.props.resource_nodes.values():
if is_instance_valid(n) and n.kind == kind and not n.depleted:
var d: float = n.global_position.distance_to(player.global_position)
if d < best_d:
best_d = d
best = n
return best
func _hold(id: String) -> void:
for i in range(player.inventory.size()):
var s = player.inventory.get_slot(i)
if s != null and s.id == id:
player.select(i)
return
## Hits `n` until it's down (chopped or broken).
func _fell(n: Node3D) -> int:
var to: Vector3 = player.global_position - n.global_position
to.y = 0.0
player.global_position = n.global_position + (to.normalized() if to.length() > 0.1 else Vector3(1, 0, 0)) * 1.4
var hits := 0
while not n.depleted and hits < 40:
player.survival.stamina = 100.0
if A.harvest_hit(player, n, n.global_position + Vector3(0, 0.6, 0)):
hits += 1
return hits
func _body() -> void:
await _starts()
await _chapter_one()
await _saving()
_finished = true
func _starts() -> void:
print("the tutorial starts")
check(G.active and G.world == world, "a tutorial world starts the guide")
var ch1: int = G.steps.filter(func(st): return st.chapter == "tools").size()
check(ch1 == 6 and G.index == 0 and String(G.current().id) == "look", "chapter 1: six steps, starting with Look around")
var pn := _panel()
check(pn != null and pn.visible and pn._box.visible, "the guide panel is up")
check(pn._title.text == "Look around" and pn._chapter.text.contains("First tools") and pn._chapter.text.contains("step 1 of %d" % G.steps.size()),
"\"%s\" / \"%s\"" % [pn._chapter.text, pn._title.text])
check(G.guide_name == "Old Tam" and pn._text.text.begins_with("Old Tam: \""), "Old Tam the hermit talks you through it: %s" % pn._text.text)
check(G.find_marker() == Vector3.INF, "(no marker for that one)")
func _chapter_one() -> void:
print("1. look around")
var start: Vector3 = player.global_position
for i in range(14):
player.global_position = start + Vector3(1.0 * (i + 1), 0, 0)
await process_frame
await process_frame
check(ticked == ["look"], "walk 10 m and it ticks (%s; walked %.0f m)" % [ticked, float(G.counters.get("walk", 0.0))])
player.global_position = start + Vector3(0, 0, 50)
await process_frame
check(float(G.counters.get("walk", 0.0)) < 15.0, "(a jump isn't counted as walking)")
player.global_position = start
await process_frame
print("2. sticks and stones")
check(String(G.current().id) == "sticks", "next: %s" % G.current().get("title", "?"))
await process_frame
G._marker = G.find_marker()
var m: Vector3 = G.marker_position()
var near_stick := false
for pk in world.props.pickups.values():
if pk.item_id == "stick" and not pk.taken and pk.global_position.distance_to(m) < 0.01:
near_stick = true
check(m != Vector3.INF and near_stick, "the marker stands on the nearest stick")
# Flax first, out of order: step 3 will tick the moment it comes up.
check(_take("fiber"), "(pick some flax early)")
check(_take("stick") and _take("stick"), "two sticks")
check(G.progress_text().contains("Sticks 2/3") and G.progress_text().contains("Stone 0/3"), "the panel counts them: \"%s\"" % G.progress_text())
check(_panel()._progress.text == G.progress_text(), "(shown on the panel)")
check(_take("stick") and _take("stone") and _take("stone"), "a third, and two stones")
check(String(G.current().id) == "sticks", "not done yet (2 of 3 stones)")
check(_take("stone"), "the third stone")
check(ticked == ["look", "sticks", "flax"], "sticks and stones tick, and so does the flax picked earlier (%s)" % [ticked])
print("4. the axe")
check(String(G.current().id) == "axe" and _panel()._title.text == "Craft a stone axe", "next: craft a stone axe")
G._marker = G.find_marker()
check(G.marker_position() != Vector3.INF, "the marker shows where flint is")
check(_take("flint") and _take("flint"), "two flint")
check(G.find_marker() == Vector3.INF, "with two in your bag, the marker goes")
check(A.craft(player, "stone_axe"), "craft it")
check(ticked.back() == "axe", "it ticks")
print("5. a tree")
G._marker = G.find_marker()
var tree := _nearest_node("tree")
check(G.marker_position().distance_to(tree.global_position) < 0.01, "the marker's on the nearest tree")
_hold("stone_axe")
var hits := _fell(tree)
check(tree.depleted and ticked.back() == "tree", "it ticks when the tree comes down (%d hits)" % hits)
print("6. pickaxe and boulder")
check(String(G.current().id) == "pickaxe", "next: the pickaxe")
while player.inventory.count("stick") < 2:
_take("stick")
if player.inventory.count("flint") < 1:
_take("flint")
check(A.craft(player, "stone_pickaxe"), "craft it")
check(String(G.current().id) == "pickaxe" and G.progress_text().contains("✓ Stone Pickaxe"), "half done: \"%s\"" % G.progress_text())
var rock := _nearest_node("rock")
_hold("stone_pickaxe")
_fell(rock)
check(rock.depleted and ticked.back() == "pickaxe", "break a boulder and it ticks")
check(ticked == ["look", "sticks", "flax", "axe", "tree", "pickaxe"], "every step ticked, in order")
check(String(G.current().get("chapter", "")) == "food" and _panel()._chapter.text.contains("Fire and food"), "chapter 1's done: on to Fire and food")
func _saving() -> void:
print("saving")
var id: String = S.create_world("Tutorial 2", 1337, {"tutorial": true})
world.queue_free()
await process_frame
S.current_id = id
ticked.clear()
await _load()
check(G.active and G.index == 0, "a fresh tutorial world starts at the beginning")
G.walked(12.0)
check(_take("stick") and _take("stone"), "a step and a bit in")
check(G.index == 1 and int(G.counters.get("gained:stick", 0)) == 1, "(on step 2, one stick so far)")
await _reload()
check(G.active and G.index == 1 and String(G.current().id) == "sticks", "after a reload it's still on step 2")
check(int(G.counters.get("gained:stick", 0)) == 1 and int(G.counters.get("gained:stone", 0)) == 1, "with what you'd gathered")
print("other worlds")
var plain: String = S.create_world("Ordinary", 1337)
world.queue_free()
await process_frame
S.current_id = plain
await _load()
_pn.queue_free()
_pn = null
check(not G.active and not _panel().visible, "an ordinary world has no tutorial")