P2 stone: stonecutter, cut stone, stone floors/walls/pillars/stairs/windows/doorway, hearth; wooden stairs, windows, posts, palisade and gate; stone support rules
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_den_sites test_fishing_diet test_iron 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_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,
|
||||
|
||||
384
tests/test_stone.gd
Normal file
384
tests/test_stone.gd
Normal file
@@ -0,0 +1,384 @@
|
||||
extends SceneTree
|
||||
## P2 stone building (docs/PROGRESSION_DESIGN.md §3): the stonecutter and cut
|
||||
## stone; stone floors, walls, pillars, stairs, windows, doorways and the
|
||||
## hearth; stone's support rules; the wooden upgrades (stairs, windows, posts,
|
||||
## palisade and gate).
|
||||
## Run: godot --headless --path . --script res://tests/test_stone.gd
|
||||
|
||||
var _fails := 0
|
||||
var _finished := false
|
||||
var A: Node
|
||||
var S: Node
|
||||
var DB: Node
|
||||
var world: Node3D
|
||||
var player: CharacterBody3D
|
||||
var bs: Node
|
||||
|
||||
const NEW_PIECES := ["cut_stone", "stonecutter", "stone_floor", "stone_wall", "stone_pillar", "stone_stairs",
|
||||
"stone_window", "stone_doorway", "stone_hearth", "wood_stairs", "wood_window", "wood_pillar", "palisade", "palisade_gate"]
|
||||
|
||||
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")
|
||||
S.root_dir = "user://test_worlds"
|
||||
for w in S.list_worlds():
|
||||
S.delete_world(w.id)
|
||||
S.current_id = S.create_world("Stone", 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)
|
||||
player.survival.set_process(false)
|
||||
world.weather.set_process(false)
|
||||
bs = world.build
|
||||
|
||||
func _settle() -> void:
|
||||
for _i in range(3):
|
||||
await physics_frame
|
||||
|
||||
func _body() -> void:
|
||||
_data()
|
||||
await _crafting()
|
||||
await _tower()
|
||||
await _span()
|
||||
await _stairs()
|
||||
await _hearth_room()
|
||||
await _palisade()
|
||||
await _saving()
|
||||
_finished = true
|
||||
|
||||
# --- data -------------------------------------------------------------------------
|
||||
|
||||
func _data() -> void:
|
||||
print("items, recipes and meshes")
|
||||
var missing := []
|
||||
for id in NEW_PIECES:
|
||||
if DB.get_item(id).is_empty() or not DB.recipes.has(id):
|
||||
missing.append(id)
|
||||
check(missing.is_empty(), "every new piece has an item and a recipe (missing %s)" % [missing])
|
||||
var bad := []
|
||||
for m in NEW_PIECES + ["stone_doorway_frame", "palisade_gate_frame"]:
|
||||
var mesh: Mesh = DB.mesh_for(m)
|
||||
# (A missing mesh falls back to a 0.3 m magenta pebble.)
|
||||
if mesh == null or mesh.get_aabb().size.length() < 0.2 or (m != "cut_stone" and mesh.get_aabb().size.length() < 0.5):
|
||||
bad.append(m)
|
||||
check(bad.is_empty(), "every new piece has its own mesh (bad: %s)" % [bad])
|
||||
var sizes := {"stone_wall": Vector3(2, 2, 0.22), "stone_floor": Vector3(2, 0.2, 2), "palisade": Vector3(2, 3, 0.38)}
|
||||
for k in sizes:
|
||||
var s: Vector3 = DB.mesh_for(k).get_aabb().size
|
||||
check(s.distance_to(sizes[k]) < 0.15, "%s mesh is %s (matches its collider)" % [k, s])
|
||||
check(DB.get_item("stone_wall").place.get("material", "") == "stone" and DB.get_item("wood_stairs").place.get("material", "wood") == "wood",
|
||||
"stone pieces are stone, the wooden upgrades wood")
|
||||
|
||||
# --- the stonecutter ------------------------------------------------------------------
|
||||
|
||||
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))
|
||||
|
||||
func _crafting() -> void:
|
||||
print("the stonecutter")
|
||||
_clear()
|
||||
_give("wood", 30)
|
||||
_give("iron_bar", 4)
|
||||
_give("stone", 40)
|
||||
_give("nails", 10)
|
||||
check(not A.can_craft(player, "stonecutter"), "no stonecutter without a workbench")
|
||||
var wb := _place("workbench", 3.0)
|
||||
check(A.craft(player, "stonecutter"), "a stonecutter at the workbench (8 wood, 4 iron, 10 stone)")
|
||||
check(not A.can_craft(player, "cut_stone"), "no cut stone until it's built")
|
||||
var sc := _place("stonecutter", -3.0)
|
||||
check(sc != null and A.station_near(player, "stonecutter"), "placed, it's a station")
|
||||
var n := 0
|
||||
while A.craft(player, "cut_stone"):
|
||||
n += 1
|
||||
check(n == 15 and player.inventory.count("cut_stone") == 15, "30 stone cut into %d blocks (2 each)" % n)
|
||||
check(A.craft(player, "stone_wall") and A.craft(player, "stone_floor") and A.craft(player, "stone_pillar"),
|
||||
"a stone wall, floor and pillar from cut stone")
|
||||
check(player.inventory.count("cut_stone") == 2, "for 6 + 4 + 3 blocks")
|
||||
check(A.craft(player, "wood_stairs") and A.craft(player, "palisade") and A.craft(player, "wood_window"),
|
||||
"wooden stairs, a palisade and a window at the workbench, with nails")
|
||||
check(player.inventory.count("nails") == 2, "(4 + 2 + 2 nails)")
|
||||
wb.queue_free()
|
||||
sc.queue_free()
|
||||
await _settle()
|
||||
|
||||
# --- support ------------------------------------------------------------------------
|
||||
|
||||
## A clear, fairly flat spot `ahead` metres in front of the spawn.
|
||||
func _site(ahead: float, half: float) -> Vector3:
|
||||
return bs.debug_find_site(player.global_position - player.global_basis.z * ahead, half)
|
||||
|
||||
func _spawn(id: String, xf: Transform3D) -> Node3D:
|
||||
return world.spawn_piece(DB.get_item(id), xf)
|
||||
|
||||
var tower_top: Node3D
|
||||
|
||||
func _tower() -> void:
|
||||
print("a stone tower")
|
||||
var site := _site(14.0, 3.0)
|
||||
var f: Transform3D = bs._free_xf("floor", site, 0.0, true)
|
||||
check(bs.check(DB.get_item("stone_floor"), f) == "", "a stone floor on the ground")
|
||||
var floor: Node3D = _spawn("stone_floor", f)
|
||||
var walls: Array = []
|
||||
var why := ""
|
||||
for i in range(5):
|
||||
var xf := Transform3D(f.basis, f.origin + Vector3(0, 2.0 * i, -1))
|
||||
why = bs.check(DB.get_item("stone_wall"), xf)
|
||||
if not why.is_empty():
|
||||
break
|
||||
walls.append(_spawn("stone_wall", xf))
|
||||
await physics_frame
|
||||
tower_top = walls.back()
|
||||
check(walls.size() == 5, "five stone walls stacked: 10 m (%s)" % why)
|
||||
check(tower_top.support > 0.78, "the top one still has %d%% support (5%% lost per storey)" % roundi(tower_top.support * 100.0))
|
||||
# The same in wood, beside it.
|
||||
var wf := Transform3D(f.basis, f.origin + Vector3(0, 0, 1))
|
||||
var wood: Array = []
|
||||
for i in range(5):
|
||||
wood.append(_spawn("wood_wall", Transform3D(f.basis, wf.origin + Vector3(0, 2.0 * i, 0))))
|
||||
check(wood.back().support < tower_top.support - 0.1, "a wooden one loses more (%d%% at the top)" % roundi(wood.back().support * 100.0))
|
||||
print("stone and wood together")
|
||||
var on_wood := Transform3D(f.basis, wf.origin + Vector3(0, 10.0, 0))
|
||||
var reason: String = bs.check(DB.get_item("stone_wall"), on_wood)
|
||||
check(reason.begins_with("Stone needs stone"), "stone on wood is refused: \"%s\"" % reason)
|
||||
var wood_on_stone := Transform3D(f.basis, f.origin + Vector3(0, 10.0, -1))
|
||||
check(bs.check(DB.get_item("wood_wall"), wood_on_stone) == "", "wood on stone is fine (stone at the bottom, wood on top)")
|
||||
wood.reverse() # (top down: taking the bottom first would drop the rest)
|
||||
for p in wood:
|
||||
world.remove_piece(p)
|
||||
await _settle()
|
||||
check(tower_top.support > 0.78 and is_instance_valid(floor), "taking the wooden wall down leaves the stone one standing")
|
||||
|
||||
func _span() -> void:
|
||||
print("pillars carry a span")
|
||||
var site := _site(-16.0, 6.0)
|
||||
var f0: Transform3D = bs._free_xf("floor", site, 0.0, true)
|
||||
var x := f0.basis.x
|
||||
# A row of floors on the ground, a wall on the west end, and floors out
|
||||
# from the wall's top, one storey up.
|
||||
for i in range(6):
|
||||
_spawn("wood_floor", Transform3D(f0.basis, f0.origin + x * 2.0 * i))
|
||||
_spawn("wood_wall", Transform3D(f0.basis.rotated(Vector3.UP, PI * 0.5), f0.origin - x * 1.0))
|
||||
await physics_frame
|
||||
var up: Array = []
|
||||
var fdef: Dictionary = DB.get_item("wood_floor")
|
||||
var refused_at := -1
|
||||
for i in range(6):
|
||||
var xf := Transform3D(f0.basis, f0.origin + x * 2.0 * i + Vector3(0, 2, 0))
|
||||
if not bs.check(fdef, xf).is_empty():
|
||||
refused_at = i
|
||||
break
|
||||
up.append(_spawn("wood_floor", xf))
|
||||
await physics_frame
|
||||
check(refused_at >= 2 and refused_at <= 4, "a floor hanging out from a wall reaches %d m" % (refused_at * 2))
|
||||
# A pillar under the far edge, standing on the ground floor's corners.
|
||||
var pdef: Dictionary = DB.get_item("stone_pillar")
|
||||
var g: Node3D = null
|
||||
for p in bs.build_pieces():
|
||||
if p.kind == "floor" and p.global_position.distance_to(f0.origin + x * 2.0 * (refused_at - 1)) < 0.1:
|
||||
g = p
|
||||
var corner: Vector3 = g.global_transform * Vector3(1, 0, 1)
|
||||
var aim := {"position": corner + Vector3(-0.2, 0, -0.2), "normal": Vector3.UP, "collider": g}
|
||||
var r: Dictionary = bs.placement(pdef, aim, 0.0)
|
||||
check(r.ok and r.xf.origin.distance_to(corner) < 0.01, "a pillar snaps onto a floor's corner (%s)" % r.reason)
|
||||
_spawn("stone_pillar", r.xf)
|
||||
_spawn("stone_pillar", Transform3D(r.xf.basis, g.global_transform * Vector3(1, 0, -1)))
|
||||
await physics_frame
|
||||
var xf := Transform3D(f0.basis, f0.origin + x * 2.0 * refused_at + Vector3(0, 2, 0))
|
||||
check(bs.check(fdef, xf) == "", "with it, the next floor out is fine")
|
||||
var held: Node3D = _spawn("wood_floor", xf)
|
||||
check(held.support > 0.8, "carried straight down to the ground (%d%% support)" % roundi(held.support * 100.0))
|
||||
check(bs.check(fdef, Transform3D(f0.basis, xf.origin + x * 2.0)) == "", "and the span goes on from there")
|
||||
var near: Array = bs.build_pieces().filter(func(p): return p.global_position.distance_to(f0.origin) < 14.0)
|
||||
near.sort_custom(func(a, b): return a.global_position.y > b.global_position.y)
|
||||
for p in near:
|
||||
if is_instance_valid(p) and p.is_inside_tree() and not p.is_queued_for_deletion():
|
||||
world.remove_piece(p)
|
||||
await _settle()
|
||||
|
||||
# --- stairs ---------------------------------------------------------------------------
|
||||
|
||||
func _stairs() -> void:
|
||||
print("stairs")
|
||||
var site := _site(-6.0, 4.0)
|
||||
var yaw: float = player.rotation.y
|
||||
var sdef: Dictionary = DB.get_item("wood_stairs")
|
||||
var r: Dictionary = bs.placement(sdef, {"position": site, "normal": Vector3.UP, "collider": world.terrain.get_node("TerrainBody")}, yaw)
|
||||
check(r.ok, "wooden stairs on the ground (%s)" % r.reason)
|
||||
var stairs: Node3D = world.spawn_piece(sdef, r.xf)
|
||||
var top: Vector3 = r.xf * Vector3(0, 2, -4)
|
||||
var landing: Node3D = _spawn("wood_floor", Transform3D(r.xf.basis, r.xf * Vector3(0, 2, -5)))
|
||||
check(landing.support >= bs.MIN_SUPPORT, "a floor at the top, held by the stairs (%d%%)" % roundi(landing.support * 100.0))
|
||||
await _settle()
|
||||
var q := PhysicsRayQueryParameters3D.create(r.xf * Vector3(0, 3, -2), r.xf * Vector3(0, -1, -2), 1)
|
||||
var hit := world.get_world_3d().direct_space_state.intersect_ray(q)
|
||||
check(not hit.is_empty() and absf(hit.position.y - (r.xf * Vector3(0, 1, -2)).y) < 0.05 and hit.normal.y > cos(deg_to_rad(40.0)),
|
||||
"a smooth ramp halfway up, gentle enough to walk (%.0f degrees)" % rad_to_deg(acos(hit.normal.y if not hit.is_empty() else 0.0)))
|
||||
# Walk up them.
|
||||
player.global_position = r.xf * Vector3(0, 0.1, 1.0)
|
||||
player.rotation.y = r.xf.basis.get_euler().y
|
||||
player.velocity = Vector3.ZERO
|
||||
player.set_physics_process(true)
|
||||
Input.action_press("move_forward")
|
||||
var best := -INF
|
||||
for _i in range(150):
|
||||
await physics_frame
|
||||
best = maxf(best, player.global_position.y)
|
||||
if player.global_position.distance_to(r.xf * Vector3(0, 2, -5.5)) < 0.6:
|
||||
break
|
||||
Input.action_release("move_forward")
|
||||
for _i in range(10):
|
||||
await physics_frame
|
||||
player.set_physics_process(false)
|
||||
var on_top: bool = absf(player.global_position.y - top.y) < 0.2 and (r.xf.affine_inverse() * player.global_position).z < -4.0
|
||||
check(on_top, "the player walks up them onto the landing (%.2f m up, %.1f m along)" % [
|
||||
player.global_position.y - r.xf.origin.y, -(r.xf.affine_inverse() * player.global_position).z])
|
||||
world.remove_piece(landing)
|
||||
world.remove_piece(stairs)
|
||||
await _settle()
|
||||
|
||||
# --- a stone house with a hearth ----------------------------------------------------------
|
||||
|
||||
func _hearth_room() -> void:
|
||||
print("a stone house")
|
||||
var sv: Node = player.survival
|
||||
var site := _site(26.0, 3.0)
|
||||
var yaw: float = player.rotation.y
|
||||
var hut: Dictionary = await bs.debug_build_hut(site, yaw, "stone")
|
||||
await _settle()
|
||||
check(hut.placed == 20 and hut.failed.is_empty(), "a stone hut with a wooden roof, by snapping (%d placed, failed %s)" % [hut.placed, hut.failed])
|
||||
var f0: Node3D = hut.pieces[0]
|
||||
var center: Vector3 = f0.global_transform * Vector3(1, 0, 1)
|
||||
var b := Basis(Vector3.UP, yaw)
|
||||
var inside: Vector3 = center + b * Vector3(0.6, 0.05, 0.8)
|
||||
check(bs.sheltered_at(inside) and bs.stone_walls_at(inside) > 0.7, "inside is sheltered, by stone (%d%%)" % roundi(bs.stone_walls_at(inside) * 100.0))
|
||||
print("the hearth")
|
||||
var hdef: Dictionary = DB.get_item("stone_hearth")
|
||||
var hxf := Transform3D(b, center + b * Vector3(0, 0, -1.25))
|
||||
check(world.placement_valid(hdef, hxf), "a hearth fits against the back wall")
|
||||
var hearth: Node3D = world.spawn_piece(hdef, hxf)
|
||||
check(hearth.is_in_group("station:campfire") and hearth.is_in_group("heat"), "it cooks and warms like a campfire")
|
||||
var camp: Node3D = world.spawn_piece(DB.get_item("campfire"), Transform3D(b, site + b * Vector3(12, 0, 0)))
|
||||
var far := 8.5
|
||||
check(hearth.heat_at(hearth.global_position + Vector3(far, 0, 0)) > 0.1 and camp.heat_at(camp.global_position + Vector3(far, 0, 0)) == 0.0,
|
||||
"its warmth reaches %.1f m, where a campfire's doesn't" % far)
|
||||
world.weather.force("storm")
|
||||
var out_hearth: Node3D = world.spawn_piece(hdef, Transform3D(b, site + b * Vector3(-12, 0, 0)))
|
||||
camp._roofed_cache = -1.0
|
||||
check(out_hearth.burn_rate() == 1.0 and camp.burn_rate() > 2.0, "a storm doesn't eat its fuel, even outside (a campfire's burns %.1fx)" % camp.burn_rate())
|
||||
world.remove_piece(out_hearth)
|
||||
world.remove_piece(camp)
|
||||
player.global_position = inside
|
||||
sv.wet = 0.0
|
||||
sv._shelter_left = 0.0
|
||||
sv.step(0.5)
|
||||
check(sv.sheltered and sv.stone_room and "Warm" in sv.statuses and "Sheltered" in sv.statuses,
|
||||
"by the hearth: Sheltered and Warm, in a stone room (%s)" % [sv.statuses])
|
||||
# With the fire out, the stone alone: a few degrees over what the air
|
||||
# gives a sheltered spot.
|
||||
hearth._set_lit(false)
|
||||
sv.warmth = 0.0
|
||||
for _i in range(60):
|
||||
sv.step(1.0)
|
||||
var air: float = world.air_warmth(inside, true)
|
||||
check(absf(sv.warmth - (air + sv.STONE_ROOM_WARMTH)) < 1.5, "stone keeps you a little warmer (%.0f, against %.0f from the air)" % [sv.warmth, air])
|
||||
hearth._set_lit(true)
|
||||
print("a window")
|
||||
var wall: Node3D = hut.pieces[4]
|
||||
var wxf: Transform3D = wall.global_transform
|
||||
world.remove_piece(wall)
|
||||
await _settle()
|
||||
check(bs.check(DB.get_item("stone_window"), wxf) == "", "a stone window goes where the wall was")
|
||||
world.spawn_piece(DB.get_item("stone_window"), wxf)
|
||||
await _settle()
|
||||
check(bs.sheltered_at(inside), "and the room is still sheltered")
|
||||
var through := PhysicsRayQueryParameters3D.create(wxf * Vector3(0, 1.35, 1), wxf * Vector3(0, 1.35, -1), 1)
|
||||
check(world.get_world_3d().direct_space_state.intersect_ray(through).is_empty(), "but you can see out of it")
|
||||
world.weather.force("clear")
|
||||
|
||||
# --- the palisade -----------------------------------------------------------------------
|
||||
|
||||
func _palisade() -> void:
|
||||
print("a palisade")
|
||||
var site := _site(-28.0, 4.0)
|
||||
var yaw: float = player.rotation.y
|
||||
var tb: Node = world.terrain.get_node("TerrainBody")
|
||||
var pdef: Dictionary = DB.get_item("palisade")
|
||||
var r: Dictionary = bs.placement(pdef, {"position": site, "normal": Vector3.UP, "collider": tb}, yaw)
|
||||
check(r.ok, "a palisade on the ground (%s)" % r.reason)
|
||||
var p0: Node3D = world.spawn_piece(pdef, r.xf)
|
||||
await _settle()
|
||||
var aim: Vector3 = p0.global_transform * Vector3(1.6, 0, 0)
|
||||
aim.y = world.terrain.sample_height(aim.x, aim.z)
|
||||
var r2: Dictionary = bs.placement(pdef, {"position": aim, "normal": Vector3.UP, "collider": tb}, yaw)
|
||||
check(r2.ok and r2.snapped and r2.xf.origin.distance_to(p0.global_transform * Vector3(2, 0, 0)) < 0.01, "the next one snaps on, 2 m along")
|
||||
var p1: Node3D = world.spawn_piece(pdef, r2.xf)
|
||||
var gdef: Dictionary = DB.get_item("palisade_gate")
|
||||
var gxf := Transform3D(p0.global_basis, p0.global_transform * Vector3(-2, 0, 0))
|
||||
var gate: Node3D = world.spawn_piece(gdef, gxf)
|
||||
await _settle()
|
||||
check(gate.has_method("set_open"), "a gate in it that opens")
|
||||
var space := world.get_world_3d().direct_space_state
|
||||
var high := PhysicsRayQueryParameters3D.create(p0.global_transform * Vector3(0, 2.6, 2), p0.global_transform * Vector3(0, 2.6, -2), 1)
|
||||
check(not space.intersect_ray(high).is_empty(), "it's 3 m tall: higher than a wall")
|
||||
var net: Node = root.get_node("Net")
|
||||
check(net._wall_hit(space, p1.global_transform * Vector3(0, 1, 2), p1.global_transform * Vector3(0, 1, -2)), "online it blocks wolves and players like a wall")
|
||||
for p in [p0, p1, gate]:
|
||||
world.remove_piece(p)
|
||||
await _settle()
|
||||
|
||||
# --- saving -------------------------------------------------------------------------------
|
||||
|
||||
func _saving() -> void:
|
||||
print("saving")
|
||||
var at: Vector3 = tower_top.global_position
|
||||
var sup: float = tower_top.support
|
||||
check(world.save_now(), "saved")
|
||||
world.queue_free()
|
||||
await process_frame
|
||||
await _load()
|
||||
var top: Node3D = null
|
||||
var hearths := 0
|
||||
for p in world.all_pieces():
|
||||
if p.item_id == "stone_wall" and p.global_position.distance_to(at) < 0.05:
|
||||
top = p
|
||||
if p.item_id == "stone_hearth":
|
||||
hearths += 1
|
||||
check(top != null and top.material == "stone" and absf(top.support - sup) < 0.001, "the stone tower comes back, just as strong")
|
||||
check(hearths == 1, "and the hearth")
|
||||
|
||||
# --- helpers --------------------------------------------------------------------------
|
||||
|
||||
func _give(id: String, n: int) -> void:
|
||||
player.inventory.add(id, n)
|
||||
|
||||
func _clear() -> void:
|
||||
player.inventory.load_data(Inventory.new(40).to_data())
|
||||
Reference in New Issue
Block a user