Files
Emberwild/tests/test_iron.gd

308 lines
14 KiB
GDScript

extends SceneTree
## P1 iron (docs/PROGRESSION_DESIGN.md §2): iron nuggets, clay and rusty
## boulders on the island; the charcoal kiln, smelter and forge; iron tools,
## arrows and the lantern; tools wearing out and being repaired.
## Run: godot --headless --path . --script res://tests/test_iron.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
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("Iron", 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
player = world.player
player.set_physics_process(false)
func _body() -> void:
await _resources()
await _mining()
await _kiln_and_smelter()
await _forge_and_tools()
await _wear_and_repair()
await _arrows_and_spears()
await _saving()
_finished = true
# --- on the island --------------------------------------------------------------
func _resources() -> void:
print("iron and clay on the island")
var nuggets: Array = world.props.pickups.values().filter(func(p): return p.item_id == "iron_nugget")
var clay: Array = world.props.pickups.values().filter(func(p): return p.item_id == "clay")
var rusty: Array = world.props.resource_nodes.values().filter(func(n): return n.is_iron)
check(nuggets.size() >= 40, "%d iron nuggets lying about" % nuggets.size())
var by_rim := nuggets.filter(func(p): return world.props._near_rim(p.global_position.x, p.global_position.z) > 0.2).size()
check(by_rim > nuggets.size() / 2, "mostly in the scree under the mountains (%d of %d)" % [by_rim, nuggets.size()])
var lake: Vector2 = world.terrain.lake_center
var at_lake := clay.filter(func(p): return Vector2(p.global_position.x - lake.x, p.global_position.z - lake.y).length() < world.terrain.lake_radius * 1.7).size()
check(clay.size() >= 30 and at_lake == clay.size(), "%d lumps of clay, all on the lake shore" % clay.size())
check(rusty.size() >= 4, "%d rusty boulders (of %d)" % [rusty.size(), world.props.resource_nodes.values().filter(func(n): return n.kind == "rock").size()])
var total: int = world.props.pickups.size()
check(world.props.pickups.has("p0") and world.props.pickups["p0"].item_id == "stick",
"the older pickups keep their ids (%d in all; the new kinds come last)" % total)
func _mining() -> void:
print("mining iron ore")
var rusty: Array = world.props.resource_nodes.values().filter(func(n): return n.is_iron and not n.depleted)
var b: Node3D = rusty[0]
_stand_by(b.global_position, 1.4)
_give("stone_pickaxe", 1)
player.select(_slot_of("stone_pickaxe"))
var hits := 0
while not b.depleted and hits < 20:
player.survival.stamina = 100.0
if A.harvest_hit(player, b, b.global_position + Vector3(0, 0.4, 0)):
hits += 1
check(b.depleted and player.inventory.count("iron_ore") >= 2, "a rusty boulder breaks into %d iron ore (and stone), %d hits" % [player.inventory.count("iron_ore"), hits])
var pick = player.inventory.get_slot(_slot_of("stone_pickaxe"))
check(DB.durability(pick) == 60 - hits, "the stone pickaxe wore down: %d of 60 uses left" % DB.durability(pick))
# --- kiln and smelter -----------------------------------------------------------
var smelter: Node3D
func _kiln_and_smelter() -> void:
print("the charcoal kiln")
var kiln := _place("charcoal_kiln", 3.0)
check(kiln != null and kiln.get("inventory") != null, "a kiln (it opens like a chest)")
kiln.inventory.add("wood", 5)
check(kiln.working(), "with wood in it, it gets to work")
kiln.step(19.0)
check(kiln.inventory.count("charcoal") == 0, "nothing yet after 19 s")
kiln.step(25.0)
check(kiln.inventory.count("charcoal") == 2 and kiln.inventory.count("wood") == 1, "after 44 s: 2 charcoal from 4 wood (1 left over)")
check(not kiln.working(), "then it's idle (one wood isn't enough)")
print("the smelter")
smelter = _place("smelter", -3.0)
check(smelter != null, "a smelter")
smelter.inventory.add("iron_ore", 2)
smelter.inventory.add("charcoal", 2)
smelter.step(30.0)
check(smelter.inventory.count("iron_bar") == 1, "ore and charcoal: an iron bar after 30 s")
smelter.step(30.0)
check(smelter.inventory.count("iron_bar") == 2 and smelter.inventory.count("iron_ore") == 0, "and another")
smelter.inventory.add("iron_nugget", 3)
smelter.inventory.add("charcoal", 1)
smelter.step(31.0)
check(smelter.inventory.count("iron_bar") == 3 and smelter.inventory.count("iron_nugget") == 0, "three nuggets and a charcoal make a bar too")
smelter.inventory.add("iron_ore", 1)
smelter.inventory.add("charcoal", 1)
world.skip_time(40.0)
check(smelter.inventory.count("iron_bar") == 4, "it works through the night (sleeping)")
## A piece of `id` on the ground beside the player (`side` metres to the right).
func _place(id: String, side: float) -> Node3D:
var at: Vector3 = player.global_position + player.global_basis.x * side - player.global_basis.z * 3.0
at.y = world.terrain.sample_height(at.x, at.z)
return world.spawn_piece(DB.get_item(id), Transform3D(Basis(), at))
# --- the forge and iron tools ---------------------------------------------------
func _forge_and_tools() -> void:
print("the forge")
_clear()
_give("iron_bar", 11)
_give("wood", 10)
_give("fiber", 2)
check(not A.can_craft(player, "iron_axe"), "no iron axe without a forge")
var forge := _place("forge", 0.0)
check(forge != null and A.station_near(player, "forge"), "a forge")
check(A.craft(player, "iron_axe"), "an iron axe")
check(A.craft(player, "iron_pickaxe") and A.craft(player, "lantern") and A.craft(player, "iron_spear"), "an iron pickaxe, a lantern and an iron spear")
_give("stick", 4)
_give("feather", 4)
check(A.craft(player, "iron_arrow") and player.inventory.count("iron_arrow") == 10, "10 iron arrows")
check(A.craft(player, "nails") and player.inventory.count("nails") == 10, "10 nails")
print("iron is better")
var trees: Array = world.props.resource_nodes.values().filter(func(n): return n.kind == "tree" and not n.depleted)
trees.sort_custom(func(a, b): return a.global_position.distance_to(player.global_position) < b.global_position.distance_to(player.global_position))
var stone_hits := await _fell(trees[0], "stone_axe")
var iron_hits := await _fell(trees[1], "iron_axe")
check(iron_hits * 2 <= stone_hits + 1, "an iron axe fells a tree in %d hits, a stone one in %d" % [iron_hits, stone_hits])
check(DB.get_item("lantern").has("light"), "the lantern gives light (held like a torch)")
func _fell(tree: Node3D, axe: String) -> int:
_stand_by(tree.global_position, 1.5)
if player.inventory.count(axe) == 0:
_give(axe, 1)
player.select(_slot_of(axe))
var hits := 0
while not tree.depleted and hits < 30:
player.survival.stamina = 100.0
if A.harvest_hit(player, tree, tree.global_position + Vector3(0, 1.0, 0)):
hits += 1
return hits
# --- wear and repair ----------------------------------------------------------------
func _wear_and_repair() -> void:
print("tools wear out")
_clear()
_give("stone_axe", 1)
var slot := _slot_of("stone_axe")
player.inventory.set_slot(slot, {"id": "stone_axe", "count": 1, "dur": 1})
player.select(slot)
var trees: Array = world.props.resource_nodes.values().filter(func(n): return n.kind == "tree" and not n.depleted)
var t: Node3D = trees[0]
_stand_by(t.global_position, 1.5)
_toasts.clear()
player.survival.stamina = 100.0
A.harvest_hit(player, t, t.global_position + Vector3(0, 1.0, 0))
check(player.inventory.count("stone_axe") == 0 and _toasts.has("Your stone axe broke"), "an axe on its last use breaks (%s)" % [_toasts])
print("repairs")
_give("stone_pickaxe", 1)
slot = _slot_of("stone_pickaxe")
player.inventory.set_slot(slot, {"id": "stone_pickaxe", "count": 1, "dur": 12})
check(not A.can_repair(player, slot), "a worn pickaxe can't be repaired without a workbench and flint")
var wb := _place("workbench", 3.0)
_give("flint", 1)
check(A.can_repair(player, slot) and A.repair(player, slot), "at a workbench, with a flint, it can")
check(DB.durability(player.inventory.get_slot(slot)) == 60 and player.inventory.count("flint") == 0, "and it's as good as new (60), for 1 flint")
check(not A.can_repair(player, slot), "(a tool that isn't worn has nothing to repair)")
_give("iron_axe", 1)
var islot := _slot_of("iron_axe")
player.inventory.set_slot(islot, {"id": "iron_axe", "count": 1, "dur": 50})
_give("iron_bar", 1)
check(A.can_repair(player, islot) == A.station_near(player, "forge"), "iron tools are repaired at the forge, with an iron bar")
wb.queue_free()
print("wear sticks with the item")
var chest := _place("chest", -3.0)
await physics_frame
player.inventory.set_slot(islot, {"id": "iron_axe", "count": 1, "dur": 77})
A.transfer(player, player.inventory, islot, chest.inventory, chest)
var in_chest = null
for s in chest.inventory.slots:
if s != null and s.id == "iron_axe":
in_chest = s
check(in_chest != null and DB.durability(in_chest) == 77, "moved into a chest, the axe is still at 77 uses")
var cursor = chest.inventory.click(chest.inventory.slots.find(in_chest), null, false)
cursor = player.inventory.click(10, cursor, true) # (right-click: put down one)
check(cursor == null and DB.durability(player.inventory.get_slot(10)) == 77, "picked up and put down by hand, still 77")
var data: Array = player.inventory.to_data()
var inv2 := Inventory.new(40)
inv2.load_data(data)
check(DB.durability(inv2.get_slot(10)) == 77, "and through saving and loading")
# --- arrows and spears ---------------------------------------------------------------
func _arrows_and_spears() -> void:
print("iron arrows")
_clear()
_give("bow", 1)
_give("arrow", 5)
_give("iron_arrow", 3)
player.select(_slot_of("bow"))
var eye: Vector3 = player.global_position + Vector3(0, 1.6, 0)
var before := world.get_children().size()
check(A.can_draw(player) and A.shoot(player, eye, -player.global_basis.z, 1.0, world), "the bow shoots")
check(player.inventory.count("iron_arrow") == 2 and player.inventory.count("arrow") == 5, "an iron arrow first")
var pr: Node = world.get_children().back()
check(pr.get("item_id") == "iron_arrow" and absf(float(pr.damage) - 32.0 * 1.5) < 0.01, "for half as much again: %.0f damage at full draw" % float(pr.get("damage")))
check(DB.durability(player.inventory.get_slot(_slot_of("bow"))) == 99, "and the bow wears a little")
print("a thrown spear keeps its wear")
_give("spear", 1)
var ss := _slot_of("spear")
player.inventory.set_slot(ss, {"id": "spear", "count": 1, "dur": 20})
player.select(ss)
player.survival.stamina = 100.0
var drops_before: int = world.all_dropped().size()
A.throw_spear(player, ss, eye, (-player.global_basis.z + Vector3(0, -0.6, 0)).normalized(), world)
var t := 0.0
while world.all_dropped().size() == drops_before and t < 5.0:
await physics_frame
t += 1.0 / 60.0
var landed: Array = world.all_dropped().filter(func(d): return d.item_id == "spear")
check(not landed.is_empty() and int(landed[0].extra.get("dur", -1)) == 19, "it lands with 19 uses left")
if not landed.is_empty():
_stand_by(landed[0].global_position, 1.0)
A.pickup(player, landed[0])
check(DB.durability(player.inventory.get_slot(_slot_of("spear"))) == 19, "and picked up again, it still has 19 (no free repairs)")
# --- saving ------------------------------------------------------------------------
func _saving() -> void:
print("saving")
_clear()
_give("iron_pickaxe", 1)
player.inventory.set_slot(_slot_of("iron_pickaxe"), {"id": "iron_pickaxe", "count": 1, "dur": 123})
smelter.inventory.load_data(Inventory.new(6).to_data())
smelter.inventory.add("iron_ore", 3)
smelter.inventory.add("charcoal", 3)
smelter.step(12.0)
var at: Vector3 = smelter.global_position
check(world.save_now(), "saved")
world.queue_free()
await process_frame
await _load()
var sm: Node3D = null
for p in world.all_pieces():
if p.item_id == "smelter" and p.global_position.distance_to(at) < 0.1:
sm = p
check(sm != null and sm.inventory.count("iron_ore") == 3 and absf(float(sm.progress) - 12.0) < 0.5,
"the smelter comes back with its ore, 12 s into the batch (%s ore, %.1f s)" % [
sm.inventory.count("iron_ore") if sm != null else -1, float(sm.progress) if sm != null else -1.0])
check(DB.durability(player.inventory.get_slot(_slot_of("iron_pickaxe"))) == 123, "and the pickaxe with 123 uses left")
# --- helpers -----------------------------------------------------------------------
func _stand_by(pos: Vector3, d: float) -> void:
var dir: Vector3 = player.global_position - pos
dir.y = 0.0
if dir.length() < 0.1:
dir = Vector3(1, 0, 0)
var at: Vector3 = pos + dir.normalized() * d
at.y = world.terrain.sample_height(at.x, at.z)
player.global_position = at
func _give(id: String, n: int) -> void:
player.inventory.add(id, n)
func _clear() -> void:
player.inventory.load_data(Inventory.new(40).to_data())
func _slot_of(id: String) -> int:
for i in range(player.inventory.size()):
var s = player.inventory.get_slot(i)
if s != null and s.id == id:
return i
return -1