T2 tutorial: all five chapters with Old Tam, scripted dusk and demo wolf, rabbits for the hunt, title-screen Tutorial entry, pause-menu skip/leave, goodbye card; full bot playthrough test
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_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_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,
|
||||
|
||||
@@ -127,10 +127,11 @@ func _body() -> void:
|
||||
func _starts() -> void:
|
||||
print("the tutorial starts")
|
||||
check(G.active and G.world == world, "a tutorial world starts the guide")
|
||||
check(G.steps.size() == 6 and G.index == 0 and String(G.current().id) == "look", "chapter 1: six steps, starting with Look around")
|
||||
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 6"),
|
||||
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)")
|
||||
@@ -195,7 +196,7 @@ func _chapter_one() -> void:
|
||||
_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(G.is_finished() and not _panel()._box.visible, "the chapter's done (and the panel steps aside)")
|
||||
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")
|
||||
|
||||
363
tests/test_tutorial_full.gd
Normal file
363
tests/test_tutorial_full.gd
Normal file
@@ -0,0 +1,363 @@
|
||||
extends SceneTree
|
||||
## T2 (docs/TUTORIAL_DESIGN.md §7): the whole tutorial, every chapter, played
|
||||
## by a bot through Actions on the tutorial world (the Valley island, seed
|
||||
## 1337): first tools, fire and food, hunting, fishing, shelter and night
|
||||
## (with the demo wolf), sleeping, and the finish card. Materials it would
|
||||
## take a while to gather by hand (wood for a hut, say) are put in the bag;
|
||||
## every goal itself is done the way a player does it.
|
||||
## Run: godot --headless --path . --script res://tests/test_tutorial_full.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 inv: Inventory
|
||||
var sv: Node
|
||||
var ticked: Array = []
|
||||
var fire: Node3D
|
||||
var bench: Node3D
|
||||
var _pn: Control
|
||||
|
||||
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)))
|
||||
A.food_rng.seed = 3 # (so raw food doesn't make the bot ill at a bad moment)
|
||||
S.root_dir = "user://test_worlds"
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
var TS: Script = load("res://scripts/ui/title_screen.gd")
|
||||
check(TS.tutorial_world().is_empty(), "no tutorial world yet")
|
||||
S.current_id = S.create_world("Tutorial", TS.TUTORIAL_SEED, {"tutorial": true})
|
||||
check(not TS.tutorial_world().is_empty(), "the title screen finds it once it's made (Tutorial, carry on)")
|
||||
world = load("res://scenes/world.tscn").instantiate()
|
||||
root.add_child(world)
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
player = world.player
|
||||
player.set_physics_process(false)
|
||||
player.survival.set_process(false)
|
||||
world.day_night.frozen = true
|
||||
world.weather.set_process(false)
|
||||
world.weather.force("clear")
|
||||
inv = player.inventory
|
||||
sv = player.survival
|
||||
_pn = load("res://scripts/ui/guide.gd").new()
|
||||
root.add_child(_pn)
|
||||
_pn.setup(player)
|
||||
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)
|
||||
|
||||
# --- helpers ------------------------------------------------------------------------------
|
||||
|
||||
func _at(id: String) -> bool:
|
||||
return String(G.current().get("id", "")) == id
|
||||
|
||||
func _ground(p: Vector3) -> Vector3:
|
||||
return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z)
|
||||
|
||||
func _near(p: Vector3, d: float = 1.2) -> void:
|
||||
var to: Vector3 = player.global_position - p
|
||||
to.y = 0.0
|
||||
player.global_position = _ground(p + (to.normalized() if to.length() > 0.1 else Vector3(1, 0, 0)) * d)
|
||||
|
||||
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 _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
|
||||
|
||||
## Puts `id` in hand: swapped into the first hotbar slot (only those can be
|
||||
## held), like a player dragging it there.
|
||||
func _hold(id: String) -> int:
|
||||
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)
|
||||
player.select(0)
|
||||
return 0
|
||||
return -1
|
||||
|
||||
func _fell(n: Node3D) -> void:
|
||||
_near(n.global_position, 1.4)
|
||||
var hits := 0
|
||||
while not n.depleted and hits < 40:
|
||||
sv.stamina = 100.0
|
||||
if A.harvest_hit(player, n, n.global_position + Vector3(0, 0.6, 0)):
|
||||
hits += 1
|
||||
|
||||
## Places what's in `id`'s slot on the ground `ahead` metres in front of you.
|
||||
func _place(id: String, ahead: float, side: float = 0.0) -> Node3D:
|
||||
var slot := _hold(id)
|
||||
var f := -player.global_basis.z
|
||||
var r := player.global_basis.x
|
||||
for k in range(12):
|
||||
var at := _ground(player.global_position + f * (ahead + 0.5 * k) + r * side)
|
||||
var xf := Transform3D(Basis(Vector3.UP, player.rotation.y), at)
|
||||
if world.placement_valid(DB.get_item(id), xf) and A.place(player, slot, xf, world):
|
||||
for p in world.all_pieces():
|
||||
if p.item_id == id and p.global_position.distance_to(at) < 0.05:
|
||||
return p
|
||||
return null
|
||||
|
||||
func _by_fire() -> void:
|
||||
_near(fire.global_position, 2.0)
|
||||
|
||||
func _step_done(id: String) -> bool:
|
||||
return ticked.has(id)
|
||||
|
||||
func _body() -> void:
|
||||
await _tools()
|
||||
await _food()
|
||||
await _hunt()
|
||||
await _fishing()
|
||||
await _night()
|
||||
_finish()
|
||||
_finished = true
|
||||
|
||||
# --- 1. first tools -------------------------------------------------------------------------
|
||||
|
||||
func _tools() -> void:
|
||||
print("1. first tools")
|
||||
check(G.active and _at("look"), "the tutorial world starts the tutorial (Old Tam: \"%s\")" % String(G.current().text).left(40))
|
||||
check(world.wolves == null or not world.wolves.enabled, "a calm island: no wolf pack")
|
||||
check(world.bears == null, "no bear, nothing stealing food")
|
||||
G.walked(11.0)
|
||||
for id in ["stick", "stick", "stick", "stone", "stone", "stone", "fiber", "flint", "flint"]:
|
||||
_take(id)
|
||||
check(A.craft(player, "stone_axe"), "an axe")
|
||||
_hold("stone_axe")
|
||||
_fell(_node("tree"))
|
||||
while inv.count("stick") < 2:
|
||||
_take("stick")
|
||||
if inv.count("flint") < 1:
|
||||
_take("flint")
|
||||
check(A.craft(player, "stone_pickaxe"), "a pickaxe")
|
||||
_hold("stone_pickaxe")
|
||||
_fell(_node("rock"))
|
||||
check(ticked == ["look", "sticks", "flax", "axe", "tree", "pickaxe"], "chapter 1 done: %s" % [ticked])
|
||||
|
||||
# --- 2. fire and food -----------------------------------------------------------------------
|
||||
|
||||
func _food() -> void:
|
||||
print("2. fire and food")
|
||||
check(_at("campfire") and _pn._chapter.text.contains("Fire and food"), "next chapter: Fire and food")
|
||||
inv.add("stone", 5)
|
||||
inv.add("stick", 4)
|
||||
check(A.craft(player, "campfire"), "a campfire, by hand")
|
||||
fire = _place("campfire", 2.5)
|
||||
check(fire != null and _step_done("campfire"), "placed: it ticks")
|
||||
fire.fuel = fire.MAX_FUEL
|
||||
print(" berries")
|
||||
G._marker = G.find_marker()
|
||||
var bush: Node3D = null
|
||||
for b in world.props.forage.values():
|
||||
if is_instance_valid(b) and b.ripe() and (bush == null or b.global_position.distance_to(player.global_position) < bush.global_position.distance_to(player.global_position)):
|
||||
bush = b
|
||||
check(bush != null and G.marker_position().distance_to(bush.global_position) < 0.05, "the marker's on the nearest ripe bush")
|
||||
_near(bush.global_position, 1.2)
|
||||
check(A.forage(player, bush) and inv.count("berries") > 0, "picked")
|
||||
sv.hunger = 40.0
|
||||
check(A.eat(player, _hold("berries")) and _step_done("berries"), "and eaten: it ticks")
|
||||
print(" a mushroom")
|
||||
check(_take("mushroom"), "found one under the trees")
|
||||
_by_fire()
|
||||
check(A.craft(player, "roasted_mushroom") and _step_done("mushroom"), "roasted at the fire: it ticks")
|
||||
print(" the diet")
|
||||
check(_at("diet") and _pn._progress.text.is_empty(), "Old Tam explains the diet bars")
|
||||
var ev := InputEventKey.new()
|
||||
ev.keycode = KEY_ENTER
|
||||
ev.pressed = true
|
||||
_pn._unhandled_input(ev)
|
||||
check(_step_done("diet"), "Enter carries on")
|
||||
|
||||
# --- 3. hunting ------------------------------------------------------------------------------
|
||||
|
||||
func _hunt() -> void:
|
||||
print("3. hunting")
|
||||
_by_fire()
|
||||
inv.add("wood", 14)
|
||||
inv.add("flint", 2)
|
||||
check(A.craft(player, "workbench"), "a workbench")
|
||||
bench = _place("workbench", 2.5, 2.5)
|
||||
check(bench != null and _step_done("workbench"), "placed")
|
||||
check(A.craft(player, "club") and _step_done("weapon"), "a club: a weapon")
|
||||
check(_at("rabbit"), "now: %s" % G.current().get("title", "?"))
|
||||
var rabbits: Array = world.creatures.alive("rabbit")
|
||||
check(rabbits.size() >= 3, "Old Tam's rabbits are about (%d)" % rabbits.size())
|
||||
G._marker = G.find_marker()
|
||||
var rb: Node3D = null
|
||||
for r in rabbits:
|
||||
if rb == null or r.global_position.distance_to(player.global_position) < rb.global_position.distance_to(player.global_position):
|
||||
rb = r
|
||||
check(G.marker_position().distance_to(rb.global_position) < 0.05, "the marker's on the nearest one")
|
||||
rb.set_physics_process(false)
|
||||
_near(rb.global_position, 1.5)
|
||||
_hold("club")
|
||||
sv.stamina = 100.0
|
||||
check(A.melee(player, rb) and rb.is_dead() and _step_done("rabbit"), "one blow with the club")
|
||||
check(A.butcher(player, rb), "butchered")
|
||||
_by_fire()
|
||||
check(A.craft(player, "cooked_meat") and _step_done("cook"), "and the meat cooked: it ticks")
|
||||
G.continue_step()
|
||||
check(_step_done("animals"), "(the tip about deer and boars)")
|
||||
|
||||
# --- 4. fishing --------------------------------------------------------------------------------
|
||||
|
||||
func _fishing() -> void:
|
||||
print("4. fishing")
|
||||
_near(bench.global_position, 1.5)
|
||||
inv.add("stick", 2)
|
||||
inv.add("fiber", 3)
|
||||
inv.add("flint", 1)
|
||||
check(A.craft(player, "fishing_rod") and _step_done("rod"), "a rod at the workbench")
|
||||
G._marker = G.find_marker()
|
||||
var shore: Vector3 = G.marker_position()
|
||||
var t: Node3D = world.terrain
|
||||
check(shore != Vector3.INF and t.sample_height(shore.x, shore.z) >= t.water_level, "the marker shows the lake shore")
|
||||
var lake: Vector2 = t.lake_center
|
||||
var x: float = lake.x
|
||||
while t.sample_height(x, lake.y) < t.water_level + 0.4 and x < lake.x + 60.0:
|
||||
x += 0.5
|
||||
player.global_position = _ground(Vector3(x + 1.0, 0, lake.y))
|
||||
_hold("fishing_rod")
|
||||
sv.stamina = 100.0
|
||||
var origin: Vector3 = player.global_position + Vector3(0, 1.6, 0)
|
||||
check(A.cast_line(player, origin, Vector3(lake.x, t.water_level, lake.y) - origin, world), "cast")
|
||||
var tt := 0.0
|
||||
while tt < 400.0:
|
||||
var b: Node3D = world.bobber
|
||||
if b == null or b.state == "biting":
|
||||
break
|
||||
b.set_physics_process(false)
|
||||
b.step(0.05)
|
||||
tt += 0.05
|
||||
check(world.bobber != null and world.bobber.state == "biting", "a bite (after %.0f s)" % tt)
|
||||
check(A.reel(player, world), "landed it")
|
||||
var fish := "perch" if inv.count("perch") > 0 else "trout"
|
||||
_by_fire()
|
||||
check(A.craft(player, "grilled_" + fish) and _step_done("fish"), "grilled: it ticks")
|
||||
|
||||
# --- 5. shelter and night ----------------------------------------------------------------------
|
||||
|
||||
func _night() -> void:
|
||||
print("5. shelter and night")
|
||||
check(_at("hut"), "now: build a hut")
|
||||
var site: Vector3 = world.build.debug_find_site(fire.global_position + Vector3(6, 0, 6), 3.0)
|
||||
var yaw: float = player.rotation.y
|
||||
var hut: Dictionary = await world.build.debug_build_hut(site, yaw)
|
||||
for _i in range(3):
|
||||
await physics_frame
|
||||
check(hut.placed == 20, "a hut (the same snapping a player uses: %d pieces)" % hut.placed)
|
||||
var b := Basis(Vector3.UP, yaw)
|
||||
var center: Vector3 = hut.pieces[0].global_transform * Vector3(1, 0, 1)
|
||||
player.global_position = center + b * Vector3(0.6, 0.05, -0.6)
|
||||
sv._shelter_left = 0.0
|
||||
sv.step(0.2)
|
||||
G.poll()
|
||||
check(_step_done("hut"), "stand inside out of the weather: it ticks")
|
||||
print(" the bed")
|
||||
_near(bench.global_position, 1.5)
|
||||
inv.add("wood", 8)
|
||||
inv.add("fiber", 6)
|
||||
check(A.craft(player, "bed"), "a bed at the workbench")
|
||||
player.global_position = center + b * Vector3(0.6, 0.05, 0.9)
|
||||
var slot := _hold("bed")
|
||||
var bxf := Transform3D(b, center + b * Vector3(-0.5, 0, -0.4))
|
||||
check(world.placement_valid(DB.get_item("bed"), bxf) and A.place(player, slot, bxf, world), "put inside")
|
||||
var bed: Node3D = null
|
||||
for p in world.all_pieces():
|
||||
if p.item_id == "bed":
|
||||
bed = p
|
||||
check(A.use_bed(player, bed), "E on it: spawn set")
|
||||
G.poll()
|
||||
check(_step_done("bed"), "it ticks")
|
||||
print(" nightfall")
|
||||
check(_at("torch") and world.day_night.hours() >= 18.5, "the sun goes down (%.1f h)" % world.day_night.hours())
|
||||
inv.add("stick", 1)
|
||||
inv.add("fiber", 2)
|
||||
check(A.craft(player, "torch") and _step_done("torch"), "a torch")
|
||||
print(" the wolf")
|
||||
check(_at("wolf") and G.demo_wolf != null, "a wolf comes out of the dark")
|
||||
var w: Node3D = G.demo_wolf
|
||||
var hp0: float = sv.health
|
||||
var closest := INF
|
||||
var t := 0.0
|
||||
while is_instance_valid(w) and not w.is_queued_for_deletion() and t < 60.0:
|
||||
w.set_physics_process(false)
|
||||
w.step(0.1)
|
||||
closest = minf(closest, w.global_position.distance_to(player.global_position))
|
||||
t += 0.1
|
||||
if int(t * 10) % 10 == 0:
|
||||
await physics_frame
|
||||
check(_step_done("wolf"), "it circles, then slinks off (%.0f s)" % t)
|
||||
check(sv.health == hp0 and closest > 6.0, "never closer than %.0f m, and never bites" % closest)
|
||||
print(" sleep")
|
||||
check(_at("sleep") and world.day_night.hours() >= 20.9, "bedtime (%.1f h)" % world.day_night.hours())
|
||||
player.global_position = center + b * Vector3(0.6, 0.05, 0.9)
|
||||
check(A.use_bed(player, bed), "into bed")
|
||||
check(_step_done("sleep"), "slept till morning: it ticks")
|
||||
|
||||
func _finish() -> void:
|
||||
print("finished")
|
||||
var order := []
|
||||
for s in G.steps:
|
||||
order.append(String(s.id))
|
||||
check(ticked == order, "all %d steps ticked, in order" % order.size())
|
||||
check(G.is_finished(), "the tutorial is done")
|
||||
check(_pn.card != null and is_instance_valid(_pn.card), "Old Tam's goodbye card is up")
|
||||
var buttons: Array = _pn.card.find_children("*", "Button", true, false)
|
||||
check(buttons.size() == 2 and buttons[0].text == "Start your own world" and buttons[1].text == "Keep exploring",
|
||||
"with Start your own world and Keep exploring")
|
||||
_pn._on_keep_exploring()
|
||||
check(_pn.card == null and not player.ui_open, "Keep exploring: back to the island")
|
||||
print("skipping")
|
||||
# (With nothing counted yet, so the next step doesn't tick at once.)
|
||||
var idx: int = G.index
|
||||
var saved: Dictionary = G.counters.duplicate()
|
||||
G.counters = {}
|
||||
G.index = 3
|
||||
G.skip()
|
||||
check(G.index == 4 and String(G.current().id) == "tree", "(Skip step moves on: the pause menu's button)")
|
||||
G.index = idx
|
||||
G.counters = saved
|
||||
Reference in New Issue
Block a user