P1 iron: nuggets, ore, clay, kiln, smelter, forge, iron tools and arrows, lantern, tool wear and repair
This commit is contained in:
307
tests/test_iron.gd
Normal file
307
tests/test_iron.gd
Normal file
@@ -0,0 +1,307 @@
|
||||
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
|
||||
Reference in New Issue
Block a user