extends Node3D ## The barrow (docs/PROGRESSION_DESIGN.md ยง4): an ancient burial mound in a ## ring of standing stones, out on open ground. Scenery, like the wolf den: ## built from the seed on every machine, so it's the same for everyone. ## ## A stone-lined passage leads under the mound into a chamber with a stone ## coffin at the back. Three skeletal guards (guard.gd) stand inside. They ## wake when someone's inside the stone ring at night (or at any time after ## day WAKE_DAY), when the coffin is opened, or when one of them is hit. ## The coffin holds iron and a barrow key piece (three open the standing ## stones on the rim, P5). It fills again RESTOCK_DAYS after it was emptied, ## and fallen guards are back at their posts then too. ## ## Guards and loot run on the server (or in single-player): start(). ## Saved with the world: when the coffin was emptied and which guards fell. ## ## Local space: +Z is the passage's mouth (facing the centre of the valley), ## the origin is the chamber floor. const IM = preload("res://scripts/items/item_meshes.gd") const P = preload("res://scripts/world/proc_mesh.gd") const GuardScript = preload("res://scripts/creatures/guard.gd") const MOUND_R := 6.5 const MOUND_H := 3.6 const RING_R := 10.0 ## the standing stones const CLEAR_R := 11.5 const LEASH := 22.0 ## guards don't follow you further than this from the centre const WAKE_DAY := 10 ## after this, they wake by day too const RESTOCK_DAYS := 5 const PASSAGE_END := 6.4 ## the mouth, local z const POSTS := [[Vector3(-1.4, 0, -0.4), 0.0], [Vector3(1.4, 0, -0.4), 0.0], [Vector3(0.0, 0, 1.2), 0.0]] const LOOT := {"iron_bar": [1, 3], "iron_nugget": [3, 6], "barrow_key": [1, 1]} const STONE := Color(0.5, 0.5, 0.47) const MOSS := Color(0.32, 0.38, 0.22) const TURF := Color(0.3, 0.4, 0.18) const EARTH := Color(0.36, 0.3, 0.2) var world: Node3D var terrain: Node3D var found := false var yaw := 0.0 var alarmed := false ## guards stirred (the coffin was opened, one was hit) var loot_day := -1 ## the day it was emptied (-1: never) var fallen: Array = [] ## post indices whose guard is dead var guards: Array = [] var running := false ## this machine runs the guards (server / single-player) var _rng := RandomNumberGenerator.new() var _mouth_drop := 0.0 ## how far the ground at the mouth is below the floor # --- placing it ----------------------------------------------------------------------- func place(w: Node3D) -> bool: world = w terrain = w.terrain var site := _find_site() if site == Vector3.INF: push_warning("[barrow] no site for the barrow on this seed") return false found = true position = site yaw = atan2(-site.x, -site.z) # mouth toward the middle of the valley rotation.y = yaw var mouth: Vector3 = position + Basis(Vector3.UP, yaw) * Vector3(0, 0, PASSAGE_END) _mouth_drop = maxf(position.y - terrain.sample_height(mouth.x, mouth.z), 0.0) _clear_area(w.props) _build(w.props._rock_mat) w.props.static_cuts.append([Transform3D(Basis(Vector3.UP, yaw), position), MOUND_R + 0.5, MOUND_R + 1.5]) return true ## Open, fairly flat ground away from the spawn, the den and the sea. Where ## nothing qualifies (a lumpy island), later passes accept rougher ground ## nearer the spawn and the den; the first pass is the original search, so ## islands that had a site keep it. func _find_site() -> Vector3: for pass_ in [[1.2, 55.0, 70.0], [2.0, 45.0, 50.0], [3.0, 40.0, 40.0]]: var c: Vector3 = _search(float(pass_[0]), float(pass_[1]), float(pass_[2])) if c != Vector3.INF: return c return Vector3.INF func _search(max_spread: float, from_spawn: float, from_den: float) -> Vector3: _rng.seed = terrain.noise_seed + 555 var spawn: Vector3 = world._spawn var den_at: Vector3 = world.den.position if world.den != null and world.den.found else Vector3(1e9, 0, 1e9) var best := Vector3.INF var best_score := INF for _i in range(600): var a := _rng.randf() * TAU var rr := _rng.randf_range(40.0, terrain.play_radius - 20.0) var c := Vector2(cos(a), sin(a)) * rr if not terrain.inside_boundary(c.x, c.y, CLEAR_R + 4.0) or terrain.sea_weight(c.x, c.y) > 0.02: continue if c.distance_to(Vector2(spawn.x, spawn.z)) < from_spawn or c.distance_to(Vector2(den_at.x, den_at.z)) < from_den: continue var hs: Array[float] = [] for k in range(8): var b := TAU * k / 8.0 for rad in [MOUND_R * 0.5, PASSAGE_END + 0.5]: hs.append(terrain.sample_height(c.x + cos(b) * rad, c.y + sin(b) * rad)) var h0: float = terrain.sample_height(c.x, c.y) hs.append(h0) if hs.min() < terrain.water_level + 1.0: continue var spread: float = hs.max() - hs.min() if spread > max_spread: continue var trees := 0 for n in world.props.resource_nodes.values(): if Vector2(n.global_position.x - c.x, n.global_position.z - c.y).length() < CLEAR_R: trees += 1 var score: float = spread * 4.0 + trees if score < best_score: best_score = score # Floor level: the highest ground inside the chamber, so the # terrain doesn't come up through the floor. var top := h0 for d in [Vector2(-2, -2), Vector2(2, -2), Vector2(-2, 2), Vector2(2, 2)]: top = maxf(top, terrain.sample_height(c.x + d.x, c.y + d.y)) best = Vector3(c.x, top + 0.03, c.y) return best func _clear_area(props: Node3D) -> void: for dict in [props.resource_nodes, props.pickups, props.forage]: for uid in dict.keys(): var n: Node3D = dict[uid] if not is_instance_valid(n): continue if Vector2(n.global_position.x - position.x, n.global_position.z - position.z).length() < CLEAR_R: if n.has_method("_set_instance"): n._set_instance(Transform3D(Basis(), n.global_position + Vector3(0, -1000, 0))) dict.erase(uid) n.queue_free() # --- building it ----------------------------------------------------------------------- var _body: StaticBody3D var _st: SurfaceTool var _stone_rng := RandomNumberGenerator.new() ## A dressed stone block: drawn and solid. func _block(c: Vector3, s: Vector3, basis: Basis = Basis()) -> void: var col := P._mul(STONE, _stone_rng.randf_range(0.8, 1.05)).lerp(MOSS, _stone_rng.randf() * 0.35) IM._box(_st, c, s, col, basis) var cs := CollisionShape3D.new() var box := BoxShape3D.new() box.size = s cs.shape = box cs.transform = Transform3D(basis, c) _body.add_child(cs) func _build(rock_mat: Material) -> void: _stone_rng.seed = terrain.noise_seed + 556 _body = StaticBody3D.new() _body.name = "Stones" _body.collision_layer = 1 | 4 # solid, and a roof for shelter checks add_child(_body) _st = P._begin() # Chamber: 4 x 4 m inside, 2.4 m high, a slab roof. _block(Vector3(0, -0.15, 0), Vector3(4.8, 0.36, 4.8)) # floor (top at 0.03) _block(Vector3(0, 1.2, -2.2), Vector3(4.8, 2.4, 0.4)) # back for s in [-1.0, 1.0]: _block(Vector3(s * 2.2, 1.2, 0), Vector3(0.4, 2.4, 4.0)) # sides _block(Vector3(s * 1.4, 1.2, 2.2), Vector3(1.6, 2.4, 0.4)) # front, either side of the passage _block(Vector3(0, 2.6, 0), Vector3(4.8, 0.4, 4.8)) # roof _block(Vector3(0, 2.15, 2.2), Vector3(1.2, 0.5, 0.4)) # over the passage's inner end # Passage: 1.2 m wide, 1.95 m high, from the chamber to the mouth. Its # floor slopes down to the ground at the mouth. var plen := PASSAGE_END - 2.4 var mid := 2.4 + plen * 0.5 var tilt := atan2(_mouth_drop, plen) _block(Vector3(0, -0.15 - _mouth_drop * 0.5, mid), Vector3(1.6, 0.36, sqrt(plen * plen + _mouth_drop * _mouth_drop)), Basis(Vector3.RIGHT, tilt)) for s in [-1.0, 1.0]: _block(Vector3(s * 0.8, 1.0 - _mouth_drop * 0.5, mid), Vector3(0.4, 2.0 + _mouth_drop, plen)) _block(Vector3(0, 2.1, mid), Vector3(2.0, 0.3, plen)) # The mouth: two tall jambs and a lintel. for s in [-1.0, 1.0]: _block(Vector3(s * 0.95, 1.1 - _mouth_drop * 0.5, PASSAGE_END + 0.1), Vector3(0.5, 2.6 + _mouth_drop, 0.5)) _block(Vector3(0, 2.45, PASSAGE_END + 0.1), Vector3(2.6, 0.45, 0.6)) # The coffin at the back: a stone chest with a lid. _coffin = Coffin.new() _coffin.barrow = self _coffin.collision_layer = 1 _coffin.position = Vector3(0, 0, -1.45) # (its origin is what reach is measured to) add_child(_coffin) var ccs := CollisionShape3D.new() var cbox := BoxShape3D.new() cbox.size = Vector3(1.9, 0.85, 0.8) ccs.shape = cbox ccs.position = Vector3(0, 0.45, 0) _coffin.add_child(ccs) IM._box(_st, Vector3(0, 0.4, -1.45), Vector3(1.8, 0.75, 0.75), P._mul(STONE, 0.85)) IM._box(_st, Vector3(0, 0.82, -1.45), Vector3(1.95, 0.12, 0.85), STONE) IM._box(_st, Vector3(0, 0.9, -1.45), Vector3(0.9, 0.05, 0.3), MOSS) var mi := MeshInstance3D.new() mi.mesh = P._commit(_st) mi.material_override = ItemDB.item_material() add_child(mi) _build_mound() _build_ring(rock_mat) ## The grass mound over the chamber: a dome that meets the ground at its ## edge, open where the passage goes in. Its surface is solid (you can climb ## onto it); inside it you see the stone. func _build_mound() -> void: var st := P._begin() var faces := PackedVector3Array() var rings := 9 var segs := 36 var nz := FastNoiseLite.new() nz.seed = terrain.noise_seed + 557 var b := Basis(Vector3.UP, yaw) var pt := func(i: int, j: int) -> Vector3: var rr: float = MOUND_R * float(i) / rings var a: float = TAU * float(j) / segs var x: float = sin(a) * rr var z: float = cos(a) * rr var w: Vector3 = position + b * Vector3(x, 0, z) var ground: float = terrain.sample_height(w.x, w.z) - position.y var k: float = rr / MOUND_R return Vector3(x, lerpf(MOUND_H, ground - 0.3, k * k) + nz.get_noise_2d(x * 3.0, z * 3.0) * 0.12 * (1.0 - k), z) for i in range(rings): for j in range(segs): var q := [pt.call(i, j), pt.call(i + 1, j), pt.call(i + 1, j + 1), pt.call(i, j + 1)] for tri in [[q[0], q[1], q[2]], [q[0], q[2], q[3]]]: var c: Vector3 = (tri[0] + tri[1] + tri[2]) / 3.0 if absf(c.x) < 1.05 and c.z > 2.0: continue # the passage if (tri[0] as Vector3).distance_to(tri[1]) < 0.001 or (tri[1] as Vector3).distance_to(tri[2]) < 0.001: continue # (degenerate, at the top) var col := TURF.lerp(EARTH, clampf(0.4 - c.y * 0.12, 0.0, 0.5)) P._tri(st, tri[0], tri[1], tri[2], c - Vector3(0, -3, 0), P._mul(col, 0.9 + 0.2 * nz.get_noise_2d(c.x * 7.0, c.z * 7.0))) var nrm: Vector3 = ((tri[1] as Vector3) - tri[0]).cross((tri[2] as Vector3) - tri[0]) if nrm.dot(c - Vector3(0, -3, 0)) < 0.0: faces.append_array(PackedVector3Array([tri[0], tri[2], tri[1]])) else: faces.append_array(PackedVector3Array([tri[0], tri[1], tri[2]])) var mi := MeshInstance3D.new() mi.name = "Mound" mi.mesh = P._commit(st) mi.material_override = ItemDB.item_material() add_child(mi) var shape := ConcavePolygonShape3D.new() shape.set_faces(faces) shape.backface_collision = true var cs := CollisionShape3D.new() cs.shape = shape var body := StaticBody3D.new() body.name = "MoundBody" body.collision_layer = 1 | 4 body.add_child(cs) add_child(body) ## Standing stones round the mound, with a gap in front of the mouth. func _build_ring(rock_mat: Material) -> void: var body := StaticBody3D.new() body.name = "Ring" body.collision_layer = 1 add_child(body) var n := 11 for i in range(n): var a := TAU * (i + 0.5) / n if absf(wrapf(a, -PI, PI)) < 0.3: continue # (the mouth faces +Z: a = 0) var lp := Vector3(sin(a) * RING_R, 0, cos(a) * RING_R) var w: Vector3 = position + Basis(Vector3.UP, yaw) * lp var radii := Vector3(0.55, 1.5 + 0.35 * sin(i * 2.3), 0.4) var y: float = terrain.sample_height(w.x, w.z) - position.y + radii.y * 0.8 - 0.3 var mesh := P.rock(terrain.noise_seed + 5200 + i, false) var turn := Basis(Vector3.UP, a + 0.2 * sin(i * 1.7)) * Basis(Vector3.RIGHT, 0.05 * sin(i * 3.1)) var mi := MeshInstance3D.new() mi.mesh = mesh mi.material_override = rock_mat mi.transform = Transform3D(turn.scaled(radii), Vector3(lp.x, y, lp.z)) add_child(mi) var hull := mesh.create_convex_shape(true, true) as ConvexPolygonShape3D var pts := PackedVector3Array() for p: Vector3 in hull.points: pts.append(turn * (p * radii)) hull.points = pts var cs := CollisionShape3D.new() cs.shape = hull cs.position = Vector3(lp.x, y, lp.z) body.add_child(cs) # --- where things are -------------------------------------------------------------------- func center() -> Vector3: return position func to_world(lp: Vector3) -> Vector3: return position + Basis(Vector3.UP, yaw) * lp func to_local_pos(p: Vector3) -> Vector3: return Basis(Vector3.UP, yaw).inverse() * (p - position) ## Inside the chamber or passage. func inside(p: Vector3) -> bool: var l := to_local_pos(p) return (absf(l.x) < 2.0 and absf(l.z) < 2.0 or absf(l.x) < 0.6 and l.z > 1.9 and l.z < PASSAGE_END + 0.3) and l.y > -1.0 and l.y < 2.6 ## The stone floor's height at `p` (world), or -INF outside. func floor_y_at(p: Vector3) -> float: var l := to_local_pos(p) if absf(l.x) < 2.0 and absf(l.z) < 2.0: return position.y + 0.03 if absf(l.x) < 0.6 and l.z >= 2.0 and l.z < PASSAGE_END: return position.y + 0.03 - _mouth_drop * (l.z - 2.4) / (PASSAGE_END - 2.4) * float(l.z > 2.4) return -INF func mouth() -> Vector3: var m := to_world(Vector3(0, 0, PASSAGE_END + 1.2)) m.y = terrain.sample_height(m.x, m.z) return m # --- guards --------------------------------------------------------------------------- func is_night() -> bool: var h: float = world.day_night.hours() return h >= 19.5 or h < 5.5 ## Whether being where `p` is wakes the guards right now. func wakes_for(p: Node3D) -> bool: if alarmed: return true if p.survival.dead: return false var near := Vector2(p.global_position.x - position.x, p.global_position.z - position.z).length() < RING_R if near and (is_night() or world.day_night.day > WAKE_DAY): alarmed = true return alarmed func alarm() -> void: alarmed = true func guards_alive() -> Array: return guards.filter(func(g): return is_instance_valid(g) and not g.is_dead()) ## Starts the guards (on the machine that runs creatures). func start() -> void: if not found: return running = true _spawn_guards() func _spawn_guards() -> void: for g in guards: if is_instance_valid(g) and not g.is_dead(): g.queue_free() guards.clear() for i in range(POSTS.size()): if fallen.has(i): continue var g: AnimatableBody3D = GuardScript.new() var post: Vector3 = to_world(POSTS[i][0]) g.setup("guard", world, post, terrain.noise_seed + 600 + i) g.barrow = self g.post = Vector3(post.x, position.y + 0.03, post.z) # Facing the mouth (+Z local); a creature's forward is -Z, so turn by PI. g.post_yaw = yaw + PI + float(POSTS[i][1]) g.state = "rest" g.yaw = g.post_yaw g.rotation.y = g.yaw g.set_meta("post", i) g.died.connect(_on_guard_died) add_child(g) g.global_position = g.post guards.append(g) func _on_guard_died(g: Node) -> void: var i := int(g.get_meta("post", -1)) if i >= 0 and not fallen.has(i): fallen.append(i) func _physics_process(_delta: float) -> void: if running: step() func step() -> void: # Everyone's gone: the guards settle (they walk back to their posts). if alarmed: var anyone := false for p in world.players(): if not p.survival.dead and p.global_position.distance_to(position) < LEASH: anyone = true if not anyone: alarmed = false # The coffin fills again (and the fallen are back) a few days after it # was emptied, at dawn, with nobody about. if loot_day >= 0 and world.day_night.day >= loot_day + RESTOCK_DAYS: var h: float = world.day_night.hours() if h >= 6.0 and h < 10.0: for p in world.players(): if p.global_position.distance_to(position) < 30.0: return loot_day = -1 fallen.clear() alarmed = false _spawn_guards() # --- the coffin --------------------------------------------------------------------------- var _coffin: StaticBody3D func stocked() -> bool: return loot_day < 0 func days_to_restock() -> int: return maxi(loot_day + RESTOCK_DAYS - int(world.day_night.day), 1) ## Opens the coffin for `player` (Actions.loot_barrow). Wakes the guards. ## Returns what was taken ({} if it was empty). func loot(player: Node3D) -> Dictionary: alarm() if not stocked(): Actions.tell(player, "The coffin is empty. Come back in %d days." % days_to_restock()) return {} var r := RandomNumberGenerator.new() r.seed = terrain.noise_seed * 31 + int(world.day_night.day) var got := {} for id in LOOT: got[id] = r.randi_range(int(LOOT[id][0]), int(LOOT[id][1])) loot_day = int(world.day_night.day) Actions.tell(player, "You lift the coffin lid... and something stirs behind you!") return got func coffin_prompt() -> String: return "Stone coffin [E] Open" func save_data() -> Dictionary: return {"loot_day": loot_day, "fallen": fallen.duplicate()} if found else {} func load_data(d: Dictionary) -> void: if not found or d.is_empty(): return loot_day = int(d.get("loot_day", -1)) fallen = [] for i in d.get("fallen", []): fallen.append(int(i)) if running: _spawn_guards() class Coffin extends StaticBody3D: var barrow: Node func interact_prompt(_p: Node) -> String: return barrow.coffin_prompt() func interact(p: Node) -> void: Actions.loot_barrow(p, barrow)