246 lines
8.9 KiB
GDScript
246 lines
8.9 KiB
GDScript
extends SceneTree
|
|
## P3 thieves (docs/PROGRESSION_DESIGN.md §4): crows by day and foxes at
|
|
## night come for food left on the ground. A crow pecks a stack away and
|
|
## takes off if you come near; a fox runs off with the lot, and drops it if
|
|
## you kill it. Food you're standing by, and food in a chest, is safe.
|
|
## Run: godot --headless --path . --script res://tests/test_thieves.gd
|
|
|
|
var _fails := 0
|
|
var _finished := false
|
|
var A: Node
|
|
var S: Node
|
|
var DB: Node
|
|
var _toasts: Array[String] = []
|
|
var world: Node3D
|
|
var player: CharacterBody3D
|
|
var th: Node
|
|
var site: Vector3
|
|
|
|
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_worlds"
|
|
for w in S.list_worlds():
|
|
S.delete_world(w.id)
|
|
S.current_id = S.create_world("Thieves", 1337)
|
|
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
|
|
world.bears.enabled = false
|
|
if world.bears.bear != null:
|
|
world.bears.bear.queue_free() # (it would eat the test food)
|
|
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")
|
|
th = world.thieves
|
|
th.enabled = false
|
|
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 _set_time(hours: float) -> void:
|
|
world.day_night.day = 3
|
|
world.day_night.time = hours / 24.0
|
|
world.day_night.apply(true)
|
|
|
|
## Runs the thieves for up to `secs`, stopping once `until` is true.
|
|
func _sim(secs: float, until: Callable = Callable()) -> float:
|
|
var t := 0.0
|
|
while t < secs:
|
|
th.step(0.1)
|
|
for c in th.get_children():
|
|
if is_instance_valid(c) and not c.is_queued_for_deletion():
|
|
c.set_physics_process(false)
|
|
c.step(0.1)
|
|
t += 0.1
|
|
if until.is_valid() and until.call():
|
|
break
|
|
if int(t * 10) % 10 == 0:
|
|
await physics_frame
|
|
return t
|
|
|
|
func _ground(p: Vector3) -> Vector3:
|
|
return Vector3(p.x, world.terrain.sample_height(p.x, p.z), p.z)
|
|
|
|
func _drop(id: String, n: int, at: Vector3) -> Node3D:
|
|
return world.drop_item(id, n, Transform3D(Basis(), _ground(at) + Vector3(0, 0.15, 0)))
|
|
|
|
func _toast_has(s: String) -> bool:
|
|
for t in _toasts:
|
|
if t.contains(s):
|
|
return true
|
|
return false
|
|
|
|
func _thief(kind: String) -> Node3D:
|
|
for c in th.alive():
|
|
if c.kind == kind:
|
|
return c
|
|
return null
|
|
|
|
func _clear() -> void:
|
|
for c in th.get_children():
|
|
c.queue_free()
|
|
for d in world.all_dropped():
|
|
d.queue_free()
|
|
await physics_frame
|
|
|
|
func _body() -> void:
|
|
site = world.build.debug_find_site(world._spawn + player.global_basis.z * 30.0, 6.0)
|
|
await _crows()
|
|
await _scared()
|
|
await _safe()
|
|
await _foxes()
|
|
await _caught()
|
|
_net()
|
|
_finished = true
|
|
|
|
func _crows() -> void:
|
|
print("crows")
|
|
_set_time(12.0)
|
|
player.global_position = _ground(site + Vector3(25, 0, 0))
|
|
var berries := _drop("berries", 4, site)
|
|
await _sim(15.0)
|
|
check(th.alive().is_empty(), "food just put down draws nothing yet")
|
|
await _sim(10.0, func(): return not th.alive().is_empty())
|
|
var crow := _thief("crow")
|
|
check(crow != null, "left alone 20 s by day, it draws a crow")
|
|
if crow == null:
|
|
return
|
|
check(crow.global_position.distance_to(berries.global_position) > 30.0 and crow.global_position.y > world.terrain.sample_height(crow.global_position.x, crow.global_position.z) + 4.0,
|
|
"flying in from %.0f m away" % crow.global_position.distance_to(berries.global_position))
|
|
_toasts.clear()
|
|
var t: float = await _sim(20.0, func(): return crow.state == "peck")
|
|
check(crow.state == "peck" and crow.global_position.distance_to(berries.global_position) < 0.6, "it lands on the berries (%.0f s)" % t)
|
|
check(_toast_has("crow is pecking"), "and you hear it (%s)" % [_toasts])
|
|
await _sim(6.5)
|
|
check(int(berries.amount) == 2, "it eats them one every 3 s (%d left)" % int(berries.amount))
|
|
await _sim(8.0, func(): return berries.taken)
|
|
check(berries.taken, "until they're gone")
|
|
check(crow.state == "fly_off", "then it flies off")
|
|
await _sim(25.0, func(): return not is_instance_valid(crow) or crow.is_queued_for_deletion())
|
|
check(not is_instance_valid(crow) or crow.is_queued_for_deletion(), "and is gone")
|
|
await _clear()
|
|
|
|
func _scared() -> void:
|
|
print("scaring it off")
|
|
var meat := _drop("cooked_meat", 3, site)
|
|
await _sim(30.0, func(): return _thief("crow") != null and _thief("crow").state == "peck")
|
|
var crow := _thief("crow")
|
|
check(crow != null and crow.state == "peck", "a crow lands on some cooked meat")
|
|
if crow == null:
|
|
return
|
|
player.global_position = _ground(site + Vector3(5, 0, 0))
|
|
await _sim(0.5)
|
|
check(crow.state == "fly_off" and int(meat.amount) >= 2 and not meat.taken, "walk over and it takes off, leaving the meat (%d)" % int(meat.amount))
|
|
print("shooting it")
|
|
crow.take_hit(10.0, player.global_position)
|
|
check(crow.is_dead(), "an arrow brings it down")
|
|
check(absf(crow.global_position.y - world.terrain.sample_height(crow.global_position.x, crow.global_position.z)) < 0.05, "it falls to the ground")
|
|
var got: Dictionary = crow.butcher_drops()
|
|
check(int(got.get("feather", 0)) >= 1, "feathers (%s)" % [got])
|
|
await _clear()
|
|
|
|
func _safe() -> void:
|
|
print("food you're watching")
|
|
var by_you := _drop("berries", 5, site)
|
|
player.global_position = _ground(site + Vector3(6, 0, 0))
|
|
await _sim(60.0)
|
|
check(th.alive().is_empty() and int(by_you.amount) == 5, "a minute with you beside it: nothing comes")
|
|
print("food in a chest")
|
|
var chest: Node3D = world.spawn_piece(DB.get_item("chest"), Transform3D(Basis(), _ground(site + Vector3(0, 0, 4))))
|
|
chest.inventory.add("raw_meat", 4)
|
|
by_you.queue_free()
|
|
player.global_position = _ground(site + Vector3(25, 0, 0))
|
|
await _sim(60.0)
|
|
check(th.alive().is_empty() and chest.inventory.count("raw_meat") == 4, "a closed chest keeps it safe")
|
|
world.remove_piece(chest)
|
|
await _clear()
|
|
|
|
func _foxes() -> void:
|
|
print("foxes")
|
|
_set_time(21.0)
|
|
var meat := _drop("raw_meat", 3, site)
|
|
await _sim(25.0, func(): return not th.alive().is_empty())
|
|
var fox := _thief("fox")
|
|
check(fox != null and _thief("crow") == null, "at night it's a fox that comes")
|
|
if fox == null:
|
|
return
|
|
_toasts.clear()
|
|
var t: float = await _sim(30.0, func(): return not fox.carry.is_empty())
|
|
check(meat.taken and int(fox.carry.get("count", 0)) == 3, "it slinks up and takes all 3 meat in one go (%.0f s)" % t)
|
|
check(_toast_has("fox has run off"), "you hear it (%s)" % [_toasts])
|
|
await _sim(40.0, func(): return not is_instance_valid(fox) or fox.is_queued_for_deletion())
|
|
check(not is_instance_valid(fox) or fox.is_queued_for_deletion(), "it gets away: the meat's gone")
|
|
var found: Array = world.all_dropped().filter(func(d): return d.item_id == "raw_meat")
|
|
check(found.is_empty(), "(none left anywhere)")
|
|
print("too close")
|
|
var fish := _drop("trout", 1, site)
|
|
await _sim(25.0, func(): return _thief("fox") != null)
|
|
fox = _thief("fox")
|
|
if fox != null:
|
|
await _sim(4.0)
|
|
player.global_position = fox.global_position + Vector3(6, 0, 0)
|
|
await _sim(0.5)
|
|
check(fox.state == "flee" and fox.carry.is_empty() and not fish.taken, "walk at it while it's creeping in and it bolts empty-handed")
|
|
await _clear()
|
|
|
|
func _caught() -> void:
|
|
print("catching one")
|
|
player.global_position = _ground(site + Vector3(25, 0, 0))
|
|
var meat := _drop("cooked_meat", 2, site)
|
|
await _sim(25.0, func(): return _thief("fox") != null)
|
|
var fox := _thief("fox")
|
|
await _sim(30.0, func(): return fox != null and not fox.carry.is_empty())
|
|
check(fox != null and not fox.carry.is_empty(), "a fox runs off with your cooked meat")
|
|
if fox == null:
|
|
return
|
|
await _sim(1.0)
|
|
var at: Vector3 = fox.global_position
|
|
fox.take_hit(40.0, player.global_position)
|
|
check(fox.is_dead(), "you shoot it")
|
|
var back: Array = world.all_dropped().filter(func(d): return d.item_id == "cooked_meat" and d.global_position.distance_to(at) < 1.5)
|
|
check(back.size() == 1 and int(back[0].amount) == 2, "and it drops the meat where it fell")
|
|
check(int(fox.butcher_drops().get("hide", 0)) == 1, "(and has a hide)")
|
|
await _clear()
|
|
|
|
func _net() -> void:
|
|
print("online")
|
|
var CN: Node = root.get_node("CreatureNet")
|
|
for k in [["crow", "peck"], ["fox", "carry"]]:
|
|
var c: Node3D = (th.CrowScript if k[0] == "crow" else th.FoxScript).new()
|
|
c.setup(k[0], world, site, 5)
|
|
c.thieves = th
|
|
c.set_physics_process(false)
|
|
th.add_child(c)
|
|
c.set_meta("cid", 42)
|
|
c.state = k[1]
|
|
var buf := PackedByteArray()
|
|
buf.resize(CN.ENTRY)
|
|
CN._encode(buf, 0, c)
|
|
var e: Array = CN._decode(buf, 0)
|
|
check(e[1] == k[0] and e[4] == k[1], "a %s goes over the network (%s, %s)" % [k[0], e[1], e[4]])
|
|
c.free()
|