extends Node3D ## The standing stones (docs/PROGRESSION_DESIGN.md ยง6): a ring of tall stones ## at the foot of the rim with an altar in the middle, built from the seed ## like the den and the barrow. The altar has three hollows for the barrow ## key pieces. With all three in, at night (19:30 to 05:30), E on the altar ## calls the Old Stag (old_stag.gd). ## ## - Lose (everyone leaves, dies, or dawn comes) and the stag goes back into ## the mist; the key pieces stay in the altar for another night. ## - Win and it drops its antler (for the antler pickaxe) and a trophy; the ## key pieces are used up, and the world remembers who beat it. ## ## The fight runs where the game decides (single-player, a server). Saved ## with the world: the key pieces in the altar and whether it's been beaten. ## Players online are sent the altar's state (world_net.gd _s2c_stones). const IM = preload("res://scripts/items/item_meshes.gd") const P = preload("res://scripts/world/proc_mesh.gd") const StagScript = preload("res://scripts/creatures/old_stag.gd") const CreatureScript = preload("res://scripts/creatures/creature.gd") const RING_R := 7.5 const STONES := 7 const CLEAR_R := 10.0 const KEYS := 3 const LEASH := 45.0 ## nobody this near for LEAVE_AFTER: the stag goes const LEAVE_AFTER := 5.0 var world: Node3D var terrain: Node3D var found := false var yaw := 0.0 var keys := 0 ## key pieces in the altar var beaten := false var beaten_by := "" var beaten_day := -1 var fighters: Array = [] ## (on a server) the names of everyone who fought it var stag: Node3D = null var wolves: Array = [] var deciding := true ## false on a player's game online var stomps := 0 var _alone := 0.0 var _rng := RandomNumberGenerator.new() var _key_meshes: Array = [] var _glow: OmniLight3D var _altar: StaticBody3D func place(w: Node3D) -> bool: world = w terrain = w.terrain var site := _find_site() if site == Vector3.INF: push_warning("[stones] no site for the standing stones on this seed") return false found = true position = site yaw = atan2(-site.x, -site.z) # the altar faces the valley rotation.y = yaw _clear_area(w.props) w.props.clear_bushes(position, CLEAR_R) _build(w.props._rock_mat) w.props.static_cuts.append([Transform3D(Basis(Vector3.UP, yaw), position), RING_R, RING_R]) return true ## At the foot of the rim, fairly flat, well away from the spawn, the den, ## the barrow and the bear's home. Rougher ground is accepted on later ## passes, so every island has one. func _find_site() -> Vector3: _rng.seed = terrain.noise_seed + 1501 var start := _rng.randf() * 360.0 var spawn: Vector3 = world._spawn var avoid: Array = [[Vector2(spawn.x, spawn.z), 60.0]] if world.den != null and world.den.found: avoid.append([Vector2(world.den.position.x, world.den.position.z), 60.0]) if world.barrow != null and world.barrow.found: avoid.append([Vector2(world.barrow.position.x, world.barrow.position.z), 50.0]) for pass_ in [[1.6, [6.0, 10.0, 14.0]], [2.6, [6.0, 10.0, 14.0, 18.0, 24.0, 30.0]], [4.0, [8.0, 16.0, 24.0, 32.0, 40.0]]]: for step in range(60): var ang := deg_to_rad(start + step * 6.0) var dir := Vector2(cos(ang), sin(ang)) var r: float = terrain.play_radius + 10.0 while r > terrain.play_radius - 50.0 and not terrain.inside_boundary(dir.x * r, dir.y * r, CLEAR_R + 2.0): r -= 1.0 for inset: float in pass_[1]: var c: Vector2 = dir * (r - inset) if not terrain.inside_boundary(c.x, c.y, CLEAR_R + 1.0) or terrain.sea_weight(c.x, c.y) > 0.02: continue var far := true for a in avoid: if c.distance_to(a[0]) < float(a[1]): far = false if not far: continue var hs: Array[float] = [] for k in range(8): var b := TAU * k / 8.0 hs.append(terrain.sample_height(c.x + cos(b) * RING_R, c.y + sin(b) * RING_R)) var h0: float = terrain.sample_height(c.x, c.y) hs.append(h0) if hs.max() - hs.min() > float(pass_[0]) or hs.min() < terrain.water_level + 1.0: continue return Vector3(c.x, h0, c.y) return Vector3.INF 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() func _build(rock_mat: Material) -> void: var body := StaticBody3D.new() body.name = "Stones" body.collision_layer = 1 add_child(body) for i in range(STONES): var a := TAU * i / STONES var lp := Vector3(sin(a) * RING_R, 0, cos(a) * RING_R) var w: Vector3 = position + Basis(Vector3.UP, yaw) * lp # Tall, narrow slabs (the rock mesh is a unit blob: stretch it up). var radii := Vector3(0.55, 3.8 + 0.6 * sin(i * 2.7), 0.38) var y: float = terrain.sample_height(w.x, w.z) - position.y + radii.y * 0.45 - 0.3 var mesh := P.rock(terrain.noise_seed + 6100 + i, false) var turn := Basis(Vector3.UP, a) * Basis(Vector3.RIGHT, 0.04 * sin(i * 1.9)) 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) # The altar: a low slab on two blocks, with three hollows on top. _altar = Altar.new() _altar.stones = self _altar.name = "Altar" _altar.collision_layer = 1 add_child(_altar) var st := P._begin() var stone := Color(0.46, 0.46, 0.44) var r := RandomNumberGenerator.new() r.seed = terrain.noise_seed + 1502 for sx in [-0.55, 0.55]: IM._box(st, Vector3(sx, 0.3, 0), Vector3(0.4, 0.6, 0.7), P._mul(stone, r.randf_range(0.85, 1.0))) IM._box(st, Vector3(0, 0.7, 0), Vector3(1.8, 0.2, 1.0), stone) for i in range(KEYS): IM._box(st, Vector3(-0.5 + 0.5 * i, 0.805, 0), Vector3(0.24, 0.01, 0.24), Color(0.16, 0.16, 0.16)) var mi := MeshInstance3D.new() mi.mesh = P._commit(st) mi.material_override = ItemDB.item_material() _altar.add_child(mi) var cs := CollisionShape3D.new() var box := BoxShape3D.new() box.size = Vector3(1.8, 0.8, 1.0) cs.shape = box cs.position = Vector3(0, 0.4, 0) _altar.add_child(cs) for i in range(KEYS): var k := MeshInstance3D.new() k.mesh = ItemDB.mesh_for("barrow_key") k.material_override = ItemDB.item_material() k.position = Vector3(-0.5 + 0.5 * i, 0.8, 0.08) k.rotation.y = PI k.visible = false _altar.add_child(k) _key_meshes.append(k) if DisplayServer.get_name() != "headless": _glow = OmniLight3D.new() _glow.light_color = Color(0.55, 0.9, 1.0) _glow.omni_range = 9.0 _glow.light_energy = 0.0 _glow.position = Vector3(0, 1.6, 0) add_child(_glow) _show() ## The key pieces in the altar, and a pale glow once all three are in. func _show() -> void: for i in range(_key_meshes.size()): _key_meshes[i].visible = i < keys if _glow != null: _glow.light_energy = 1.8 if keys >= KEYS else 0.3 * keys func center() -> Vector3: return position func altar() -> Node3D: return _altar func is_night() -> bool: var h: float = world.day_night.hours() return h >= 19.5 or h < 5.5 func fighting() -> bool: return stag != null and is_instance_valid(stag) and not stag.is_dead() # --- the altar -------------------------------------------------------------------------- func altar_prompt() -> String: if fighting(): return "The altar (the Old Stag is here!)" if keys < KEYS: return "Stone altar: %d of %d hollows filled [E] Set a barrow key piece" % [keys, KEYS] if is_night(): return "Stone altar, humming [E] Call the Old Stag" return "Stone altar, humming (come back at night)" ## E on the altar (Actions.use_stones): set key pieces in it, or call the ## stag. Returns what happened: "set", "summoned", "" (nothing). func use(player: Node3D) -> String: if fighting(): return "" if keys < KEYS: var have: int = player.inventory.count("barrow_key") if have <= 0: Actions.tell(player, "Three hollows in the stone (%d filled). Barrow key pieces would fit them." % keys) return "" var n := mini(have, KEYS - keys) player.inventory.remove("barrow_key", n) keys += n _changed() if keys < KEYS: Actions.tell(player, "You set a key piece in the altar (%d of %d)." % [keys, KEYS]) else: Actions.tell(player, "The last key piece clicks into place. The stones begin to hum%s." % ("" if is_night() else ": come back at night")) return "set" if not is_night(): Actions.tell(player, "The stones hum, waiting for the dark.") return "" summon(player) return "summoned" ## Calls the Old Stag, on the far side of the ring from `player`. func summon(player: Node3D) -> Node3D: var away := position - player.global_position away.y = 0.0 away = away.normalized() if away.length() > 0.1 else Vector3(1, 0, 0) var at := _inside_point(away, RING_R + 6.0) var s: AnimatableBody3D = StagScript.new() s.setup("old_stag", world, at, _rng.randi()) s.stones = self s.state = "summon" s._state_t = 2.5 var to: Vector3 = player.global_position - at s.yaw = atan2(-to.x, -to.z) s.rotation.y = s.yaw s.top_level = true # (in world space: the stones are moved and turned, a creature steers in world space) add_child(s) s.scale_for(group_size()) stag = s _alone = 0.0 world.weather.flash = 0.8 Actions.tell_all("The standing stones wake... the Old Stag has come!") return s ## How many (living) players are at the stones: the stag's toughened for ## them, and calls a wolf more for each. func group_size() -> int: var n := 0 for p in world.players(): if not p.survival.dead and p.global_position.distance_to(position) < LEASH: n += 1 return maxi(n, 1) ## The stag calls `n` spirit wolves out of the mist round the ring (and one ## more for each extra player, up to 4 more). func summon_wolves(s: Node3D, n: int) -> void: n += mini(group_size() - 1, 4) for i in range(n): var a := _rng.randf() * TAU var at := _inside_point(Vector3(cos(a), 0, sin(a)), RING_R + 4.0) var w: AnimatableBody3D = CreatureScript.new() w.setup("spirit_wolf", world, at, _rng.randi()) w.state = "charge" w._aggro = 9999.0 w.top_level = true add_child(w) wolves.append(w) var who: Node = world.nearest_player(s.global_position) if who != null: Actions.tell(who, "The Old Stag bellows, and spirit wolves come out of the mist!") ## A point `r` from the centre roughly along `dir`, turned until it's inside ## the valley with room to move (the stones are at the foot of the rim). func _inside_point(dir: Vector3, r: float) -> Vector3: for turn in [0.0, 0.5, -0.5, 1.0, -1.0, 1.6, -1.6, 2.2, -2.2, PI]: var p: Vector3 = position + dir.rotated(Vector3.UP, turn) * r if terrain.inside_boundary(p.x, p.z, 4.0) and terrain.sample_height(p.x, p.z) > terrain.water_level + 0.3: return p return position + dir * 3.0 func stomped(_s: Node3D) -> void: stomps += 1 world.weather.flash = maxf(world.weather.flash, 0.3) func wolves_alive() -> Array: return wolves.filter(func(w): return is_instance_valid(w) and not w.is_dead()) func _physics_process(_delta: float) -> void: if deciding: step(_delta) ## Sends the stag (and its wolves) back into the mist when everyone's gone ## or dead, or at dawn. func step(dt: float) -> void: if not fighting(): return var anyone := false for p in world.players(): if not p.survival.dead and p.global_position.distance_to(position) < LEASH: anyone = true _alone = 0.0 if anyone else _alone + dt if _alone >= LEAVE_AFTER or not is_night(): fade("dawn" if not is_night() else "gone") func fade(why: String) -> void: if stag != null and is_instance_valid(stag): stag.queue_free() stag = null _clear_wolves() Actions.tell_all("The Old Stag fades into the mist%s. The key pieces are still in the altar." % (" with the dawn" if why == "dawn" else "")) func _clear_wolves() -> void: for w in wolves: if is_instance_valid(w): w.queue_free() wolves.clear() ## Beaten: its antler and a trophy where it fell, the key pieces used up, ## and the world remembers. func stag_died(s: Node3D) -> void: var at: Vector3 = s.global_position + Vector3(0, 0.4, 0) world.drop_item("stag_antler", 1, Transform3D(Basis(), at)) world.drop_item("stag_trophy", 1, Transform3D(Basis(), at + Vector3(1.2, 0, 0.6))) _clear_wolves() keys = 0 var names: Array = s.fighter_names() beaten = true beaten_by = join_names(names) if world.server_mode else "you" fighters = names.duplicate() if world.server_mode else [] beaten_day = int(world.day_night.day) stag = null _changed() if world.server_mode: Actions.tell_all("The Old Stag has fallen! Beaten by %s on day %d." % [beaten_by if not names.is_empty() else "the island", beaten_day]) else: Actions.tell_all("The Old Stag has fallen! Its antler lies where it fell.") ## "Ana", "Ana and Bob", "Ana, Bob and Cara". static func join_names(names: Array) -> String: if names.is_empty(): return "" if names.size() == 1: return String(names[0]) return ", ".join(PackedStringArray(names.slice(0, names.size() - 1))) + " and " + String(names.back()) func _changed() -> void: _show() if world != null and world.server_mode: WorldNet.stones_state(save_data()) func save_data() -> Dictionary: if not found: return {} return {"keys": keys, "beaten": beaten, "beaten_by": beaten_by, "beaten_day": beaten_day, "fighters": fighters.duplicate()} func load_data(d: Dictionary) -> void: if not found or d.is_empty(): return keys = clampi(int(d.get("keys", 0)), 0, KEYS) beaten = bool(d.get("beaten", false)) beaten_by = String(d.get("beaten_by", "")) beaten_day = int(d.get("beaten_day", -1)) fighters = (d.get("fighters", []) as Array).duplicate() _show() class Altar extends StaticBody3D: var stones: Node func interact_prompt(_p: Node) -> String: return stones.altar_prompt() func interact(p: Node) -> void: Actions.use_stones(p, stones)