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:
@@ -4,7 +4,26 @@
|
|||||||
"about": "the hermit who's lived on this island longer than he'll admit"
|
"about": "the hermit who's lived on this island longer than he'll admit"
|
||||||
},
|
},
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{"id": "tools", "title": "First tools"}
|
{
|
||||||
|
"id": "tools",
|
||||||
|
"title": "First tools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "food",
|
||||||
|
"title": "Fire and food"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hunt",
|
||||||
|
"title": "Hunting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fish",
|
||||||
|
"title": "Fishing"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "night",
|
||||||
|
"title": "Shelter and night"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
@@ -12,47 +31,320 @@
|
|||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Look around",
|
"title": "Look around",
|
||||||
"text": "Awake at last! Washed up on my island, have you. Stretch your legs: WASD to walk, the mouse to look. Shift if you're in a hurry.",
|
"text": "Awake at last! Washed up on my island, have you. Stretch your legs: WASD to walk, the mouse to look. Shift if you're in a hurry.",
|
||||||
"goal": {"walk": 10}
|
"goal": {
|
||||||
|
"walk": 10
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "sticks",
|
"id": "sticks",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Gather sticks and stones",
|
"title": "Gather sticks and stones",
|
||||||
"text": "Empty hands are no use here. Look at a stick or a stone on the ground and press E. Three of each, mind.",
|
"text": "Empty hands are no use here. Look at a stick or a stone on the ground and press E. Three of each, mind.",
|
||||||
"marker": {"nearest": "pickup:stick"},
|
"marker": {
|
||||||
"goal": {"all": [{"gained": "stick", "count": 3}, {"gained": "stone", "count": 3}]}
|
"nearest": "pickup:stick"
|
||||||
|
},
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"gained": "stick",
|
||||||
|
"count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"gained": "stone",
|
||||||
|
"count": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "flax",
|
"id": "flax",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Pick wild flax",
|
"title": "Pick wild flax",
|
||||||
"text": "See the pale, tall plants? Wild flax. Pick some. The fibre makes torches, bows and a decent bed.",
|
"text": "See the pale, tall plants? Wild flax. Pick some. The fibre makes torches, bows and a decent bed.",
|
||||||
"marker": {"nearest": "pickup:fiber"},
|
"marker": {
|
||||||
"goal": {"gained": "fiber", "count": 2}
|
"nearest": "pickup:fiber"
|
||||||
|
},
|
||||||
|
"goal": {
|
||||||
|
"gained": "fiber",
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "axe",
|
"id": "axe",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Craft a stone axe",
|
"title": "Craft a stone axe",
|
||||||
"text": "Now an axe. Two sticks and two flint: flint lies about, more by the water. Tab opens your pack, and the recipe's in there.",
|
"text": "Now an axe. Two sticks and two flint: flint lies about, more by the water. Tab opens your pack, and the recipe's in there.",
|
||||||
"marker": {"nearest": "pickup:flint", "unless_have": {"flint": 2}},
|
"marker": {
|
||||||
"goal": {"crafted": "stone_axe"}
|
"nearest": "pickup:flint",
|
||||||
|
"unless_have": {
|
||||||
|
"flint": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"goal": {
|
||||||
|
"crafted": "stone_axe"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "tree",
|
"id": "tree",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Chop down a tree",
|
"title": "Chop down a tree",
|
||||||
"text": "Axe in hand (the number keys), hold the left button at a trunk and keep at it. A tree gives wood, and sticks when it falls.",
|
"text": "Axe in hand (the number keys), hold the left button at a trunk and keep at it. A tree gives wood, and sticks when it falls.",
|
||||||
"marker": {"nearest": "tree"},
|
"marker": {
|
||||||
"goal": {"felled": "tree"}
|
"nearest": "tree"
|
||||||
|
},
|
||||||
|
"goal": {
|
||||||
|
"felled": "tree"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pickaxe",
|
"id": "pickaxe",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Craft a pickaxe and break a boulder",
|
"title": "Craft a pickaxe and break a boulder",
|
||||||
"text": "Rocks are next. A pickaxe takes two sticks, three stone and a flint. Then go and break a boulder for stone and flint of your own.",
|
"text": "Rocks are next. A pickaxe takes two sticks, three stone and a flint. Then go and break a boulder for stone and flint of your own.",
|
||||||
"marker": {"nearest": "rock"},
|
"marker": {
|
||||||
"goal": {"all": [{"crafted": "stone_pickaxe"}, {"felled": "rock"}]}
|
"nearest": "rock"
|
||||||
|
},
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"crafted": "stone_pickaxe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"felled": "rock"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "campfire",
|
||||||
|
"chapter": "food",
|
||||||
|
"title": "Build a campfire",
|
||||||
|
"text": "Five stone and four sticks make a campfire. Pick it in your pack, then place it with the left button (R turns it). Fire gives light, warmth and somewhere to cook. Feed it wood with E.",
|
||||||
|
"goal": {
|
||||||
|
"placed": "campfire"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "berries",
|
||||||
|
"chapter": "food",
|
||||||
|
"title": "Pick berries and eat some",
|
||||||
|
"text": "Those red berries on the bushes are safe. Pick some with E, then hold them and press the right button to eat.",
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"gained": "berries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ate": "berries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"nearest": "bush"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mushroom",
|
||||||
|
"chapter": "food",
|
||||||
|
"title": "Roast a mushroom",
|
||||||
|
"text": "Mushrooms grow under the trees. Raw ones are all right; roasted at the fire, much better. Find one and roast it (stand by the fire and open your pack).",
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"gained": "mushroom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"crafted": "roasted_mushroom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"nearest": "pickup:mushroom",
|
||||||
|
"unless_have": {
|
||||||
|
"mushroom": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diet",
|
||||||
|
"chapter": "food",
|
||||||
|
"title": "Mind your diet",
|
||||||
|
"text": "See the three Diet bars, bottom left? Meat, Fish, Greens. Each food fills one. Let one run low and you stop healing; let two, and you'll waste away on a full belly. Keep all three up and you heal faster. Press Enter when you've got that.",
|
||||||
|
"goal": {
|
||||||
|
"continue": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "workbench",
|
||||||
|
"chapter": "hunt",
|
||||||
|
"title": "Build a workbench",
|
||||||
|
"text": "Better tools want a workbench nearby. Ten wood and two flint. Chop a few more trees if you're short.",
|
||||||
|
"goal": {
|
||||||
|
"placed": "workbench"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "weapon",
|
||||||
|
"chapter": "hunt",
|
||||||
|
"title": "Make a weapon",
|
||||||
|
"text": "You'll not catch your supper with your bare hands. A club is four wood. A spear, at the bench, reaches further.",
|
||||||
|
"goal": {
|
||||||
|
"any": [
|
||||||
|
{
|
||||||
|
"crafted": "club"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"crafted": "spear"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rabbit",
|
||||||
|
"chapter": "hunt",
|
||||||
|
"title": "Hunt a rabbit",
|
||||||
|
"text": "There, rabbits! They bolt if you run at them. Walk up quiet, weapon in hand, and strike.",
|
||||||
|
"goal": {
|
||||||
|
"killed": "rabbit"
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"nearest": "creature:rabbit"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"spawn": {
|
||||||
|
"kind": "rabbit",
|
||||||
|
"count": 3,
|
||||||
|
"distance": 14.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cook",
|
||||||
|
"chapter": "hunt",
|
||||||
|
"title": "Butcher it and cook the meat",
|
||||||
|
"text": "E on the carcass to butcher it. Raw meat turns your stomach; cooked at the fire it heals you and keeps you Well fed.",
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"butchered": "rabbit"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"crafted": "cooked_meat"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"nearest": "carcass"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "animals",
|
||||||
|
"chapter": "hunt",
|
||||||
|
"title": "Know your animals",
|
||||||
|
"text": "Deer are quick: a bow's your friend there. Boars are calm until you hit one, then they come for you. Wolves you'll meet soon enough. Press Enter.",
|
||||||
|
"goal": {
|
||||||
|
"continue": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rod",
|
||||||
|
"chapter": "fish",
|
||||||
|
"title": "Make a fishing rod",
|
||||||
|
"text": "Two sticks, three fibre and a flint, at the workbench.",
|
||||||
|
"goal": {
|
||||||
|
"crafted": "fishing_rod"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fish",
|
||||||
|
"chapter": "fish",
|
||||||
|
"title": "Catch a fish and grill it",
|
||||||
|
"text": "Down to the lake. Rod in hand, the left button casts. When the float dips, click quick, before it gets away. Grill what you catch at the fire.",
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"caught": "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"any": [
|
||||||
|
{
|
||||||
|
"crafted": "grilled_perch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"crafted": "grilled_trout"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"nearest": "lake"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "hut",
|
||||||
|
"chapter": "night",
|
||||||
|
"title": "Build a little hut",
|
||||||
|
"text": "Nights get cold, and things prowl. At the bench: floors, walls, a door and a roof. Walls stand on floors, roofs on walls. Done when you can stand inside out of the weather.",
|
||||||
|
"goal": {
|
||||||
|
"shelter": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bed",
|
||||||
|
"chapter": "night",
|
||||||
|
"title": "Make a bed and sleep spot",
|
||||||
|
"text": "Eight wood and six fibre make a bed. Put it inside, then E on it: that's where you'll wake if anything happens to you.",
|
||||||
|
"goal": {
|
||||||
|
"all": [
|
||||||
|
{
|
||||||
|
"placed": "bed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bed_spawn": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "torch",
|
||||||
|
"chapter": "night",
|
||||||
|
"title": "Night is coming: make a torch",
|
||||||
|
"text": "There, the sun's going down. It gets dark and cold. A torch (a stick and two fibre) lights the way and keeps you a little warm.",
|
||||||
|
"goal": {
|
||||||
|
"crafted": "torch"
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"time": 18.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wolf",
|
||||||
|
"chapter": "night",
|
||||||
|
"title": "Wolves",
|
||||||
|
"text": "Hush. See it, at the edge of the light? Wolves. From the fourth night they hunt. They won't come close to a lit fire, a torch makes them think twice, and they can't bite through walls. Just watch.",
|
||||||
|
"goal": {
|
||||||
|
"demo_wolf": true
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"demo_wolf": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "sleep",
|
||||||
|
"chapter": "night",
|
||||||
|
"title": "Sleep till morning",
|
||||||
|
"text": "Off to bed with you. You can only sleep at night, under a roof with walls round you, and not with wolves about.",
|
||||||
|
"goal": {
|
||||||
|
"slept": true
|
||||||
|
},
|
||||||
|
"setup": {
|
||||||
|
"time": 21.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"finish": {
|
||||||
|
"title": "You'll do",
|
||||||
|
"text": "That's the island, near enough: gather, craft, eat a bit of everything, keep warm and dry, and be indoors or by a fire when the wolves are about. E picks up and uses, Tab is your pack, X takes a thing down, R turns what you're placing, the right button eats or wears. Off you go, then."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,33 @@
|
|||||||
# Tutorial world design
|
# Tutorial world design
|
||||||
|
|
||||||
Status: **T1 built** (the Guide, Actions' events, chapter 1 in
|
Status: **T1 and T2 built**: the whole tutorial, on the Valley island
|
||||||
data/tutorial.json, the guide panel and marker); T2-T4 are proposals.
|
(seed 1337), with Old Tam the hermit. T3 (its own island) and T4 (hints in
|
||||||
|
real worlds) are proposals.
|
||||||
|
|
||||||
|
*As built in T2:*
|
||||||
|
- All five chapters, 22 steps (data/tutorial.json), in Old Tam's voice:
|
||||||
|
first tools; fire and food (campfire, berries, a roasted mushroom, the
|
||||||
|
diet bars); hunting (workbench, a weapon, a rabbit, butcher and cook,
|
||||||
|
a tip about the other animals); fishing (rod, a catch, grilled); shelter
|
||||||
|
and night (a hut you can stand inside, a bed and spawn, nightfall and a
|
||||||
|
torch, the demo wolf, sleeping).
|
||||||
|
- More goals: `any` (club or spear, perch or trout), `shelter`,
|
||||||
|
`bed_spawn`, `demo_wolf`, `continue` (Enter on the reading steps).
|
||||||
|
- Step set-ups: the clock jumps to dusk (18:36) and to bedtime (21:00);
|
||||||
|
three rabbits appear 14 m off for the hunt; the demo wolf
|
||||||
|
(scripts/creatures/tutorial_wolf.gd) circles at 11 m for 18 s and slinks
|
||||||
|
off, never biting.
|
||||||
|
- Markers for berry bushes, rabbits, the carcass, the lake shore.
|
||||||
|
- The tutorial island is calm: no wolf pack, no bear, no thieves.
|
||||||
|
- Title screen: **Tutorial** above New World (it says "carry on" once
|
||||||
|
started; with no worlds yet: "New here? Try the tutorial"). Pause menu:
|
||||||
|
Skip this step, Leave the tutorial (saved, carry on later).
|
||||||
|
- The end: Old Tam's goodbye card, **Start your own world** (to the New
|
||||||
|
World page) or **Keep exploring**.
|
||||||
|
- tests/test_tutorial_full.gd: a bot plays all 22 steps through Actions,
|
||||||
|
in order, to the card. (Bulk materials, like a hut's wood, go straight
|
||||||
|
into its bag; every goal is done as a player does it.)
|
||||||
|
- Not yet: a ghost outline for the hut (§2 step 17), sounds for the tick.
|
||||||
|
|
||||||
*As built in T1:*
|
*As built in T1:*
|
||||||
- Actions has signals for what players do: `crafted`, `harvested` (with
|
- Actions has signals for what players do: `crafted`, `harvested` (with
|
||||||
|
|||||||
@@ -14,10 +14,18 @@ extends Node
|
|||||||
## {"felled": "tree" | "rock"} chopped down / broken
|
## {"felled": "tree" | "rock"} chopped down / broken
|
||||||
## {"placed": item_id}, {"ate": item_id or "any"}, {"killed": kind},
|
## {"placed": item_id}, {"ate": item_id or "any"}, {"killed": kind},
|
||||||
## {"butchered": kind}, {"caught": "any"}, {"slept": true}
|
## {"butchered": kind}, {"caught": "any"}, {"slept": true}
|
||||||
## {"continue": true} the player pressed Continue
|
## {"continue": true} the player pressed Enter ("Continue")
|
||||||
|
## {"shelter": true} standing under a roof with walls round you
|
||||||
|
## {"bed_spawn": true} you've set your spawn at a bed
|
||||||
|
## {"demo_wolf": true} the tutorial's wolf has been and gone
|
||||||
## {"all": [goal, goal, ...]} every one of them
|
## {"all": [goal, goal, ...]} every one of them
|
||||||
## Markers: {"nearest": "pickup:<id>" | "tree" | "rock"}, optionally
|
## {"any": [goal, goal, ...]} one of them
|
||||||
|
## Markers: {"nearest": "pickup:<id>" | "tree" | "rock" | "bush" | "lake" |
|
||||||
|
## "creature:<kind>" | "carcass" | "piece:<item id>"}, optionally
|
||||||
## {"unless_have": {id: n}} to hide it once you've got enough.
|
## {"unless_have": {id: n}} to hide it once you've got enough.
|
||||||
|
## Set-up, when a step comes up: {"time": hours} (the clock jumps on),
|
||||||
|
## {"spawn": {"kind", "count", "distance"}} (calm animals round you),
|
||||||
|
## {"demo_wolf": true} (a wolf that circles and leaves: tutorial_wolf.gd).
|
||||||
##
|
##
|
||||||
## Single-player only (the tutorial is your own world). The panel and the
|
## Single-player only (the tutorial is your own world). The panel and the
|
||||||
## in-world marker are ui/guide.gd. Progress is saved with the world.
|
## in-world marker are ui/guide.gd. Progress is saved with the world.
|
||||||
@@ -90,10 +98,12 @@ func _load_data() -> void:
|
|||||||
steps = d.get("steps", [])
|
steps = d.get("steps", [])
|
||||||
chapters = d.get("chapters", [])
|
chapters = d.get("chapters", [])
|
||||||
guide_name = String(d.get("guide", {}).get("name", ""))
|
guide_name = String(d.get("guide", {}).get("name", ""))
|
||||||
|
finish = d.get("finish", {})
|
||||||
|
|
||||||
## Who's talking you through it (data/tutorial.json "guide": Old Tam, the
|
## Who's talking you through it (data/tutorial.json "guide": Old Tam, the
|
||||||
## island's hermit).
|
## island's hermit).
|
||||||
var guide_name := ""
|
var guide_name := ""
|
||||||
|
var finish: Dictionary = {} ## the card at the end: title and text
|
||||||
|
|
||||||
# --- running it -----------------------------------------------------------------------
|
# --- running it -----------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -129,10 +139,44 @@ func _enter() -> void:
|
|||||||
if index >= steps.size():
|
if index >= steps.size():
|
||||||
finished.emit()
|
finished.emit()
|
||||||
return
|
return
|
||||||
|
_setup(steps[index].get("setup", {}))
|
||||||
step_started.emit(index, steps[index])
|
step_started.emit(index, steps[index])
|
||||||
_marker_t = 0.0
|
_marker_t = 0.0
|
||||||
_check() # (done early? it ticks straight away)
|
_check() # (done early? it ticks straight away)
|
||||||
|
|
||||||
|
const DemoWolf = preload("res://scripts/creatures/tutorial_wolf.gd")
|
||||||
|
var demo_wolf: Node3D = null
|
||||||
|
|
||||||
|
## A step's set-up: the clock, some animals, the demo wolf.
|
||||||
|
func _setup(su: Dictionary) -> void:
|
||||||
|
if su.is_empty() or world == null or not is_instance_valid(world):
|
||||||
|
return
|
||||||
|
if su.has("time"):
|
||||||
|
var want: float = float(su.time) / 24.0
|
||||||
|
if world.day_night.time < want:
|
||||||
|
world.day_night.time = want
|
||||||
|
world.day_night.apply(true)
|
||||||
|
if su.has("spawn"):
|
||||||
|
var sp: Dictionary = su.spawn
|
||||||
|
var p: Vector3 = world.player.global_position
|
||||||
|
var rng := RandomNumberGenerator.new()
|
||||||
|
rng.seed = int(p.x * 13.0 + p.z * 7.0)
|
||||||
|
for i in range(int(sp.get("count", 1))):
|
||||||
|
for _try in range(12):
|
||||||
|
var a := rng.randf() * TAU
|
||||||
|
var at := p + Vector3(cos(a), 0, sin(a)) * float(sp.get("distance", 12.0))
|
||||||
|
if world.terrain.inside_boundary(at.x, at.z, 4.0) and world.terrain.sample_height(at.x, at.z) > world.terrain.water_level + 0.3:
|
||||||
|
world.creatures.spawn(String(sp.kind), at)
|
||||||
|
break
|
||||||
|
if su.get("demo_wolf", false):
|
||||||
|
if demo_wolf != null and is_instance_valid(demo_wolf):
|
||||||
|
demo_wolf.queue_free()
|
||||||
|
demo_wolf = DemoWolf.new()
|
||||||
|
demo_wolf.setup("wolf", world, world.player.global_position + Vector3(20, 0, 0), 4242)
|
||||||
|
world.add_child(demo_wolf)
|
||||||
|
demo_wolf.begin(world.player)
|
||||||
|
demo_wolf.gone.connect(func(): _count("demo_wolf:true", 1))
|
||||||
|
|
||||||
## The player read a "Continue" step.
|
## The player read a "Continue" step.
|
||||||
func continue_step() -> void:
|
func continue_step() -> void:
|
||||||
_count("continue:true", 1)
|
_count("continue:true", 1)
|
||||||
@@ -168,15 +212,24 @@ func _advance() -> void:
|
|||||||
|
|
||||||
## [[label, have, need], ...] for each part of `goal`.
|
## [[label, have, need], ...] for each part of `goal`.
|
||||||
func parts(goal: Dictionary) -> Array:
|
func parts(goal: Dictionary) -> Array:
|
||||||
|
if goal.has("any"):
|
||||||
|
var names := []
|
||||||
|
var met := false
|
||||||
|
for g in goal.any:
|
||||||
|
for pp in parts(g):
|
||||||
|
names.append(String(pp[0]))
|
||||||
|
met = met or _met(g)
|
||||||
|
return [[" or ".join(PackedStringArray(names)), 1.0 if met else 0.0, 1.0]]
|
||||||
if goal.has("all"):
|
if goal.has("all"):
|
||||||
var out := []
|
var out := []
|
||||||
for g in goal.all:
|
for g in goal.all:
|
||||||
out.append_array(parts(g))
|
out.append_array(parts(g))
|
||||||
return out
|
return out
|
||||||
var need := float(goal.get("count", 1))
|
var need := float(goal.get("count", 1))
|
||||||
for k in ["gained", "crafted", "felled", "placed", "ate", "killed", "butchered", "caught", "slept", "continue"]:
|
for k in ["gained", "crafted", "felled", "placed", "ate", "killed", "butchered", "caught", "slept", "continue",
|
||||||
|
"shelter", "bed_spawn", "demo_wolf"]:
|
||||||
if goal.has(k):
|
if goal.has(k):
|
||||||
var key := "%s:%s" % [k, String(goal[k]).to_lower() if goal[k] is bool else String(goal[k])]
|
var key := "%s:%s" % [k, str(goal[k]).to_lower() if goal[k] is bool else str(goal[k])] # (true -> "true")
|
||||||
return [[_label(k, goal), float(counters.get(key, 0.0)), need]]
|
return [[_label(k, goal), float(counters.get(key, 0.0)), need]]
|
||||||
if goal.has("walk"):
|
if goal.has("walk"):
|
||||||
return [["Walk", floorf(float(counters.get("walk", 0.0))), float(goal.walk)]]
|
return [["Walk", floorf(float(counters.get("walk", 0.0))), float(goal.walk)]]
|
||||||
@@ -191,7 +244,13 @@ func _label(kind: String, goal: Dictionary) -> String:
|
|||||||
var r: Dictionary = ItemDB.recipes.get(String(goal.crafted), {})
|
var r: Dictionary = ItemDB.recipes.get(String(goal.crafted), {})
|
||||||
return ItemDB.item_name(String(r.get("output", goal.crafted)))
|
return ItemDB.item_name(String(r.get("output", goal.crafted)))
|
||||||
"felled": return "Tree" if goal.felled == "tree" else "Boulder"
|
"felled": return "Tree" if goal.felled == "tree" else "Boulder"
|
||||||
"continue": return ""
|
"continue", "shelter", "bed_spawn", "demo_wolf": return ""
|
||||||
|
"placed": return ItemDB.item_name(String(goal.placed)) + " placed"
|
||||||
|
"ate": return "Eaten"
|
||||||
|
"killed": return String(goal.killed).capitalize()
|
||||||
|
"butchered": return "Butchered"
|
||||||
|
"caught": return "A fish"
|
||||||
|
"slept": return "Slept"
|
||||||
return kind.capitalize()
|
return kind.capitalize()
|
||||||
|
|
||||||
static func _plural(name: String, n: int) -> String:
|
static func _plural(name: String, n: int) -> String:
|
||||||
@@ -233,6 +292,17 @@ func _process(delta: float) -> void:
|
|||||||
if _marker_t <= 0.0:
|
if _marker_t <= 0.0:
|
||||||
_marker_t = 0.5
|
_marker_t = 0.5
|
||||||
_marker = find_marker()
|
_marker = find_marker()
|
||||||
|
poll()
|
||||||
|
|
||||||
|
## Goals that are states, not events: checked twice a second.
|
||||||
|
func poll() -> void:
|
||||||
|
if not active or world == null or not is_instance_valid(world):
|
||||||
|
return
|
||||||
|
var pl: Node = world.player
|
||||||
|
if pl.survival.sheltered and float(counters.get("shelter:true", 0.0)) < 1.0:
|
||||||
|
_count("shelter:true", 1)
|
||||||
|
if world.has_bed_spawn and float(counters.get("bed_spawn:true", 0.0)) < 1.0:
|
||||||
|
_count("bed_spawn:true", 1)
|
||||||
|
|
||||||
## Called by tests (no frames): moves the walk counter on by `metres`.
|
## Called by tests (no frames): moves the walk counter on by `metres`.
|
||||||
func walked(metres: float) -> void:
|
func walked(metres: float) -> void:
|
||||||
@@ -265,6 +335,31 @@ func find_marker() -> Vector3:
|
|||||||
for n in world.props.resource_nodes.values():
|
for n in world.props.resource_nodes.values():
|
||||||
if is_instance_valid(n) and n.kind == what and not n.depleted:
|
if is_instance_valid(n) and n.kind == what and not n.depleted:
|
||||||
cands.append(n)
|
cands.append(n)
|
||||||
|
elif what == "bush":
|
||||||
|
for b in world.props.forage.values():
|
||||||
|
if is_instance_valid(b) and b.ripe():
|
||||||
|
cands.append(b)
|
||||||
|
elif what.begins_with("creature:"):
|
||||||
|
for c in world.creatures.alive(what.substr(9)):
|
||||||
|
cands.append(c)
|
||||||
|
elif what == "carcass":
|
||||||
|
for c in world.get_tree().get_nodes_in_group("carcass"):
|
||||||
|
cands.append(c)
|
||||||
|
elif what.begins_with("piece:"):
|
||||||
|
for pc in world.all_pieces():
|
||||||
|
if pc.item_id == what.substr(6):
|
||||||
|
cands.append(pc)
|
||||||
|
elif what == "lake":
|
||||||
|
# The lake's shore, on your side of it.
|
||||||
|
var c: Vector2 = world.terrain.lake_center
|
||||||
|
var dir := Vector2(from.x - c.x, from.z - c.y)
|
||||||
|
dir = dir.normalized() if dir.length() > 0.1 else Vector2(1, 0)
|
||||||
|
var r: float = world.terrain.lake_radius * 0.6
|
||||||
|
while r < world.terrain.lake_radius * 2.0 and world.terrain.sample_height(c.x + dir.x * r, c.y + dir.y * r) < world.terrain.water_level + 0.3:
|
||||||
|
r += 0.5
|
||||||
|
var sx: float = c.x + dir.x * r
|
||||||
|
var sz: float = c.y + dir.y * r
|
||||||
|
return Vector3(sx, world.terrain.sample_height(sx, sz), sz)
|
||||||
for c in cands:
|
for c in cands:
|
||||||
var d: float = (c as Node3D).global_position.distance_to(from)
|
var d: float = (c as Node3D).global_position.distance_to(from)
|
||||||
if d < best_d:
|
if d < best_d:
|
||||||
|
|||||||
79
scripts/creatures/tutorial_wolf.gd
Normal file
79
scripts/creatures/tutorial_wolf.gd
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
extends "res://scripts/creatures/creature.gd"
|
||||||
|
## The tutorial's wolf (docs/TUTORIAL_DESIGN.md §2, step "Wolves"): it comes
|
||||||
|
## out of the dark, circles at the edge of the light for a while, and slinks
|
||||||
|
## off. It never attacks. When it's gone, `gone` fires and the step ticks.
|
||||||
|
|
||||||
|
signal gone
|
||||||
|
|
||||||
|
const RING := 11.0
|
||||||
|
const CIRCLE_SECONDS := 18.0
|
||||||
|
const LEAVE_AFTER := 40.0 ## this far from you it's gone
|
||||||
|
|
||||||
|
var target: Node3D
|
||||||
|
var _a := 0.0
|
||||||
|
var _goal_d := 0.0
|
||||||
|
var _done := false
|
||||||
|
|
||||||
|
func begin(t: Node3D) -> void:
|
||||||
|
target = t
|
||||||
|
var away: Vector3 = global_position - t.global_position
|
||||||
|
_a = atan2(away.z, away.x)
|
||||||
|
state = "stalk"
|
||||||
|
_state_t = CIRCLE_SECONDS
|
||||||
|
|
||||||
|
func display_name() -> String:
|
||||||
|
return "Wolf"
|
||||||
|
|
||||||
|
func interact_prompt(_p: Node) -> String:
|
||||||
|
return "Wolf (watching you from the dark)"
|
||||||
|
|
||||||
|
func _pick_target() -> Node3D:
|
||||||
|
return target
|
||||||
|
|
||||||
|
func _think(dt: float) -> void:
|
||||||
|
if target == null or not is_instance_valid(target):
|
||||||
|
_leave()
|
||||||
|
return
|
||||||
|
_state_t -= dt
|
||||||
|
var tp: Vector3 = target.global_position
|
||||||
|
match state:
|
||||||
|
"stalk":
|
||||||
|
_a += dt * 0.35
|
||||||
|
_go_to(tp + Vector3(cos(_a), 0, sin(_a)) * RING)
|
||||||
|
if _state_t <= 0.0:
|
||||||
|
state = "flee"
|
||||||
|
_state_t = 15.0
|
||||||
|
"flee":
|
||||||
|
var away := global_position - tp
|
||||||
|
away.y = 0.0
|
||||||
|
steer(atan2(-away.x, -away.z))
|
||||||
|
_goal_d = 99.0
|
||||||
|
if global_position.distance_to(tp) > LEAVE_AFTER or _state_t <= 0.0:
|
||||||
|
_leave()
|
||||||
|
_:
|
||||||
|
state = "stalk"
|
||||||
|
|
||||||
|
func _go_to(p: Vector3) -> void:
|
||||||
|
var to := p - global_position
|
||||||
|
to.y = 0.0
|
||||||
|
if to.length() > 0.3:
|
||||||
|
steer(atan2(-to.x, -to.z))
|
||||||
|
_goal_d = to.length()
|
||||||
|
|
||||||
|
func _desired_speed() -> float:
|
||||||
|
match state:
|
||||||
|
"stalk": return 0.0 if _goal_d < 1.0 else (3.0 if _goal_d < 6.0 else 6.5)
|
||||||
|
"flee": return 7.5
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
func _leave() -> void:
|
||||||
|
if _done:
|
||||||
|
return
|
||||||
|
_done = true
|
||||||
|
gone.emit()
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
## (A swipe makes it run.)
|
||||||
|
func _react(_from: Vector3) -> void:
|
||||||
|
state = "flee"
|
||||||
|
_state_t = 10.0
|
||||||
@@ -61,9 +61,14 @@ func setup(p: Node3D) -> void:
|
|||||||
_redraw()
|
_redraw()
|
||||||
|
|
||||||
func _unhandled_input(event: InputEvent) -> void:
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
if Guide.active and event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_H:
|
if not Guide.active or not (event is InputEventKey) or not event.pressed or event.echo:
|
||||||
|
return
|
||||||
|
if event.keycode == KEY_H:
|
||||||
_hidden = not _hidden
|
_hidden = not _hidden
|
||||||
_redraw()
|
_redraw()
|
||||||
|
elif event.keycode in [KEY_ENTER, KEY_KP_ENTER] and Guide.current().get("goal", {}).has("continue"):
|
||||||
|
Guide.continue_step()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
func _redraw() -> void:
|
func _redraw() -> void:
|
||||||
var s: Dictionary = Guide.current()
|
var s: Dictionary = Guide.current()
|
||||||
@@ -84,13 +89,62 @@ func _on_done(_i: int, s: Dictionary) -> void:
|
|||||||
tw.tween_interval(1.4)
|
tw.tween_interval(1.4)
|
||||||
tw.tween_property(_tick, "modulate:a", 0.0, 0.6)
|
tw.tween_property(_tick, "modulate:a", 0.0, 0.6)
|
||||||
|
|
||||||
|
## The end: Old Tam's summary, and Start your own world / Keep exploring.
|
||||||
|
var card: PanelContainer
|
||||||
|
|
||||||
func _on_finished() -> void:
|
func _on_finished() -> void:
|
||||||
_box.visible = false
|
_box.visible = false
|
||||||
_tick.text = "✓ Chapter done. More of the tutorial is on its way."
|
if card != null and is_instance_valid(card):
|
||||||
_tick.modulate.a = 1.0
|
return
|
||||||
var tw := create_tween()
|
card = UITheme.panel()
|
||||||
tw.tween_interval(3.0)
|
card.set_anchors_and_offsets_preset(Control.PRESET_CENTER)
|
||||||
tw.tween_property(_tick, "modulate:a", 0.0, 1.0)
|
card.grow_horizontal = Control.GROW_DIRECTION_BOTH
|
||||||
|
card.grow_vertical = Control.GROW_DIRECTION_BOTH
|
||||||
|
card.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(card)
|
||||||
|
var v := VBoxContainer.new()
|
||||||
|
v.add_theme_constant_override("separation", 10)
|
||||||
|
v.custom_minimum_size = Vector2(520, 0)
|
||||||
|
card.add_child(v)
|
||||||
|
var f: Dictionary = Guide.finish
|
||||||
|
v.add_child(UITheme.label(String(f.get("title", "Tutorial done")), 26))
|
||||||
|
var t := UITheme.label("%s: \"%s\"" % [Guide.guide_name, String(f.get("text", ""))] if not Guide.guide_name.is_empty() else String(f.get("text", "")), 16, UITheme.TEXT_DIM)
|
||||||
|
t.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
t.custom_minimum_size = Vector2(520, 0)
|
||||||
|
v.add_child(t)
|
||||||
|
var h := HBoxContainer.new()
|
||||||
|
h.add_theme_constant_override("separation", 10)
|
||||||
|
v.add_child(h)
|
||||||
|
for b in [["Start your own world", _on_new_world], ["Keep exploring", _on_keep_exploring]]:
|
||||||
|
var btn := Button.new()
|
||||||
|
btn.text = b[0]
|
||||||
|
btn.focus_mode = Control.FOCUS_NONE
|
||||||
|
btn.custom_minimum_size = Vector2(240, 44)
|
||||||
|
btn.add_theme_font_size_override("font_size", 18)
|
||||||
|
btn.pressed.connect(b[1])
|
||||||
|
h.add_child(btn)
|
||||||
|
if player != null and player.get("ui_open") != null:
|
||||||
|
player.ui_open = true
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
func _on_keep_exploring() -> void:
|
||||||
|
if card != null:
|
||||||
|
card.queue_free()
|
||||||
|
card = null
|
||||||
|
if player != null and player.get("ui_open") != null:
|
||||||
|
player.ui_open = false
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||||
|
|
||||||
|
## Saves the tutorial world and goes to the title screen's New World page.
|
||||||
|
func _on_new_world() -> void:
|
||||||
|
if Guide.world != null and is_instance_valid(Guide.world):
|
||||||
|
Guide.world.save_now()
|
||||||
|
get_tree().paused = false
|
||||||
|
Saves.current_id = ""
|
||||||
|
get_tree().auto_accept_quit = true
|
||||||
|
var TS: Script = load("res://scripts/ui/title_screen.gd")
|
||||||
|
TS.start_page = "new"
|
||||||
|
get_tree().change_scene_to_file("res://scenes/title.tscn")
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
_update_marker()
|
_update_marker()
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ func bind(p: CharacterBody3D) -> void:
|
|||||||
sens_row.add_child(sens)
|
sens_row.add_child(sens)
|
||||||
_main.add_child(sens_row)
|
_main.add_child(sens_row)
|
||||||
|
|
||||||
|
# The tutorial: skip a step, or leave it (saved, to carry on later).
|
||||||
|
if Guide.active:
|
||||||
|
_main.add_child(_button("Skip this tutorial step", func(): Guide.skip(); close()))
|
||||||
|
_main.add_child(_button("Leave the tutorial (carry on later)", _on_quit_to_title))
|
||||||
|
|
||||||
var saved: bool = Saves.has_world()
|
var saved: bool = Saves.has_world()
|
||||||
_online = Net.state == "connected"
|
_online = Net.state == "connected"
|
||||||
if _online:
|
if _online:
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ var _pages: Dictionary = {} ## name -> Control
|
|||||||
var _name_edit: LineEdit
|
var _name_edit: LineEdit
|
||||||
var _seed_edit: LineEdit
|
var _seed_edit: LineEdit
|
||||||
var _rot_pick: OptionButton
|
var _rot_pick: OptionButton
|
||||||
|
## The page to open on (the tutorial's "Start your own world" sends you to "new").
|
||||||
|
static var start_page := ""
|
||||||
## [setting, what the New World page calls it]
|
## [setting, what the New World page calls it]
|
||||||
const ROT_CHOICES := [["normal", "Normally (raw meat lasts 2 days)"], ["slow", "Slowly (twice as long)"], ["off", "Never"]]
|
const ROT_CHOICES := [["normal", "Normally (raw meat lasts 2 days)"], ["slow", "Slowly (twice as long)"], ["off", "Never"]]
|
||||||
var _list: VBoxContainer
|
var _list: VBoxContainer
|
||||||
@@ -100,7 +102,8 @@ func _ready() -> void:
|
|||||||
Net.players_changed.connect(func():
|
Net.players_changed.connect(func():
|
||||||
if _pages["lobby"].visible:
|
if _pages["lobby"].visible:
|
||||||
_fill_lobby())
|
_fill_lobby())
|
||||||
_show("main")
|
_show(start_page if start_page in _pages else "main")
|
||||||
|
start_page = ""
|
||||||
# Back from a server (left, kicked, or it went away): say why.
|
# Back from a server (left, kicked, or it went away): say why.
|
||||||
if Net.state == "offline" and not Net.last_reason.is_empty():
|
if Net.state == "offline" and not Net.last_reason.is_empty():
|
||||||
_show("join")
|
_show("join")
|
||||||
@@ -202,9 +205,13 @@ func _main_page() -> Control:
|
|||||||
var v := VBoxContainer.new()
|
var v := VBoxContainer.new()
|
||||||
v.add_theme_constant_override("separation", 10)
|
v.add_theme_constant_override("separation", 10)
|
||||||
var worlds := Saves.list_worlds()
|
var worlds := Saves.list_worlds()
|
||||||
if not worlds.is_empty():
|
if worlds.is_empty():
|
||||||
|
v.add_child(UITheme.label("New here? Try the tutorial (about 20 minutes).", 15, UITheme.TEXT_DIM))
|
||||||
|
else:
|
||||||
var w: Dictionary = worlds[0]
|
var w: Dictionary = worlds[0]
|
||||||
v.add_child(_button("Continue: %s" % w.name, _play.bind(w.id)))
|
v.add_child(_button("Continue: %s" % w.name, _play.bind(w.id)))
|
||||||
|
var tut := tutorial_world()
|
||||||
|
v.add_child(_button("Tutorial" if tut.is_empty() else "Tutorial (carry on)", _play_tutorial))
|
||||||
v.add_child(_button("New World", _show.bind("new")))
|
v.add_child(_button("New World", _show.bind("new")))
|
||||||
var lb := _button("Load World", _show.bind("load"))
|
var lb := _button("Load World", _show.bind("load"))
|
||||||
lb.disabled = worlds.is_empty()
|
lb.disabled = worlds.is_empty()
|
||||||
@@ -673,6 +680,20 @@ func _create() -> void:
|
|||||||
var rot: String = ROT_CHOICES[maxi(_rot_pick.selected, 0)][0]
|
var rot: String = ROT_CHOICES[maxi(_rot_pick.selected, 0)][0]
|
||||||
_play(Saves.create_world(world_name, seed, {"rot": rot}))
|
_play(Saves.create_world(world_name, seed, {"rot": rot}))
|
||||||
|
|
||||||
|
## The tutorial (docs/TUTORIAL_DESIGN.md): its own world on the original
|
||||||
|
## island (seed 1337), made the first time and carried on after that.
|
||||||
|
const TUTORIAL_SEED := 1337
|
||||||
|
|
||||||
|
static func tutorial_world() -> Dictionary:
|
||||||
|
for w in Saves.list_worlds():
|
||||||
|
if bool(w.get("tutorial", false)):
|
||||||
|
return w
|
||||||
|
return {}
|
||||||
|
|
||||||
|
func _play_tutorial() -> void:
|
||||||
|
var w := tutorial_world()
|
||||||
|
_play(String(w.id) if not w.is_empty() else Saves.create_world("Tutorial", TUTORIAL_SEED, {"tutorial": true}))
|
||||||
|
|
||||||
func _play(id: String) -> void:
|
func _play(id: String) -> void:
|
||||||
Saves.current_id = id
|
Saves.current_id = id
|
||||||
get_tree().change_scene_to_file(WORLD_SCENE)
|
get_tree().change_scene_to_file(WORLD_SCENE)
|
||||||
|
|||||||
@@ -416,11 +416,15 @@ func _on_terrain_ready() -> void:
|
|||||||
wolves.name = "Wolves"
|
wolves.name = "Wolves"
|
||||||
add_child(wolves)
|
add_child(wolves)
|
||||||
wolves.setup(self, den)
|
wolves.setup(self, den)
|
||||||
if not "--no-creatures" in OS.get_cmdline_user_args():
|
|
||||||
_setup_bear()
|
|
||||||
# The tutorial (docs/TUTORIAL_DESIGN.md): a world made for it (meta.json
|
# The tutorial (docs/TUTORIAL_DESIGN.md): a world made for it (meta.json
|
||||||
# "tutorial"), or any world with `-- --tutorial` (for now, T1).
|
# "tutorial"), or any world with `-- --tutorial`. Its island is calm: no
|
||||||
if bool(Saves.read_meta(Saves.current_id).get("tutorial", false)) or "--tutorial" in OS.get_cmdline_user_args():
|
# wolf pack (just Old Tam's demo wolf), no bear, nothing stealing your food.
|
||||||
|
var tutorial := bool(Saves.read_meta(Saves.current_id).get("tutorial", false)) or "--tutorial" in OS.get_cmdline_user_args()
|
||||||
|
if tutorial and wolves != null:
|
||||||
|
wolves.enabled = false
|
||||||
|
if not "--no-creatures" in OS.get_cmdline_user_args() and not tutorial:
|
||||||
|
_setup_bear()
|
||||||
|
if tutorial:
|
||||||
Guide.start(self, _saved_tutorial)
|
Guide.start(self, _saved_tutorial)
|
||||||
_maybe_capture()
|
_maybe_capture()
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
|
|||||||
echo "Importing the project first (fresh copy)..."
|
echo "Importing the project first (fresh copy)..."
|
||||||
godot --headless --path . --import >/dev/null 2>&1
|
godot --headless --path . --import >/dev/null 2>&1
|
||||||
fi
|
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=""
|
extra=""
|
||||||
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
|
||||||
# A suite that waits on a world that failed to load would hang forever,
|
# A suite that waits on a world that failed to load would hang forever,
|
||||||
|
|||||||
@@ -127,10 +127,11 @@ func _body() -> void:
|
|||||||
func _starts() -> void:
|
func _starts() -> void:
|
||||||
print("the tutorial starts")
|
print("the tutorial starts")
|
||||||
check(G.active and G.world == world, "a tutorial world starts the guide")
|
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()
|
var pn := _panel()
|
||||||
check(pn != null and pn.visible and pn._box.visible, "the guide panel is up")
|
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])
|
"\"%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.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)")
|
check(G.find_marker() == Vector3.INF, "(no marker for that one)")
|
||||||
@@ -195,7 +196,7 @@ func _chapter_one() -> void:
|
|||||||
_fell(rock)
|
_fell(rock)
|
||||||
check(rock.depleted and ticked.back() == "pickaxe", "break a boulder and it ticks")
|
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(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:
|
func _saving() -> void:
|
||||||
print("saving")
|
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