P5 the Old Stag: standing stones and altar, three-phase boss with charges, stun window, spirit wolves and stomps; antler pickaxe and trophy; group scaling and credit for every fighter; online 3-player test (protocol 12)
This commit is contained in:
@@ -243,7 +243,7 @@ func harvest_hit(player: Node3D, node: Node3D, hit_point: Vector3) -> bool:
|
||||
if tool.get("kind", "") != node.tool_kind or int(tool.get("tier", 0)) < node.min_tier:
|
||||
notify.emit("Needs a %s (Tab to craft)" % ItemDB.item_name("stone_" + node.tool_kind))
|
||||
return false
|
||||
if not player.survival.use_stamina(6.0):
|
||||
if not player.survival.use_stamina(float(tool.get("stamina", 6.0))): # (the antler pickaxe is lighter)
|
||||
notify.emit("Too tired")
|
||||
return false
|
||||
var drops: Dictionary = node.hit(float(tool.get("damage", 1.0)), hit_point, player.global_position)
|
||||
@@ -323,6 +323,15 @@ func loot_barrow(player: Node3D, barrow: Node) -> bool:
|
||||
tell(player, "+%d %s" % [int(got[id]), ItemDB.item_name(id)])
|
||||
return true
|
||||
|
||||
## E on the standing stones' altar (standing_stones.gd, P5): set barrow key
|
||||
## pieces in it, or (all three in, at night) call the Old Stag.
|
||||
func use_stones(player: Node3D, stones: Node) -> bool:
|
||||
if stones == null or not stones.found or not _in_reach(player, stones.altar(), 0.5):
|
||||
return false
|
||||
if _online():
|
||||
return _ask("stones", [])
|
||||
return not stones.use(player).is_empty()
|
||||
|
||||
## E on a door an alpha broke: mend it (two wood), shut.
|
||||
func repair_door(player: Node3D, door: Node3D) -> bool:
|
||||
if not is_instance_valid(door) or not door.get("broken") or not _in_reach(player, door, 1.0):
|
||||
@@ -405,6 +414,8 @@ func melee(player: Node3D, target: Node3D) -> bool:
|
||||
return false
|
||||
# Blunt weapons (the club) crack bone: +50% on skeletons.
|
||||
var bone := bool(target.def.get("bone", false)) if target.get("def") != null else false
|
||||
if target.has_method("credit"):
|
||||
target.credit(player)
|
||||
target.take_hit(m.damage * (1.5 if bone and m.get("blunt", false) else 1.0), player.global_position)
|
||||
use_held(player)
|
||||
if target.is_dead():
|
||||
@@ -449,6 +460,8 @@ func projectile_hit(shooter: Node3D, target: Node3D, damage: float, at: Vector3,
|
||||
if not is_instance_valid(target) or not target.is_in_group("creature"):
|
||||
return
|
||||
var was_alive: bool = not target.is_dead()
|
||||
if shooter != null and target.has_method("credit"):
|
||||
target.credit(shooter) # (the Old Stag remembers everyone who hurt it)
|
||||
target.take_hit(damage, shooter.global_position if shooter != null else at, embed_id)
|
||||
if was_alive and target.is_dead():
|
||||
tell(shooter, "Killed a %s" % target.display_name().to_lower())
|
||||
|
||||
@@ -180,8 +180,17 @@ func _desired_speed() -> float:
|
||||
"charge", "flee": return def.run
|
||||
return 0.0
|
||||
|
||||
## (The chest it's going for isn't in its way: it walks right up to it.)
|
||||
## (The chest it's going for isn't in its way: it walks right up to it. Nor
|
||||
## is the person it's charging: it runs into them, not round them.)
|
||||
func _blocked(dir: Vector3) -> bool:
|
||||
if state == "charge" and player != null and global_position.distance_to(player.global_position) < 6.0:
|
||||
var probe := global_position + dir * (1.4 + speed * 0.15)
|
||||
var h: float = terrain.sample_height(probe.x, probe.z)
|
||||
if not terrain.inside_boundary(probe.x, probe.z, 6.0) or h < terrain.water_level + 0.2 or absf(h - global_position.y) > 1.1:
|
||||
return true
|
||||
var q := PhysicsRayQueryParameters3D.create(global_position + Vector3(0, 0.45, 0), Vector3(probe.x, h + 0.45, probe.z), 1)
|
||||
q.exclude = [get_rid(), player.get_rid(), terrain.get_node("TerrainBody").get_rid()]
|
||||
return not get_world_3d().direct_space_state.intersect_ray(q).is_empty()
|
||||
if state == "raid" and food != null and is_instance_valid(food) and global_position.distance_to(food.global_position) < 3.5:
|
||||
var probe := global_position + dir * 1.4
|
||||
var h: float = terrain.sample_height(probe.x, probe.z)
|
||||
|
||||
@@ -63,6 +63,8 @@ func _find_home() -> bool:
|
||||
continue
|
||||
if world.barrow != null and world.barrow.found and Vector2(c.x - world.barrow.position.x, c.z - world.barrow.position.z).length() < 40.0:
|
||||
continue
|
||||
if world.stones != null and world.stones.found and Vector2(c.x - world.stones.position.x, c.z - world.stones.position.z).length() < 42.0:
|
||||
continue
|
||||
if terrain.sample_height(c.x, c.z) < terrain.water_level + 1.0:
|
||||
continue
|
||||
var near := 0
|
||||
|
||||
@@ -60,6 +60,14 @@ const KINDS := {
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
|
||||
"damage": 20.0, "attack_range": 2.0, "attack_cd": 2.0, "bone": true,
|
||||
"drops": {"iron_nugget": [1, 2]}},
|
||||
# P5: the Old Stag (old_stag.gd), called by the standing stones, and the
|
||||
# spirit wolves it summons (they just run at you: the base "charge").
|
||||
"old_stag": {"name": "Old Stag", "hp": 900.0, "walk": 2.0, "run": 13.0, "turn": 3.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 60.0,
|
||||
"damage": 30.0, "attack_range": 2.8, "attack_cd": 1.0, "drops": {}},
|
||||
"spirit_wolf": {"name": "Spirit Wolf", "hp": 40.0, "walk": 1.6, "run": 7.4, "turn": 6.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 60.0,
|
||||
"damage": 8.0, "attack_range": 1.8, "attack_cd": 1.3, "aggro": 9999.0, "drops": {}},
|
||||
# One bear per island, in the deep forest by the rim (bear.gd, bear_range.gd).
|
||||
# Slower than you sprinting, but it hits like a falling tree.
|
||||
"bear": {"name": "Bear", "hp": 220.0, "walk": 1.3, "run": 7.0, "turn": 3.5,
|
||||
@@ -132,7 +140,8 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
var eyes := MeshInstance3D.new()
|
||||
eyes.mesh = parts.eyes
|
||||
var em := StandardMaterial3D.new()
|
||||
var eye: Color = {"alpha": Color(1.0, 0.18, 0.1), "guard": Color(0.45, 0.8, 1.0)}.get(k, Color(1.0, 0.75, 0.3))
|
||||
var eye: Color = {"alpha": Color(1.0, 0.18, 0.1), "guard": Color(0.45, 0.8, 1.0), "old_stag": Color(0.55, 0.95, 1.0),
|
||||
"spirit_wolf": Color(0.55, 0.95, 1.0)}.get(k, Color(1.0, 0.75, 0.3))
|
||||
em.albedo_color = eye.lerp(Color.WHITE, 0.2)
|
||||
em.emission_enabled = true
|
||||
em.emission = eye
|
||||
@@ -157,7 +166,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
cs.position = Vector3(0, parts.height * 0.5, 0)
|
||||
if k != "rabbit" and k != "guard": # (rabbits are round, guards stand upright)
|
||||
# Long bodies: lay the capsule along the spine.
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3}.get(k, 1.4)
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3, "old_stag": 3.3, "spirit_wolf": 1.25}.get(k, 1.4)
|
||||
cs.rotation.x = PI * 0.5
|
||||
cs.position = Vector3(0, parts.height * 0.55, 0)
|
||||
add_child(cs)
|
||||
@@ -411,7 +420,7 @@ func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
|
||||
|
||||
## How a living animal responds to being hit (wolves override this).
|
||||
func _react(from: Vector3) -> void:
|
||||
if kind == "boar" and hp > def.hp * 0.3:
|
||||
if (kind == "boar" and hp > def.hp * 0.3) or kind == "spirit_wolf": # (spirit wolves never run)
|
||||
state = "charge"
|
||||
_aggro = def.aggro
|
||||
_attack_cd = 0.4
|
||||
|
||||
@@ -23,6 +23,8 @@ static func build(kind: String) -> Dictionary:
|
||||
"fox": d = _scaled(_wolf_parts(Color(0.78, 0.4, 0.15), Color(0.92, 0.88, 0.8), 281), 0.6)
|
||||
"crow": d = _crow()
|
||||
"guard": d = _guard()
|
||||
"old_stag": d = _scaled(_stag(), 2.1)
|
||||
"spirit_wolf": d = _wolf_parts(Color(0.62, 0.72, 0.78), Color(0.85, 0.92, 0.95), 351)
|
||||
_: d = _boar()
|
||||
_cache[kind] = d
|
||||
return d
|
||||
@@ -250,3 +252,39 @@ static func _guard() -> Dictionary:
|
||||
return {"body": P._commit(st), "eyes": P._commit(est), "leg": P._commit(lst),
|
||||
"hips": [Vector3(-0.1, 0.93, 0), Vector3(0.1, 0.93, 0)],
|
||||
"radius": 0.3, "height": 1.85}
|
||||
|
||||
## The Old Stag (before scaling up): a pale, moss-hung spirit deer with a
|
||||
## huge crown of antlers.
|
||||
static func _stag() -> Dictionary:
|
||||
var coat := Color(0.72, 0.74, 0.7)
|
||||
var moss := Color(0.32, 0.42, 0.24)
|
||||
var bone := Color(0.9, 0.87, 0.76)
|
||||
var dark := Color(0.15, 0.16, 0.18)
|
||||
var st := P._begin()
|
||||
var r := _rng(341)
|
||||
var n := _noise(341, 1.2)
|
||||
P._blob(st, Vector3(0, 1.0, 0), Vector3(0.3, 0.33, 0.66), n, 0.14, coat, r, 1)
|
||||
P._blob(st, Vector3(0, 1.18, -0.3), Vector3(0.3, 0.28, 0.32), n, 0.3, moss, r, 1) # mossy mane
|
||||
P._cylinder(st, Vector3(0, 1.15, -0.45), Vector3(0, 1.6, -0.68), 0.15, 0.1, 6, coat, r)
|
||||
P._blob(st, Vector3(0, 1.66, -0.8), Vector3(0.12, 0.13, 0.22), n, 0.1, coat, r, 1)
|
||||
P._blob(st, Vector3(0, 1.6, -1.0), Vector3(0.05, 0.05, 0.05), n, 0.0, dark, r, 0)
|
||||
for s in [-1.0, 1.0]:
|
||||
P._cylinder(st, Vector3(s * 0.07, 1.74, -0.74), Vector3(s * 0.2, 1.85, -0.7), 0.04, 0.015, 4, coat, r)
|
||||
# Antlers: a beam with tines, wide and high.
|
||||
var base := Vector3(s * 0.05, 1.78, -0.78)
|
||||
var mid := Vector3(s * 0.32, 2.2, -0.7)
|
||||
var top := Vector3(s * 0.52, 2.6, -0.55)
|
||||
P._cylinder(st, base, mid, 0.035, 0.028, 5, bone, r)
|
||||
P._cylinder(st, mid, top, 0.028, 0.012, 5, bone, r)
|
||||
for t in [[mid, Vector3(s * 0.28, 2.45, -0.95)], [mid, Vector3(s * 0.55, 2.25, -0.85)],
|
||||
[top, Vector3(s * 0.62, 2.85, -0.7)], [top, Vector3(s * 0.75, 2.6, -0.45)], [base.lerp(mid, 0.3), Vector3(s * 0.12, 2.05, -1.05)]]:
|
||||
P._cylinder(st, t[0], t[1], 0.02, 0.006, 4, bone, r)
|
||||
# Moss hanging from the antlers.
|
||||
P._cylinder(st, mid, mid + Vector3(0, -0.25, 0), 0.03, 0.01, 4, moss, r)
|
||||
P._blob(st, Vector3(0, 1.1, 0.64), Vector3(0.06, 0.09, 0.05), n, 0.1, coat, r, 0)
|
||||
var est := P._begin()
|
||||
for s in [-1.0, 1.0]:
|
||||
P._blob(est, Vector3(s * 0.085, 1.7, -0.9), Vector3.ONE * 0.025, n, 0.0, Color(0.6, 0.95, 1.0), r, 0)
|
||||
return {"body": P._commit(st), "eyes": P._commit(est), "leg": _leg(0.9, 0.06, 0.04, coat, dark, 342),
|
||||
"hips": [Vector3(-0.15, 0.9, -0.44), Vector3(0.15, 0.9, -0.44), Vector3(-0.15, 0.92, 0.44), Vector3(0.15, 0.92, 0.44)],
|
||||
"radius": 0.42, "height": 1.8}
|
||||
|
||||
210
scripts/creatures/old_stag.gd
Normal file
210
scripts/creatures/old_stag.gd
Normal file
@@ -0,0 +1,210 @@
|
||||
extends "res://scripts/creatures/creature.gd"
|
||||
## The Old Stag (docs/PROGRESSION_DESIGN.md §6): the island's one boss, called
|
||||
## by the standing stones (standing_stones.gd) at night with three barrow key
|
||||
## pieces. A huge antlered spirit.
|
||||
##
|
||||
## The fight goes round a loop: it circles you at a distance, stops and
|
||||
## lowers its antlers (the warning), then charges in a straight line through
|
||||
## where you were. Step aside and it thunders past; get caught and it hits for
|
||||
## 30. After a charge it stands stunned for a moment: hits then do half as
|
||||
## much again. Three phases by health:
|
||||
## - 1 (above 2/3): circle, charge, stunned 2.5 s.
|
||||
## - 2 (from 2/3): it rears and calls three spirit wolves out of the mist,
|
||||
## then goes on.
|
||||
## - 3 (from 1/3): two more wolves; quicker warnings and charges, a shorter
|
||||
## stun, and if you stay close it stamps the ground (15 to everyone within
|
||||
## 5.5 m).
|
||||
## It doesn't run, isn't afraid of fire, and goes back into the mist at dawn
|
||||
## or if everyone leaves the stones (the stones keep their key pieces).
|
||||
##
|
||||
## With more people at the stones when it's called it's tougher (60% more
|
||||
## health for each extra player; standing_stones.gd scale_for()), and it
|
||||
## remembers everyone who struck it (credit()): all of them are named when
|
||||
## it falls.
|
||||
|
||||
const CIRCLE_R := 14.0
|
||||
const CHARGE_HIT := 3.0 ## caught within this of its path
|
||||
const STUN_DAMAGE := 1.5 ## hits while stunned
|
||||
const STOMP_R := 5.5
|
||||
const STOMP_DAMAGE := 15.0
|
||||
const STOMP_CD := 4.0
|
||||
|
||||
var stones: Node ## standing_stones.gd
|
||||
var phase := 1
|
||||
var max_hp := 900.0 ## (scaled for the group when it's called)
|
||||
var fighters: Dictionary = {} ## who struck it: player id -> name, in order
|
||||
var _target := Vector3.ZERO ## where it's charging to
|
||||
var _dir := Vector3.FORWARD
|
||||
var _hit_this_charge := false
|
||||
var _stomp_cd := 0.0
|
||||
var _circle_a := 0.0
|
||||
var _goal_d := 0.0
|
||||
var charges := 0 ## (tests read these)
|
||||
var charge_hits := 0
|
||||
|
||||
func display_name() -> String:
|
||||
return def.name
|
||||
|
||||
func interact_prompt(_p: Node) -> String:
|
||||
if is_dead():
|
||||
return "The Old Stag (fading)"
|
||||
var tell := {"circle": "circling you", "telegraph": "lowering its antlers!", "charge": "CHARGING!",
|
||||
"stunned": "stunned: strike now!", "summon": "calling its wolves", "stomp": "rearing up!"}
|
||||
return "The Old Stag %d / %d (%s)" % [ceili(hp), int(max_hp), tell.get(state, "")]
|
||||
|
||||
func _pick_target() -> Node3D:
|
||||
return world.nearest_player(global_position)
|
||||
|
||||
func _phase_for(h: float) -> int:
|
||||
if h > max_hp * 2.0 / 3.0:
|
||||
return 1
|
||||
return 2 if h > max_hp / 3.0 else 3
|
||||
|
||||
## Tougher for a bigger group: `players` = how many were at the stones.
|
||||
func scale_for(players: int) -> void:
|
||||
max_hp = float(def.hp) * (1.0 + 0.6 * float(maxi(players, 1) - 1))
|
||||
hp = max_hp
|
||||
|
||||
## `who` struck it (Actions: a blow, an arrow or a thrown spear).
|
||||
func credit(who: Node) -> void:
|
||||
if who == null:
|
||||
return
|
||||
var id := String(who.get("player_id")) if who.get("player_id") != null else "you"
|
||||
if not fighters.has(id):
|
||||
fighters[id] = String(who.get("display")) if who.get("display") != null and not String(who.get("display")).is_empty() else "you"
|
||||
|
||||
## Everyone who struck it, by name, in the order they joined in.
|
||||
func fighter_names() -> Array:
|
||||
return fighters.values()
|
||||
|
||||
func _times() -> Dictionary:
|
||||
if phase >= 3:
|
||||
return {"circle": 2.0, "telegraph": 0.8, "stun": 1.5}
|
||||
return {"circle": 3.0, "telegraph": 1.2, "stun": 2.5}
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
var pp: Vector3 = player.global_position
|
||||
_state_t -= dt
|
||||
_stomp_cd = maxf(_stomp_cd - dt, 0.0)
|
||||
var want := _phase_for(hp)
|
||||
if want > phase and state != "charge":
|
||||
phase = want
|
||||
state = "summon"
|
||||
_state_t = 2.0
|
||||
stones.summon_wolves(self, 3 if phase == 2 else 2)
|
||||
return
|
||||
match state:
|
||||
"summon":
|
||||
_face(pp)
|
||||
if _state_t <= 0.0:
|
||||
_start_circle()
|
||||
"circle":
|
||||
_circle_a += dt * 0.5
|
||||
var c := pp + Vector3(cos(_circle_a), 0, sin(_circle_a)) * CIRCLE_R
|
||||
_go_to(c)
|
||||
if phase >= 3 and global_position.distance_to(pp) < STOMP_R and _stomp_cd <= 0.0:
|
||||
state = "stomp"
|
||||
_state_t = 0.6
|
||||
elif _state_t <= 0.0:
|
||||
state = "telegraph"
|
||||
_state_t = _times().telegraph
|
||||
"telegraph":
|
||||
_face(pp)
|
||||
if _state_t <= 0.0:
|
||||
_dir = Vector3(pp.x - global_position.x, 0, pp.z - global_position.z).normalized()
|
||||
_target = pp + _dir * 7.0
|
||||
_hit_this_charge = false
|
||||
charges += 1
|
||||
state = "charge"
|
||||
_state_t = 4.0
|
||||
"charge":
|
||||
# (Checked every step along the way: anyone near its path is hit.)
|
||||
if not _hit_this_charge:
|
||||
for p in world.players():
|
||||
var d := Vector2(p.global_position.x - global_position.x, p.global_position.z - global_position.z).length()
|
||||
if d < CHARGE_HIT and not p.survival.dead:
|
||||
p.survival.damage(def.damage * world.difficulty_mult("damage"), global_position, "the Old Stag")
|
||||
_hit_this_charge = true
|
||||
charge_hits += 1
|
||||
var left := Vector2(_target.x - global_position.x, _target.z - global_position.z)
|
||||
if left.dot(Vector2(_dir.x, _dir.z)) <= 0.0 or _state_t <= 0.0:
|
||||
state = "stunned"
|
||||
_state_t = _times().stun
|
||||
"stunned":
|
||||
if _state_t <= 0.0:
|
||||
_start_circle()
|
||||
"stomp":
|
||||
_face(pp)
|
||||
if _state_t <= 0.0:
|
||||
_stomp_cd = STOMP_CD
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(global_position) < STOMP_R and not p.survival.dead:
|
||||
p.survival.damage(STOMP_DAMAGE * world.difficulty_mult("damage"), global_position, "the Old Stag")
|
||||
stones.stomped(self)
|
||||
_start_circle()
|
||||
_:
|
||||
_start_circle()
|
||||
|
||||
func _start_circle() -> void:
|
||||
state = "circle"
|
||||
_state_t = _times().circle
|
||||
var away := global_position - player.global_position
|
||||
_circle_a = atan2(away.z, away.x)
|
||||
|
||||
func _face(p: Vector3) -> void:
|
||||
var to := p - global_position
|
||||
target_yaw = atan2(-to.x, -to.z)
|
||||
_goal_d = 0.0
|
||||
|
||||
func _go_to(p: Vector3) -> void:
|
||||
var to := p - global_position
|
||||
to.y = 0.0
|
||||
if to.length() > 0.3:
|
||||
steer(atan2(-to.x, -to.z))
|
||||
_goal_d = to.length()
|
||||
|
||||
func _desired_speed() -> float:
|
||||
match state:
|
||||
"circle": return 0.0 if _goal_d < 1.0 else 6.5
|
||||
"charge": return def.run
|
||||
return 0.0
|
||||
|
||||
## A charge is a straight line: over the ground, through bushes and saplings
|
||||
## (it only stops for the edge of the valley and the water).
|
||||
func _move(dt: float) -> void:
|
||||
if state != "charge":
|
||||
super._move(dt)
|
||||
return
|
||||
speed = move_toward(speed, def.run, dt * def.run * 4.0)
|
||||
yaw = rotate_toward(yaw, atan2(-_dir.x, -_dir.z), 8.0 * dt)
|
||||
var p := global_position + _dir * speed * dt
|
||||
if not terrain.inside_boundary(p.x, p.z, 0.5) or terrain.sample_height(p.x, p.z) < terrain.water_level + 0.2:
|
||||
state = "stunned" # (pulled up short at the edge)
|
||||
_state_t = _times().stun
|
||||
return
|
||||
p.y = terrain.sample_height(p.x, p.z)
|
||||
global_position = p
|
||||
rotation.y = yaw
|
||||
|
||||
## Rearing for the summon and the stomp; head down for the warning.
|
||||
func _animate(dt: float) -> void:
|
||||
if state in ["summon", "stomp"]:
|
||||
_visual.rotation.x = lerpf(_visual.rotation.x, 0.5, minf(dt * 5.0, 1.0))
|
||||
return
|
||||
if state == "telegraph":
|
||||
_visual.rotation.x = lerpf(_visual.rotation.x, -0.25, minf(dt * 5.0, 1.0))
|
||||
return
|
||||
super._animate(dt)
|
||||
|
||||
func take_hit(damage: float, from: Vector3, embed_id: String = "") -> void:
|
||||
super.take_hit(damage * (STUN_DAMAGE if state == "stunned" and not puppet else 1.0), from, embed_id)
|
||||
|
||||
## It doesn't flinch or run; it just keeps coming.
|
||||
func _react(_from: Vector3) -> void:
|
||||
pass
|
||||
|
||||
func _die(from: Vector3) -> void:
|
||||
super._die(from)
|
||||
carcass_left = 4.0 # (it fades back into the mist)
|
||||
if not puppet and stones != null:
|
||||
stones.stag_died(self)
|
||||
@@ -119,6 +119,9 @@ static func build(key: String) -> ArrayMesh:
|
||||
"rotten_food": m = _pebble(321, Color(0.33, 0.3, 0.16), Vector3(0.11, 0.06, 0.1), 0.45)
|
||||
"salt": m = _salt()
|
||||
"smoking_rack": m = _smoking_rack()
|
||||
"stag_antler": m = _antler()
|
||||
"stag_trophy": m = _stag_trophy()
|
||||
"antler_pickaxe": m = _antler_pickaxe()
|
||||
"smoked_meat": m = _meat(Color(0.36, 0.17, 0.1))
|
||||
"salted_meat": m = _meat(Color(0.68, 0.42, 0.38))
|
||||
"smoked_fish": m = _fish(Color(0.72, 0.5, 0.22), 0.2, true)
|
||||
@@ -1062,3 +1065,48 @@ static func _smoking_rack() -> ArrayMesh:
|
||||
var d := Vector3(cos(a), 0, sin(a)) * 0.18
|
||||
P._cylinder(st, Vector3(0, 0.06, 0) - d, Vector3(0, 0.06, 0) + d, 0.035, 0.035, 5, P.BARK, r, -1.0, CHARCOAL)
|
||||
return P._commit(st)
|
||||
|
||||
# --- P5 the Old Stag ---------------------------------------------------------------------
|
||||
|
||||
const ANTLER := Color(0.9, 0.87, 0.76)
|
||||
|
||||
## One branching antler lying on its side.
|
||||
static func _antler_into(st: SurfaceTool, r: RandomNumberGenerator, o: Vector3, k: float, up: Vector3 = Vector3.UP) -> void:
|
||||
var side := up.cross(Vector3.FORWARD).normalized() if absf(up.dot(Vector3.FORWARD)) < 0.9 else Vector3.RIGHT
|
||||
var a := o
|
||||
var b := o + up * 0.35 * k + side * 0.12 * k
|
||||
var c := b + up * 0.3 * k + side * 0.18 * k
|
||||
P._cylinder(st, a, b, 0.035 * k, 0.028 * k, 5, ANTLER, r)
|
||||
P._cylinder(st, b, c, 0.028 * k, 0.01 * k, 5, ANTLER, r)
|
||||
for t in [[a.lerp(b, 0.4), Vector3(0, 0.15, -0.12)], [b, Vector3(0, 0.2, -0.15)], [b.lerp(c, 0.6), Vector3(0, 0.12, 0.14)]]:
|
||||
P._cylinder(st, t[0], (t[0] as Vector3) + (up * (t[1] as Vector3).y + Vector3(0, 0, (t[1] as Vector3).z)) * k * 1.4, 0.018 * k, 0.005 * k, 4, ANTLER, r)
|
||||
|
||||
static func _antler() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
_antler_into(st, _rng(361), Vector3(-0.25, 0.04, 0), 1.0, Vector3(1, 0.12, 0).normalized())
|
||||
return P._commit(st)
|
||||
|
||||
## The skull and antlers on a wooden stand.
|
||||
static func _stag_trophy() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(363)
|
||||
var n := FastNoiseLite.new()
|
||||
n.seed = 363
|
||||
_box(st, Vector3(0, 0.08, 0), Vector3(1.2, 0.16, 0.7), P._mul(BEAM, 0.9))
|
||||
_box(st, Vector3(0, 0.6, -0.15), Vector3(0.16, 0.9, 0.16), BEAM)
|
||||
P._blob(st, Vector3(0, 1.12, -0.1), Vector3(0.16, 0.16, 0.26), n, 0.1, Color(0.88, 0.85, 0.75), r, 1)
|
||||
P._blob(st, Vector3(0, 1.08, 0.12), Vector3(0.07, 0.06, 0.08), n, 0.0, Color(0.2, 0.2, 0.2), r, 0)
|
||||
for s in [-1.0, 1.0]:
|
||||
_antler_into(st, r, Vector3(s * 0.08, 1.25, -0.12), 1.6, Vector3(s * 0.7, 1.0, 0).normalized())
|
||||
P._blob(st, Vector3(s * 0.07, 1.16, 0.08), Vector3.ONE * 0.025, n, 0.0, Color(0.55, 0.9, 1.0), r, 0)
|
||||
return P._commit(st)
|
||||
|
||||
static func _antler_pickaxe() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(365)
|
||||
_handle(st, 0.62, r)
|
||||
_box(st, Vector3(0, 0.57, 0), Vector3(0.06, 0.08, 0.06), IRON)
|
||||
P._cylinder(st, Vector3(-0.02, 0.58, 0), Vector3(0.27, 0.5, 0), 0.032, 0.004, 5, ANTLER, r, -1.0, ANTLER)
|
||||
P._cylinder(st, Vector3(0.02, 0.58, 0), Vector3(-0.24, 0.53, 0), 0.032, 0.004, 5, ANTLER, r, -1.0, ANTLER)
|
||||
P._cylinder(st, Vector3(0.1, 0.56, 0), Vector3(0.14, 0.66, 0), 0.012, 0.003, 4, ANTLER, r)
|
||||
return P._commit(st)
|
||||
|
||||
@@ -20,7 +20,8 @@ extends Node
|
||||
const Creature = preload("res://scripts/creatures/creature.gd")
|
||||
const BearScript = preload("res://scripts/creatures/bear.gd")
|
||||
const CrowScript = preload("res://scripts/creatures/crow.gd")
|
||||
const GuardScript = preload("res://scripts/creatures/guard.gd") # (falls apart in its own way)
|
||||
const GuardScript = preload("res://scripts/creatures/guard.gd")
|
||||
const StagScript = preload("res://scripts/creatures/old_stag.gd") # (rears and lowers its antlers) # (falls apart in its own way)
|
||||
const Bobber = preload("res://scripts/fishing/bobber.gd")
|
||||
const Projectile = preload("res://scripts/creatures/projectile.gd")
|
||||
const WolfPack = preload("res://scripts/creatures/wolf_pack.gd")
|
||||
@@ -98,9 +99,9 @@ func _server_tick(w: Node3D, delta: float) -> void:
|
||||
|
||||
const PER_PACKET := 40
|
||||
const ENTRY := 23
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard"]
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf"]
|
||||
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash",
|
||||
"warn", "raid", "eat", "fly", "fly_off", "peck", "sneak", "carry"]
|
||||
"warn", "raid", "eat", "fly", "fly_off", "peck", "sneak", "carry", "circle", "telegraph", "stunned", "summon", "stomp"]
|
||||
|
||||
## One creature in 23 bytes: id, kind, state, position, facing, speed, health.
|
||||
static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void:
|
||||
@@ -113,13 +114,13 @@ static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void:
|
||||
buf.encode_float(o + 14, p.z)
|
||||
buf.encode_u16(o + 18, int(fposmod(c.yaw, TAU) / TAU * 65535.0))
|
||||
buf.encode_u8(o + 20, clampi(int(c.speed * 20.0), 0, 255))
|
||||
buf.encode_u16(o + 21, clampi(int(c.hp * 100.0), 0, 65535))
|
||||
buf.encode_u16(o + 21, clampi(roundi(c.hp * 10.0), 0, 65535)) # (tenths: up to 6553, the Old Stag has 900)
|
||||
|
||||
static func _decode(buf: PackedByteArray, o: int) -> Array:
|
||||
return [buf.decode_u32(o), KIND_IDS[mini(buf.decode_u8(o + 4), KIND_IDS.size() - 1)],
|
||||
Vector3(buf.decode_float(o + 6), buf.decode_float(o + 10), buf.decode_float(o + 14)),
|
||||
buf.decode_u16(o + 18) / 65535.0 * TAU, STATE_IDS[mini(buf.decode_u8(o + 5), STATE_IDS.size() - 1)],
|
||||
buf.decode_u8(o + 20) / 20.0, buf.decode_u16(o + 21) / 100.0]
|
||||
buf.decode_u8(o + 20) / 20.0, buf.decode_u16(o + 21) / 10.0]
|
||||
|
||||
func _on_launched(id: String, origin: Vector3, vel: Vector3, shooter: Node3D) -> void:
|
||||
var by := int(shooter.peer) if shooter != null and is_instance_valid(shooter) and shooter.get("peer") != null else 0
|
||||
@@ -197,7 +198,7 @@ func _s2c_life(packed: PackedByteArray, fl: Array) -> void:
|
||||
var c: Node3D = got if is_instance_valid(got) else null
|
||||
if c == null:
|
||||
# (Kinds with their own look: the bear rears up, a crow flaps.)
|
||||
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript}.get(kind, Creature)
|
||||
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript, "old_stag": StagScript}.get(kind, Creature)
|
||||
c = script.new()
|
||||
c.puppet = true
|
||||
c.setup(kind, world, e[2], cid)
|
||||
|
||||
@@ -32,7 +32,7 @@ signal clock(day: int, time: float) ## the server's time of day
|
||||
const Identity = preload("res://scripts/net/identity.gd")
|
||||
const ServerConfig = preload("res://scripts/net/server_config.gd")
|
||||
|
||||
const PROTOCOL := 11 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3); 11: food spoilage, lightning (P4)
|
||||
const PROTOCOL := 12 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3); 11: food spoilage, lightning (P4); 12: the standing stones and the Old Stag (P5)
|
||||
const DEFAULT_PORT := 24242
|
||||
const HANDSHAKE_TIMEOUT := 10.0
|
||||
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
||||
|
||||
@@ -324,6 +324,9 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
||||
if d != null and d.get("open") != null and _allowed(Rules.use_problem(p.player_id, d)) \
|
||||
and Actions.toggle_door(p, d):
|
||||
WorldNet.piece_state(d)
|
||||
"stones":
|
||||
if w.stones != null:
|
||||
Actions.use_stones(p, w.stones)
|
||||
"barrow_loot":
|
||||
if w.barrow != null:
|
||||
Actions.loot_barrow(p, w.barrow)
|
||||
@@ -429,8 +432,19 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
||||
for m in w.get_tree().get_nodes_in_group("processor"):
|
||||
m.step(float(a[0]) if a.size() > 0 and (a[0] is float or a[0] is int) else 60.0)
|
||||
"debug_spawn":
|
||||
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard"]) and _v(a, 1).is_finite():
|
||||
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard", "old_stag", "spirit_wolf"]) and _v(a, 1).is_finite():
|
||||
w.creatures.spawn(_s(a, 0), _v(a, 1))
|
||||
"debug_boss":
|
||||
# Tests: ["hold", secs] stands the Old Stag still (stunned);
|
||||
# ["hp", n] sets its health.
|
||||
var s: Node3D = w.stones.stag if w.stones != null and w.stones.fighting() else null
|
||||
if cheats and s != null:
|
||||
match _s(a, 0):
|
||||
"hold":
|
||||
s.state = "stunned"
|
||||
s._state_t = float(a[1]) if a.size() > 1 else 30.0
|
||||
"hp":
|
||||
s.hp = clampf(float(a[1]) if a.size() > 1 else 1.0, 1.0, s.max_hp)
|
||||
"debug_time":
|
||||
if cheats:
|
||||
w.day_night.day = maxi(_i(a, 0), 1)
|
||||
|
||||
@@ -365,6 +365,17 @@ func _s2c_node(uid: String, hp: float, point: Vector3, from: Vector3) -> void:
|
||||
n.apply_remote(hp, point, from)
|
||||
received += 1
|
||||
|
||||
## The standing stones' altar changed (key pieces set, the stag beaten).
|
||||
func stones_state(d: Dictionary) -> void:
|
||||
_all("_s2c_stones", [d])
|
||||
|
||||
@rpc("authority", "call_remote", "reliable")
|
||||
func _s2c_stones(d: Dictionary) -> void:
|
||||
if not is_client() or world.stones == null:
|
||||
return
|
||||
world.stones.load_data(d)
|
||||
received += 1
|
||||
|
||||
## Lightning struck a tree (hazards.gd, P4): everyone sees it burn. (It
|
||||
## falls later through _s2c_node, like any felled tree.)
|
||||
func struck(n: Node3D) -> void:
|
||||
|
||||
@@ -65,6 +65,7 @@ func place(w: Node3D) -> bool:
|
||||
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)
|
||||
w.props.clear_bushes(position, CLEAR_R)
|
||||
_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
|
||||
@@ -364,6 +365,7 @@ func _spawn_guards() -> void:
|
||||
g.rotation.y = g.yaw
|
||||
g.set_meta("post", i)
|
||||
g.died.connect(_on_guard_died)
|
||||
g.top_level = true # (world space: the barrow is turned, and a creature's facing is its own rotation)
|
||||
add_child(g)
|
||||
g.global_position = g.post
|
||||
guards.append(g)
|
||||
|
||||
@@ -545,6 +545,25 @@ func _scatter_bushes() -> void:
|
||||
_group(groups, p.x, p.y, vi, Transform3D(_ground_basis(n, 0.5, yaw, s), Vector3(p.x, h - 0.1, p.y)))
|
||||
_flush(groups, meshes, true, "Bushes")
|
||||
|
||||
## Hides the decorative bushes within `r` of `center` (the barrow's and the
|
||||
## standing stones' clearings: they're placed after the undergrowth).
|
||||
## Deterministic, like the rest of the scenery.
|
||||
func clear_bushes(center: Vector3, r: float) -> int:
|
||||
var root := get_node_or_null("Bushes")
|
||||
if root == null:
|
||||
return 0
|
||||
var n := 0
|
||||
for mmi in root.get_children():
|
||||
if not mmi is MultiMeshInstance3D:
|
||||
continue
|
||||
var mm: MultiMesh = mmi.multimesh
|
||||
for i in range(mm.instance_count):
|
||||
var xf: Transform3D = mmi.global_transform * mm.get_instance_transform(i)
|
||||
if Vector2(xf.origin.x - center.x, xf.origin.z - center.z).length() < r:
|
||||
mm.set_instance_transform(i, Transform3D(xf.basis, xf.origin + Vector3(0, -1000, 0)))
|
||||
n += 1
|
||||
return n
|
||||
|
||||
func _place_logs() -> void:
|
||||
var root := Node3D.new()
|
||||
root.name = "Logs"
|
||||
|
||||
393
scripts/world/standing_stones.gd
Normal file
393
scripts/world/standing_stones.gd
Normal file
@@ -0,0 +1,393 @@
|
||||
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)
|
||||
@@ -67,6 +67,7 @@ var wolves: Node3D ## wolf_pack.gd (null if this seed has no den si
|
||||
var bears: Node3D ## bear_range.gd: the island's bear (null online as a client)
|
||||
var thieves: Node3D ## thieves.gd: crows and foxes after food left out
|
||||
var hazards: Node3D ## hazards.gd: lightning on trees, hard-winter ice (P4)
|
||||
var stones: Node3D ## standing_stones.gd: the altar and the Old Stag (P5)
|
||||
var barrow: Node3D ## barrow.gd: the burial mound and its guards (scenery; guards where creatures run)
|
||||
var _saved_bear: Dictionary = {} ## its saved state, kept until bears exists (and re-saved if it never does)
|
||||
var net_sync: Node ## net_sync.gd when playing on a server, else null
|
||||
@@ -352,6 +353,12 @@ func _on_terrain_ready() -> void:
|
||||
add_child(barrow)
|
||||
if barrow.place(self):
|
||||
_update_grass_cuts()
|
||||
# The standing stones (P5): where the Old Stag is called.
|
||||
stones = preload("res://scripts/world/standing_stones.gd").new()
|
||||
stones.name = "StandingStones"
|
||||
add_child(stones)
|
||||
if stones.place(self):
|
||||
_update_grass_cuts()
|
||||
if DisplayServer.get_name() != "headless":
|
||||
var fish := preload("res://scripts/fishing/lake_fish.gd").new()
|
||||
fish.name = "LakeFish"
|
||||
@@ -366,6 +373,7 @@ func _on_terrain_ready() -> void:
|
||||
hazards.name = "Hazards"
|
||||
add_child(hazards)
|
||||
hazards.setup(self, not is_online())
|
||||
stones.deciding = not is_online()
|
||||
if server_mode:
|
||||
if Saves.has_world():
|
||||
var saved: Dictionary = Saves.load_state(Saves.current_id)
|
||||
@@ -601,6 +609,7 @@ func collect_save() -> Dictionary:
|
||||
"pieces": pieces,
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
"barrow": barrow.save_data() if barrow != null else {},
|
||||
"stones": stones.save_data() if stones != null else {},
|
||||
}
|
||||
|
||||
func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, dropped: Array, pieces: Array) -> Dictionary:
|
||||
@@ -614,6 +623,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
|
||||
"pieces": pieces,
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
"barrow": barrow.save_data() if barrow != null else {},
|
||||
"stones": stones.save_data() if stones != null else {},
|
||||
}
|
||||
|
||||
## The live pieces (placed and not being removed).
|
||||
@@ -742,6 +752,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
|
||||
props.restore_regrow(s.get("regrow", {}))
|
||||
if barrow != null:
|
||||
barrow.load_data(s.get("barrow", {}))
|
||||
if stones != null:
|
||||
stones.load_data(s.get("stones", {}))
|
||||
# The bear (bear_range.gd): applied when it's set up, after loading.
|
||||
_saved_bear = s.get("bear", {})
|
||||
if bears != null:
|
||||
|
||||
Reference in New Issue
Block a user