Tutorial: Old Tam's hut outline beside the fire, pieces snap into it

This commit is contained in:
Paul Bailey
2026-11-05 15:01:59 +00:00
parent f93ebe1bea
commit dbcc6f6e59
6 changed files with 157 additions and 8 deletions

View File

@@ -36,6 +36,7 @@ var pieces_root: Node3D
var _hash: Dictionary = {} ## Vector3i -> Array of [piece, point]
var last_blocker: Object ## what the last failed overlap check hit (debugging)
var outline: Node3D = null ## the tutorial's hut outline, while it's up (placement snaps to it)
var _dirty := true
func setup(w: Node3D) -> void:
@@ -206,6 +207,15 @@ func placement(def: Dictionary, hit: Dictionary, yaw: float) -> Dictionary:
var info: Dictionary = BuildPiece.kinds()[kind]
var hp: Vector3 = hit.position
var cands := []
# The tutorial's hut outline (ui/hut_outline.gd): a piece aimed near an
# empty slot of its own kind goes there, like building to a plan.
if outline != null and is_instance_valid(outline):
for s in outline.slots:
if s[0] == String(def.id) and (s[2] as Node3D).visible:
var sxf: Transform3D = s[1]
var d := sxf.origin.distance_to(hp)
if d < 2.0:
cands.append([d * 0.3, sxf])
for piece in build_pieces():
if piece.global_position.distance_to(hp) > 4.5:
continue
@@ -394,16 +404,19 @@ func _hut_place(out: Dictionary, center: Vector3, yaw: float, id: String, local:
## A spot near `near` with a clear, fairly flat `half`-metre square: nothing
## solid in the way and the ground within 0.6 m across it. Searches outward
## in rings. Returns `near` itself if nothing is found.
func debug_find_site(near: Vector3, half: float) -> Vector3:
func debug_find_site(near: Vector3, half: float, step: float = 4.0, min_dist: float = 0.0) -> Vector3:
var space := world.get_world_3d().direct_space_state
var box := BoxShape3D.new()
box.size = Vector3(half * 2.0 + 2.0, 6.0, half * 2.0 + 2.0)
var ex: Array[RID] = [terrain.get_node("TerrainBody").get_rid(), world.player.get_rid()]
for ring in range(0, 12):
var rings := int(ceil(48.0 / step))
for ring in range(0, rings):
if ring * step < min_dist:
continue
var n := maxi(1, ring * 6)
for i in range(n):
var a := TAU * i / n
var c := near + Vector3(cos(a), 0, sin(a)) * ring * 4.0
var c := near + Vector3(cos(a), 0, sin(a)) * ring * step
if not terrain.inside_boundary(c.x, c.z, half + 4.0):
continue
var hs: Array[float] = []

View File

@@ -120,6 +120,7 @@ func start(w: Node3D, from_saved: Dictionary = {}) -> void:
_enter()
func stop() -> void:
_clear_outline()
active = false
world = null
@@ -145,7 +146,16 @@ func _enter() -> void:
_check() # (done early? it ticks straight away)
const DemoWolf = preload("res://scripts/creatures/tutorial_wolf.gd")
const HutOutline = preload("res://scripts/ui/hut_outline.gd")
var demo_wolf: Node3D = null
var outline: Node3D = null ## the hut outline, while the hut step is on
func _clear_outline() -> void:
if outline != null and is_instance_valid(outline):
outline.queue_free()
outline = null
if world != null and is_instance_valid(world):
world.build.outline = null
## A step's set-up: the clock, some animals, the demo wolf.
func _setup(su: Dictionary) -> void:
@@ -168,6 +178,28 @@ func _setup(su: Dictionary) -> void:
if world.terrain.inside_boundary(at.x, at.z, 4.0) and world.terrain.sample_height(at.x, at.z) > world.terrain.water_level + 0.3:
world.creatures.spawn(String(sp.kind), at)
break
if String(su.get("outline", "")) == "hut":
_clear_outline()
# A good spot a few metres from your fire (or from you).
# Search out from the fire in small rings, starting far enough out
# that the hut (3 m half-width) leaves the fire 2 m clear of its walls.
var near: Vector3 = world.player.global_position
var step := 4.0
var min_dist := 0.0
for pc in world.all_pieces():
if pc.item_id == "campfire" and pc.global_position.distance_to(near) < 30.0:
near = pc.global_position
step = 1.5
min_dist = 5.0
var site: Vector3 = world.build.debug_find_site(near, 3.0, step, min_dist)
var yaw: float = world.player.rotation.y
var b := Basis(Vector3.UP, yaw)
var first: Transform3D = world.build._free_xf("floor", site + b * Vector3(-1, 0, -1), yaw, true)
outline = HutOutline.new()
outline.name = "HutOutline"
world.add_child(outline)
outline.setup(world, first)
world.build.outline = outline
if su.get("demo_wolf", false):
if demo_wolf != null and is_instance_valid(demo_wolf):
demo_wolf.queue_free()
@@ -200,6 +232,7 @@ func _check() -> void:
_advance()
func _advance() -> void:
_clear_outline() # (it was only for the step that's ending)
var s := current()
done.append(String(s.id))
step_done.emit(index, s)
@@ -349,6 +382,11 @@ func find_marker() -> Vector3:
for pc in world.all_pieces():
if pc.item_id == what.substr(6):
cands.append(pc)
elif what == "outline":
# Wherever the outline still has pieces to build: its middle.
if outline != null and is_instance_valid(outline):
return outline.center()
return Vector3.INF
elif what == "lake":
# The lake's shore, on your side of it.
var c: Vector2 = world.terrain.lake_center

74
scripts/ui/hut_outline.gd Normal file
View File

@@ -0,0 +1,74 @@
extends Node3D
## The tutorial's hut outline (docs/TUTORIAL_DESIGN.md §2, step 17): a pale,
## see-through 4 x 4 m hut on a good spot near your fire, the same 20 pieces
## the game's own snapping makes (floors, walls and a door, roofs, gables).
## Each piece of the outline fades out once a real piece of that kind stands
## in its place. It's a suggestion: you can build anywhere.
## [item, offset from the first floor, yaw] (from build_system.gd
## debug_build_hut at yaw 0: the pieces snap to a fixed grid).
const LAYOUT := [
["wood_floor", Vector3(0, 0, 0), 0.0], ["wood_floor", Vector3(2, 0, 0), 0.0],
["wood_floor", Vector3(0, 0, 2), 0.0], ["wood_floor", Vector3(2, 0, 2), 0.0],
["wood_wall", Vector3(0, 0, -1), 0.0], ["wood_wall", Vector3(0, 0, 3), 0.0],
["wood_wall", Vector3(2, 0, -1), 0.0], ["wood_door", Vector3(2, 0, 3), 0.0],
["wood_wall", Vector3(3, 0, 0), PI * 0.5], ["wood_wall", Vector3(-1, 0, 0), PI * 0.5],
["wood_wall", Vector3(3, 0, 2), PI * 0.5], ["wood_wall", Vector3(-1, 0, 2), PI * 0.5],
["wood_roof", Vector3(0, 2, 3), 0.0], ["wood_roof", Vector3(0, 2, -1), PI],
["wood_roof", Vector3(2, 2, 3), 0.0], ["wood_roof", Vector3(2, 2, -1), PI],
["wood_gable", Vector3(-1, 2, 2), PI * 0.5], ["wood_gable", Vector3(-1, 2, 0), -PI * 0.5],
["wood_gable", Vector3(3, 2, 2), PI * 0.5], ["wood_gable", Vector3(3, 2, 0), -PI * 0.5],
]
const MATCH := 0.35 ## a real piece this close to a slot fills it
var world: Node3D
var origin := Transform3D() ## the first floor's place
var slots: Array = [] ## [item, Transform3D, MeshInstance3D]
var _check := 0.0
## Lays the outline out with its first floor at `xf` (from build_system's
## free placement, so it sits level on the ground).
func setup(w: Node3D, xf: Transform3D) -> void:
world = w
origin = xf
var m := StandardMaterial3D.new()
m.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
m.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
m.albedo_color = Color(0.6, 0.88, 1.0, 0.22)
m.cull_mode = BaseMaterial3D.CULL_DISABLED
m.no_depth_test = false
for e in LAYOUT:
var t := Transform3D(xf.basis * Basis(Vector3.UP, float(e[2])), xf * (e[1] as Vector3))
var mi := MeshInstance3D.new()
mi.mesh = ItemDB.mesh_for(String(e[0]))
mi.material_override = m
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
add_child(mi)
mi.global_transform = t
slots.append([String(e[0]), t, mi])
## The middle of the hut (the guide's marker points here).
func center() -> Vector3:
return origin * Vector3(1, 0, 1)
func _process(delta: float) -> void:
_check -= delta
if _check <= 0.0:
_check = 0.5
refresh()
## Hides each slot a real piece of its kind now fills. Returns how many are
## left to build.
func refresh() -> int:
var left := 0
var built: Array = world.build.build_pieces() if world != null else []
for s in slots:
var filled := false
for p in built:
if p.item_id == s[0] and (p as Node3D).global_position.distance_to((s[1] as Transform3D).origin) < MATCH:
filled = true
break
(s[2] as MeshInstance3D).visible = not filled
if not filled:
left += 1
return left