P3 thieves and barrow: crows and foxes steal food left out; the barrow with its guards, coffin loot and key pieces; blunt club vs bone (protocol 10)
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_den_sites test_fishing_diet test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_host 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_den_sites test_fishing_diet test_iron test_stone test_net_lobby test_net_world test_net_shared test_net_life test_net_rules test_host 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,
|
||||
|
||||
312
tests/test_barrow.gd
Normal file
312
tests/test_barrow.gd
Normal file
@@ -0,0 +1,312 @@
|
||||
extends SceneTree
|
||||
## P3 the barrow (docs/PROGRESSION_DESIGN.md §4): the burial mound in its
|
||||
## stone ring, its passage and chamber, the guards that wake at night (or
|
||||
## after day 10, or when the coffin's opened), clubs cracking bone, the loot
|
||||
## and its key piece, restocking, and saving.
|
||||
## Run: godot --headless --path . --script res://tests/test_barrow.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 bw: Node
|
||||
|
||||
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("Barrow", 1337)
|
||||
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
|
||||
world.bears.enabled = false
|
||||
world.thieves.enabled = false
|
||||
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")
|
||||
bw = world.barrow
|
||||
bw.running = false # (the test steps it)
|
||||
for g in bw.guards:
|
||||
g.set_physics_process(false)
|
||||
|
||||
func _reload() -> void:
|
||||
check(world.save_now(), "saved")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
await _load()
|
||||
|
||||
func _set_time(day: int, hours: float) -> void:
|
||||
world.day_night.day = day
|
||||
world.day_night.time = hours / 24.0
|
||||
world.day_night.apply(true)
|
||||
|
||||
func _sim(secs: float, until: Callable = Callable()) -> float:
|
||||
var t := 0.0
|
||||
while t < secs:
|
||||
bw.step()
|
||||
for g in bw.guards:
|
||||
if is_instance_valid(g):
|
||||
g.set_physics_process(false)
|
||||
g.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 _awake() -> int:
|
||||
return bw.guards_alive().filter(func(g): return g.state == "charge").size()
|
||||
|
||||
func _toast_has(s: String) -> bool:
|
||||
for t in _toasts:
|
||||
if t.contains(s):
|
||||
return true
|
||||
return false
|
||||
|
||||
## Stand somewhere in barrow-local space (feet on the floor or the ground).
|
||||
func _stand(lp: Vector3) -> void:
|
||||
var w: Vector3 = bw.to_world(lp)
|
||||
var f: float = bw.floor_y_at(w)
|
||||
w.y = f if f > -INF else world.terrain.sample_height(w.x, w.z)
|
||||
player.global_position = w + Vector3(0, 0.05, 0)
|
||||
player.velocity = Vector3.ZERO
|
||||
|
||||
func _reset_guards() -> void:
|
||||
bw.alarmed = false
|
||||
for g in bw.guards_alive():
|
||||
g.global_position = g.post
|
||||
g.state = "rest"
|
||||
g._aggro = 0.0
|
||||
|
||||
func _body() -> void:
|
||||
await _scenery()
|
||||
await _walk_in()
|
||||
await _by_day()
|
||||
await _loot()
|
||||
await _at_night()
|
||||
await _late()
|
||||
await _bone()
|
||||
await _restock()
|
||||
await _saving()
|
||||
_net()
|
||||
_finished = true
|
||||
|
||||
# --- the mound ----------------------------------------------------------------------
|
||||
|
||||
func _scenery() -> void:
|
||||
print("the barrow")
|
||||
check(bw.found, "this island has a barrow")
|
||||
var c: Vector3 = bw.center()
|
||||
var spawn: Vector3 = world._spawn
|
||||
check(Vector2(c.x - spawn.x, c.z - spawn.z).length() > 55.0 and c.distance_to(world.den.position) > 70.0,
|
||||
"out in the valley, away from the spawn (%.0f m) and the den (%.0f m)" % [Vector2(c.x - spawn.x, c.z - spawn.z).length(), c.distance_to(world.den.position)])
|
||||
check(Vector2(c.x - world.bears.home.x, c.z - world.bears.home.z).length() > 40.0, "and from the bear's home")
|
||||
var trees := 0
|
||||
for n in world.props.resource_nodes.values():
|
||||
if Vector2(n.global_position.x - c.x, n.global_position.z - c.z).length() < bw.CLEAR_R:
|
||||
trees += 1
|
||||
check(trees == 0, "nothing grows inside the stone ring")
|
||||
var again: Node3D = load("res://scripts/world/barrow.gd").new()
|
||||
again.world = world
|
||||
again.terrain = world.terrain
|
||||
check(again._find_site().distance_to(c) < 0.01, "the same place every time for this seed")
|
||||
again.free()
|
||||
var stones := 0
|
||||
for ch in bw.get_node("Ring").get_children():
|
||||
stones += 1
|
||||
check(stones >= 9, "ringed by %d standing stones" % stones)
|
||||
check(bw.guards.size() == 3 and bw.guards.all(func(g): return g.state == "rest"), "three guards stand at their posts")
|
||||
var inside: Vector3 = bw.to_world(Vector3(0, 0.05, 0.5))
|
||||
check(world.build.roofed_at(inside), "the chamber has a roof (dry inside)")
|
||||
var top := PhysicsRayQueryParameters3D.create(c + Vector3(0, 10, 0), c + Vector3(0, -2, 0), 1)
|
||||
var hit := world.get_world_3d().direct_space_state.intersect_ray(top)
|
||||
check(not hit.is_empty() and hit.position.y > c.y + 2.8, "you can stand on top of the mound (%.1f m up)" % (hit.position.y - c.y if not hit.is_empty() else 0.0))
|
||||
|
||||
func _walk_in() -> void:
|
||||
print("going in")
|
||||
_set_time(3, 12.0)
|
||||
var m: Vector3 = bw.mouth()
|
||||
var along := PhysicsRayQueryParameters3D.create(bw.to_world(Vector3(0, 1.0, bw.PASSAGE_END + 1.0)), bw.to_world(Vector3(0, 1.0, 0.0)), 1)
|
||||
along.exclude = [player.get_rid()]
|
||||
check(world.get_world_3d().direct_space_state.intersect_ray(along).is_empty(), "the passage is open from the mouth to the chamber")
|
||||
player.global_position = m + Vector3(0, 0.1, 0)
|
||||
var to: Vector3 = bw.center() - m
|
||||
player.rotation = Vector3(0, atan2(-to.x, -to.z), 0)
|
||||
player.velocity = Vector3.ZERO
|
||||
player.set_physics_process(true)
|
||||
Input.action_press("move_forward")
|
||||
var got_in := false
|
||||
for _i in range(160):
|
||||
await physics_frame
|
||||
var l: Vector3 = bw.to_local_pos(player.global_position)
|
||||
if l.z < 0.6 and absf(l.x) < 2.0:
|
||||
got_in = true
|
||||
break
|
||||
Input.action_release("move_forward")
|
||||
for _i in range(5):
|
||||
await physics_frame
|
||||
player.set_physics_process(false)
|
||||
var l2: Vector3 = bw.to_local_pos(player.global_position)
|
||||
check(got_in, "you walk down the passage into the chamber (at %.1f, %.1f, %.1f local)" % [l2.x, l2.y, l2.z])
|
||||
check(bw.inside(player.global_position), "(inside the barrow)")
|
||||
|
||||
func _by_day() -> void:
|
||||
print("by day")
|
||||
_stand(Vector3(0, 0, 0.3))
|
||||
await _sim(8.0)
|
||||
check(_awake() == 0, "day 3, noon, in the chamber: the guards don't stir")
|
||||
check(bw.guards[0].interact_prompt(player).contains("still"), "\"%s\"" % bw.guards[0].interact_prompt(player))
|
||||
|
||||
func _loot() -> void:
|
||||
print("the coffin")
|
||||
player.inventory.load_data(Inventory.new(40).to_data())
|
||||
_stand(Vector3(0, 0, -0.5))
|
||||
_toasts.clear()
|
||||
check(A.loot_barrow(player, bw), "you open the coffin")
|
||||
check(player.inventory.count("barrow_key") == 1, "a barrow key piece")
|
||||
check(player.inventory.count("iron_bar") >= 1 and player.inventory.count("iron_nugget") >= 3,
|
||||
"and iron (%d bars, %d nuggets)" % [player.inventory.count("iron_bar"), player.inventory.count("iron_nugget")])
|
||||
check(_toast_has("something stirs"), "and something stirs (%s)" % [_toasts])
|
||||
await _sim(0.5)
|
||||
check(_awake() == 3, "all three guards wake, even by day")
|
||||
check(not A.loot_barrow(player, bw) and _toast_has("empty") and player.inventory.count("barrow_key") == 1, "open it again: empty")
|
||||
print("getting away")
|
||||
var hp0: float = player.survival.health
|
||||
await _sim(6.0, func(): return player.survival.health < hp0)
|
||||
check(player.survival.health <= hp0 - 19.0, "stay and they hit hard (%.0f)" % (hp0 - player.survival.health))
|
||||
player.survival.health = 100.0
|
||||
var g: Node3D = bw.guards[0]
|
||||
check(float(g.def.run) < 5.0, "but they're slower than you walk (%.1f m/s)" % float(g.def.run))
|
||||
_stand(Vector3(0, 0, 30))
|
||||
await _sim(15.0)
|
||||
check(_awake() == 0 and bw.guards_alive().all(func(gg): return gg.state in ["home", "rest"]), "past the stone ring and the leash they give up")
|
||||
await _sim(25.0, func(): return bw.guards_alive().all(func(gg): return gg.state == "rest"))
|
||||
check(bw.guards_alive().all(func(gg): return gg.state == "rest" and gg.global_position.distance_to(gg.post) < 0.6), "and walk back to their posts")
|
||||
|
||||
func _at_night() -> void:
|
||||
print("at night")
|
||||
_reset_guards()
|
||||
_set_time(4, 22.0)
|
||||
_stand(Vector3(0, 0, 14))
|
||||
await _sim(3.0)
|
||||
check(_awake() == 0, "outside the ring at night, they stay put")
|
||||
_stand(Vector3(0, 0, 8))
|
||||
await _sim(1.0)
|
||||
check(_awake() == 3, "step inside the ring at night and they come")
|
||||
player.inventory.set_slot(0, {"id": "torch", "count": 1})
|
||||
player.select(0)
|
||||
await _sim(2.0)
|
||||
check(_awake() == 3, "a torch doesn't frighten them")
|
||||
_stand(Vector3(0, 0, 40))
|
||||
await _sim(30.0, func(): return bw.guards_alive().all(func(gg): return gg.state == "rest"))
|
||||
|
||||
func _late() -> void:
|
||||
print("after day 10")
|
||||
_reset_guards()
|
||||
_set_time(11, 12.0)
|
||||
_stand(Vector3(0, 0, 8))
|
||||
await _sim(1.0)
|
||||
check(_awake() == 3, "day 11, noon: they wake by day too")
|
||||
_stand(Vector3(0, 0, 40))
|
||||
await _sim(30.0, func(): return bw.guards_alive().all(func(gg): return gg.state == "rest"))
|
||||
_reset_guards()
|
||||
|
||||
func _bone() -> void:
|
||||
print("bone")
|
||||
_set_time(3, 12.0)
|
||||
var g: Node3D = bw.guards[2]
|
||||
_stand(bw.to_local_pos(g.global_position) + Vector3(0, 0, 1.6))
|
||||
player.inventory.set_slot(0, {"id": "club", "count": 1})
|
||||
player.select(0)
|
||||
player.survival.stamina = 100.0
|
||||
var hp0: float = g.hp
|
||||
check(A.melee(player, g), "a club...")
|
||||
var club: float = hp0 - g.hp
|
||||
player.inventory.set_slot(0, {"id": "iron_axe", "count": 1})
|
||||
player.select(0)
|
||||
player.survival.stamina = 100.0
|
||||
hp0 = g.hp
|
||||
A.melee(player, g)
|
||||
var axe: float = hp0 - g.hp
|
||||
check(absf(club - 18.0) < 0.01 and absf(axe - 14.0) < 0.01, "cracks bone: 18 a blow (12 +50%%), an iron axe 14 (%.0f, %.0f)" % [club, axe])
|
||||
check(bw.alarmed and g.state == "charge", "hit one and they all wake")
|
||||
for gg in bw.guards_alive():
|
||||
gg.take_hit(500.0, player.global_position)
|
||||
check(bw.guards_alive().is_empty() and bw.fallen.size() == 3, "all three fall")
|
||||
var drops: Dictionary = g.butcher_drops()
|
||||
check(int(drops.get("iron_nugget", 0)) >= 1, "their bones hold a little iron (%s)" % [drops])
|
||||
|
||||
func _restock() -> void:
|
||||
print("restocking")
|
||||
var looted: int = bw.loot_day
|
||||
check(looted == 3, "(emptied on day 3)")
|
||||
_set_time(looted + 4, 7.0)
|
||||
_stand(Vector3(0, 0, 60))
|
||||
await _sim(0.3)
|
||||
check(not bw.stocked() and bw.guards_alive().is_empty(), "4 days on: still empty, no guards")
|
||||
_set_time(looted + 5, 7.0)
|
||||
_stand(Vector3(0, 0, 12))
|
||||
await _sim(0.3)
|
||||
check(not bw.stocked(), "5 days on, but you're right there: not yet")
|
||||
_stand(Vector3(0, 0, 60))
|
||||
await _sim(0.3)
|
||||
check(bw.stocked() and bw.guards_alive().size() == 3, "once you're away: the coffin's full and the guards are back")
|
||||
|
||||
func _saving() -> void:
|
||||
print("saving")
|
||||
_set_time(9, 12.0)
|
||||
_stand(Vector3(0, 0, -0.5))
|
||||
A.loot_barrow(player, bw)
|
||||
bw.guards[1].take_hit(500.0, player.global_position)
|
||||
await _reload()
|
||||
check(not bw.stocked() and bw.loot_day == 9, "the coffin is still empty after a reload")
|
||||
check(bw.guards.size() == 2 and bw.guards.all(func(g): return int(g.get_meta("post")) != 1), "and the fallen guard is still gone (%d standing)" % bw.guards.size())
|
||||
check(player.inventory.count("barrow_key") == 2, "two key pieces now (%d)" % player.inventory.count("barrow_key"))
|
||||
|
||||
func _net() -> void:
|
||||
print("online")
|
||||
var CN: Node = root.get_node("CreatureNet")
|
||||
var g: Node3D = bw.guards[0]
|
||||
g.set_meta("cid", 7)
|
||||
g.state = "charge"
|
||||
var buf := PackedByteArray()
|
||||
buf.resize(CN.ENTRY)
|
||||
CN._encode(buf, 0, g)
|
||||
var e: Array = CN._decode(buf, 0)
|
||||
check(e[1] == "guard" and e[4] == "charge", "a guard goes over the network (%s, %s)" % [e[1], e[4]])
|
||||
245
tests/test_thieves.gd
Normal file
245
tests/test_thieves.gd
Normal file
@@ -0,0 +1,245 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user