P3 alpha: wolf moon every 7th night, the alpha breaks wooden doors, iron-banded door, broken-door repair, alpha pelt and cloak (protocol 7)

This commit is contained in:
Paul Bailey
2026-11-05 01:40:52 +00:00
parent 411bfef34a
commit fc58c8d0e1
18 changed files with 742 additions and 40 deletions

View File

@@ -895,6 +895,48 @@
"piece": "door", "piece": "door",
"kind": "door" "kind": "door"
} }
},
{
"id": "iron_door",
"name": "Iron-banded Door",
"category": "building",
"stack": 10,
"description": "A heavy door bound with iron. Nothing on the island can break it down.",
"mesh": "iron_door",
"place": {
"piece": "door",
"kind": "door",
"sturdy": true,
"panel": "iron_door_panel"
}
},
{
"id": "alpha_pelt",
"name": "Alpha Pelt",
"category": "resource",
"stack": 5,
"description": "The alpha's black fur, thick as a blanket.",
"mesh": "alpha_pelt"
},
{
"id": "wolf_fang",
"name": "Wolf Fang",
"category": "resource",
"stack": 20,
"description": "A long yellowed fang from the alpha. A trophy, and a fine clasp.",
"mesh": "wolf_fang"
},
{
"id": "alpha_cloak",
"name": "Alpha Cloak",
"category": "tool",
"stack": 1,
"description": "Right-click to wear it (+35 warmth, rain barely touches you). Take it off from the inventory screen.",
"mesh": "alpha_cloak",
"wear": {
"warmth": 35,
"wet_chill": 0.25
}
} }
] ]
} }

View File

@@ -496,6 +496,31 @@
"wood": 6, "wood": 6,
"nails": 4 "nails": 4
} }
},
{
"id": "iron_door",
"output": "iron_door",
"count": 1,
"station": "forge",
"category": "building",
"inputs": {
"wood": 4,
"iron_bar": 2,
"nails": 4
}
},
{
"id": "alpha_cloak",
"output": "alpha_cloak",
"count": 1,
"station": "workbench",
"category": "clothing",
"inputs": {
"alpha_pelt": 1,
"wolf_pelt": 2,
"wolf_fang": 1,
"fiber": 4
}
} }
] ]
} }

View File

@@ -1,6 +1,7 @@
# Progression and challenges design # Progression and challenges design
Status: **P1 (iron) and P2 (stone) built**; P3-P5 are proposals. Status: **P1 (iron) and P2 (stone) built; P3 started (the alpha)**; the
rest of P3, and P4-P5, are proposals.
*As built in P1:* *As built in P1:*
- Iron nuggets (70, in the scree under the rim), clay (60, on the lake - Iron nuggets (70, in the scree under the rim), clay (60, on the lake
@@ -37,6 +38,19 @@ Status: **P1 (iron) and P2 (stone) built**; P3-P5 are proposals.
windows still counts. windows still counts.
- Palisade: 3 m tall, blocks like a wall online too. No fences. - Palisade: 3 m tall, blocks like a wall online too. No fences.
*As built in P3 so far (the alpha):*
- Wolf moon: nights 7, 14, 21... (worked out from the day, so every game on
a server agrees). The moon turns red and a toast warns you.
- The alpha (135 HP, bites for 16) comes out of the den with the pack and
leaves at dawn. It isn't afraid of fire and never runs. If it can't reach
you it batters the nearest shut door near you: a wooden door (or the
wooden leaf in a stone doorway or palisade gate) gives way after about
five blows and hangs broken until mended (E, 2 wood); then it comes in.
- Iron-banded door (forge: 2 iron, 4 wood, 4 nails) can't be broken.
- Drops: alpha pelt, 1-2 fangs, meat. Alpha cloak (+35 warmth) at the
workbench. Network protocol 7.
- Not yet: the bear, crows and foxes, the barrow and its guards.
Today the island runs out of goals after a few days: once you have stone Today the island runs out of goals after a few days: once you have stone
tools, a hut, a bow and a fire, the only threats are wolves at night, the tools, a hut, a bow and a fire, the only threats are wolves at night, the
cold and your diet. This plan adds a **second tier of materials** (iron and cold and your diet. This plan adds a **second tier of materials** (iron and

View File

@@ -39,6 +39,7 @@ Detailed plan in `docs/SURVIVAL_DESIGN.md` (phases M1a–M1d).
- [x] Wild flax (fibre plants) *(polish)* - [x] Wild flax (fibre plants) *(polish)*
- [x] Iron: nuggets, ore, clay, kiln, smelter, forge, iron tools and arrows, lantern, tool wear and repair *(P1)* - [x] Iron: nuggets, ore, clay, kiln, smelter, forge, iron tools and arrows, lantern, tool wear and repair *(P1)*
- [x] Stone building: stonecutter, cut stone, stone floors, walls, pillars, stairs, windows, doorways, hearth; wooden stairs, windows, posts, palisade and gate *(P2)* - [x] Stone building: stonecutter, cut stone, stone floors, walls, pillars, stairs, windows, doorways, hearth; wooden stairs, windows, posts, palisade and gate *(P2)*
- [x] The wolf moon and the alpha: breaks wooden doors, iron-banded doors hold, alpha pelt and cloak *(P3, part 1)*
- [ ] **Iron, stone building and new adversaries**: iron nuggets and ore, - [ ] **Iron, stone building and new adversaries**: iron nuggets and ore,
smelting and a forge, iron tools and durability, cut-stone building, smelting and a forge, iron tools and durability, cut-stone building,
an alpha wolf, a bear, a barrow with guards, spoilage and blizzards an alpha wolf, a bear, a barrow with guards, spoilage and blizzards

View File

@@ -1,17 +1,28 @@
extends "res://scripts/building/build_piece.gd" extends "res://scripts/building/build_piece.gd"
## Wooden door: a wall-sized frame (snaps like a wall) with a hinged panel. ## Door: a wall-sized frame (snaps like a wall) with a hinged panel. E opens
## E opens or closes it. The panel is an AnimatableBody3D so it pushes the ## or closes it. The panel is an AnimatableBody3D so it pushes the player
## player out of the way rather than trapping them. ## out of the way rather than trapping them.
##
## An alpha wolf can break a closed wooden door (bash()): after DOOR_HP worth
## of blows the panel gives way and hangs open, and the door shows as broken
## until you repair it (E, two wood). An iron-banded door (items.json
## place.sturdy) can't be broken. The frame stays either way, so whatever it
## holds up keeps standing.
const OPEN_ANGLE := 1.75 ## radians: swings toward -Z, away from where you stood to place it const OPEN_ANGLE := 1.75 ## radians: swings toward -Z, away from where you stood to place it
const DOOR_HP := 100.0
const REPAIR_COST := {"wood": 2}
var open := false var open := false
var broken := false
var door_hp := DOOR_HP
var _hinge: Node3D var _hinge: Node3D
var _panel: AnimatableBody3D var _panel: AnimatableBody3D
var _panel_shape: CollisionShape3D
var _tween: Tween var _tween: Tween
func _visual_mesh() -> Mesh: func _visual_mesh() -> Mesh:
return ItemDB.mesh_for(String(def.get("mesh", item_id)) + "_frame") # (wood_door_frame, stone_door_frame) return ItemDB.mesh_for(String(def.get("mesh", item_id)) + "_frame") # (wood_door_frame, stone_doorway_frame...)
func _build() -> void: func _build() -> void:
_hinge = Node3D.new() _hinge = Node3D.new()
@@ -27,23 +38,38 @@ func _build() -> void:
_panel.set_meta("owner_piece", self) _panel.set_meta("owner_piece", self)
_hinge.add_child(_panel) _hinge.add_child(_panel)
var mi := MeshInstance3D.new() var mi := MeshInstance3D.new()
mi.mesh = ItemDB.mesh_for("wood_door_panel") mi.mesh = ItemDB.mesh_for(String(def.place.get("panel", "wood_door_panel")))
mi.material_override = ItemDB.item_material() mi.material_override = ItemDB.item_material()
_panel.add_child(mi) _panel.add_child(mi)
var cs := CollisionShape3D.new() _panel_shape = CollisionShape3D.new()
var box := BoxShape3D.new() var box := BoxShape3D.new()
box.size = Vector3(1.06, 1.86, 0.08) box.size = Vector3(1.06, 1.86, 0.08)
cs.shape = box _panel_shape.shape = box
cs.position = Vector3(0.54, 0.95, 0.0) _panel_shape.position = Vector3(0.54, 0.95, 0.0)
_panel.add_child(cs) _panel.add_child(_panel_shape)
## Iron-banded: nothing breaks it.
func sturdy() -> bool:
return bool(def.place.get("sturdy", false))
## Shut and in one piece (something trying to get in has to get past it).
func blocks() -> bool:
return not open and not broken
func interact_prompt(_player: Node) -> String: func interact_prompt(_player: Node) -> String:
if broken:
return "Broken door [E] Repair (2 wood)"
return "Door [E] %s" % ("Close" if open else "Open") return "Door [E] %s" % ("Close" if open else "Open")
func interact(player: Node) -> void: func interact(player: Node) -> void:
if broken:
Actions.repair_door(player, self)
else:
Actions.toggle_door(player, self) Actions.toggle_door(player, self)
func set_open(on: bool, animate: bool = true) -> void: func set_open(on: bool, animate: bool = true) -> void:
if broken:
on = true # (it hangs open off one hinge)
open = on open = on
var target := OPEN_ANGLE if on else 0.0 var target := OPEN_ANGLE if on else 0.0
if _tween != null: if _tween != null:
@@ -54,8 +80,46 @@ func set_open(on: bool, animate: bool = true) -> void:
else: else:
_hinge.rotation.y = target _hinge.rotation.y = target
## A blow from something trying to get in. Returns "hit", "held" (iron
## bands), "broke" (this blow broke it) or "" (open or already broken).
func bash(damage: float) -> String:
if not blocks():
return ""
if sturdy():
return "held"
door_hp -= damage
if door_hp > 0.0:
return "hit"
set_broken(true)
return "broke"
## Broken: the panel hangs open, askew, and stops anything. Repaired: shut
## and whole again.
func set_broken(on: bool) -> void:
broken = on
if on:
door_hp = 0.0
set_open(true, false)
_hinge.rotation = Vector3(0.0, OPEN_ANGLE + 0.35, 0.18)
_panel_shape.disabled = true
else:
door_hp = DOOR_HP
_hinge.rotation = Vector3.ZERO
_panel_shape.disabled = false
set_open(false, false)
func save_extra() -> Dictionary: func save_extra() -> Dictionary:
return {"open": open} return {"open": open, "broken": broken}
func load_extra(d: Dictionary) -> void: func load_extra(d: Dictionary) -> void:
set_broken(bool(d.get("broken", false)))
if not broken:
set_open(bool(d.get("open", false)), false) set_open(bool(d.get("open", false)), false)
## A server telling us this door's state: swing it if it only opened or
## closed, snap it if it broke or was mended.
func net_state(d: Dictionary) -> void:
if bool(d.get("broken", false)) != broken:
load_extra(d)
elif not broken:
set_open(bool(d.get("open", false)))

View File

@@ -305,6 +305,22 @@ func toggle_door(player: Node3D, door: Node3D) -> bool:
door.set_open(not door.open) door.set_open(not door.open)
return true return true
## 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):
return false
if _online():
return _ask("door_repair", [WorldNet.nid(door)])
for id in door.REPAIR_COST:
if player.inventory.count(id) < int(door.REPAIR_COST[id]):
tell(player, "Need %d %s to mend the door" % [int(door.REPAIR_COST[id]), ItemDB.item_name(id).to_lower()])
return false
for id in door.REPAIR_COST:
player.inventory.remove(id, int(door.REPAIR_COST[id]))
door.set_broken(false)
tell(player, "Door mended")
return true
## E on a bed: claim it as your spawn point, or (if it's already yours) sleep. ## E on a bed: claim it as your spawn point, or (if it's already yours) sleep.
func use_bed(player: Node3D, bed: Node3D) -> bool: func use_bed(player: Node3D, bed: Node3D) -> bool:
if not is_instance_valid(bed) or not _in_reach(player, bed, 1.0): if not is_instance_valid(bed) or not _in_reach(player, bed, 1.0):

View File

@@ -39,6 +39,13 @@ const KINDS := {
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0, "notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
"damage": 9.0, "attack_range": 1.8, "attack_cd": 1.2, "damage": 9.0, "attack_range": 1.8, "attack_cd": 1.2,
"drops": {"raw_meat": [1, 2], "wolf_pelt": [1, 1]}}, "drops": {"raw_meat": [1, 2], "wolf_pelt": [1, 1]}},
# The alpha: leads the pack on a wolf moon (wolf_pack.gd). Three wolves'
# worth of health, bites harder, isn't afraid of fire and breaks down
# wooden doors (wolf.gd).
"alpha": {"name": "Alpha Wolf", "hp": 135.0, "walk": 1.6, "run": 8.2, "turn": 5.0,
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
"damage": 16.0, "attack_range": 2.1, "attack_cd": 1.4, "door_damage": 22.0,
"drops": {"raw_meat": [2, 3], "alpha_pelt": [1, 1], "wolf_fang": [1, 2]}},
} }
const CARCASS_SECONDS := 300.0 const CARCASS_SECONDS := 300.0
@@ -105,9 +112,10 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
var eyes := MeshInstance3D.new() var eyes := MeshInstance3D.new()
eyes.mesh = parts.eyes eyes.mesh = parts.eyes
var em := StandardMaterial3D.new() var em := StandardMaterial3D.new()
em.albedo_color = Color(1.0, 0.85, 0.4) var red := k == "alpha"
em.albedo_color = Color(1.0, 0.3, 0.2) if red else Color(1.0, 0.85, 0.4)
em.emission_enabled = true em.emission_enabled = true
em.emission = Color(1.0, 0.75, 0.3) em.emission = Color(1.0, 0.18, 0.1) if red else Color(1.0, 0.75, 0.3)
em.emission_energy_multiplier = 2.5 em.emission_energy_multiplier = 2.5
eyes.material_override = em eyes.material_override = em
eyes.set_meta("keep_mat", true) eyes.set_meta("keep_mat", true)
@@ -129,7 +137,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
cs.position = Vector3(0, parts.height * 0.5, 0) cs.position = Vector3(0, parts.height * 0.5, 0)
if k != "rabbit": if k != "rabbit":
# Long bodies: lay the capsule along the spine. # Long bodies: lay the capsule along the spine.
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25}.get(k, 1.4) cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65}.get(k, 1.4)
cs.rotation.x = PI * 0.5 cs.rotation.x = PI * 0.5
cs.position = Vector3(0, parts.height * 0.55, 0) cs.position = Vector3(0, parts.height * 0.55, 0)
add_child(cs) add_child(cs)

View File

@@ -18,6 +18,7 @@ static func build(kind: String) -> Dictionary:
"rabbit": d = _rabbit() "rabbit": d = _rabbit()
"deer": d = _deer() "deer": d = _deer()
"wolf": d = _wolf() "wolf": d = _wolf()
"alpha": d = _wolf(true)
_: d = _boar() _: d = _boar()
_cache[kind] = d _cache[kind] = d
return d return d
@@ -100,13 +101,42 @@ static func _boar() -> Dictionary:
## Grey wolf: lean body, long snout, pointed ears, bushy tail. "eyes" is a ## Grey wolf: lean body, long snout, pointed ears, bushy tail. "eyes" is a
## separate mesh so it can glow (creature.gd gives it an emissive material). ## separate mesh so it can glow (creature.gd gives it an emissive material).
static func _wolf() -> Dictionary: ## `alpha`: a third bigger, near-black with a grizzled ruff and a scarred
var coat := Color(0.42, 0.42, 0.44) ## muzzle (wolf moon nights).
var pale := Color(0.72, 0.71, 0.68) static func _wolf(alpha: bool = false) -> Dictionary:
if alpha:
return _scaled(_wolf_parts(Color(0.17, 0.16, 0.17), Color(0.5, 0.48, 0.45), 241), 1.32)
return _wolf_parts(Color(0.42, 0.42, 0.44), Color(0.72, 0.71, 0.68), 231)
## The same parts scaled up `k` times (body, eyes and leg meshes, hips, radius, height).
static func _scaled(d: Dictionary, k: float) -> Dictionary:
var out := d.duplicate()
for key in ["body", "eyes", "leg"]:
if d.has(key):
out[key] = _scale_mesh(d[key], k)
var hips: Array = []
for h: Vector3 in d.hips:
hips.append(h * k)
out.hips = hips
out.radius = float(d.radius) * k
out.height = float(d.height) * k
return out
static func _scale_mesh(m: ArrayMesh, k: float) -> ArrayMesh:
var arrays := m.surface_get_arrays(0)
var v: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
for i in range(v.size()):
v[i] *= k
arrays[Mesh.ARRAY_VERTEX] = v
var out := ArrayMesh.new()
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
return out
static func _wolf_parts(coat: Color, pale: Color, seed: int) -> Dictionary:
var dark := Color(0.16, 0.15, 0.15) var dark := Color(0.16, 0.15, 0.15)
var st := P._begin() var st := P._begin()
var r := _rng(231) var r := _rng(seed)
var n := _noise(231, 1.5) var n := _noise(seed, 1.5)
P._blob(st, Vector3(0, 0.7, 0.05), Vector3(0.22, 0.24, 0.55), n, 0.15, coat, r, 1) P._blob(st, Vector3(0, 0.7, 0.05), Vector3(0.22, 0.24, 0.55), n, 0.15, coat, r, 1)
P._blob(st, Vector3(0, 0.8, -0.32), Vector3(0.24, 0.26, 0.3), n, 0.25, P._mul(coat, 1.08), r, 1) # ruff P._blob(st, Vector3(0, 0.8, -0.32), Vector3(0.24, 0.26, 0.3), n, 0.25, P._mul(coat, 1.08), r, 1) # ruff
P._blob(st, Vector3(0, 0.6, -0.1), Vector3(0.16, 0.12, 0.35), n, 0.1, pale, r, 1) # belly P._blob(st, Vector3(0, 0.6, -0.1), Vector3(0.16, 0.12, 0.35), n, 0.1, pale, r, 1) # belly
@@ -120,6 +150,6 @@ static func _wolf() -> Dictionary:
var est := P._begin() var est := P._begin()
for s in [-1.0, 1.0]: for s in [-1.0, 1.0]:
P._blob(est, Vector3(s * 0.065, 0.97, -0.75), Vector3.ONE * 0.022, n, 0.0, Color(1.0, 0.8, 0.3), r, 0) P._blob(est, Vector3(s * 0.065, 0.97, -0.75), Vector3.ONE * 0.022, n, 0.0, Color(1.0, 0.8, 0.3), r, 0)
return {"body": P._commit(st), "eyes": P._commit(est), "leg": _leg(0.52, 0.045, 0.03, coat, dark, 232), return {"body": P._commit(st), "eyes": P._commit(est), "leg": _leg(0.52, 0.045, 0.03, coat, dark, seed + 1),
"hips": [Vector3(-0.11, 0.52, -0.38), Vector3(0.11, 0.52, -0.38), Vector3(-0.11, 0.54, 0.38), Vector3(0.11, 0.54, 0.38)], "hips": [Vector3(-0.11, 0.52, -0.38), Vector3(0.11, 0.52, -0.38), Vector3(-0.11, 0.54, 0.38), Vector3(0.11, 0.54, 0.38)],
"radius": 0.32, "height": 1.0} "radius": 0.32, "height": 1.0}

View File

@@ -13,22 +13,32 @@ extends "res://scripts/creatures/creature.gd"
## is safe. ## is safe.
## - A wolf on its own is timid: it keeps its distance and backs off. ## - A wolf on its own is timid: it keeps its distance and backs off.
## - A badly hurt wolf runs, and comes back another night. ## - A badly hurt wolf runs, and comes back another night.
##
## The alpha (kind "alpha", wolf moon nights only) is different: it isn't
## afraid of fire, it doesn't take turns, and if it can't get at you it
## goes for your door. A wooden door gives way after a few blows (door.gd);
## an iron-banded one holds. It doesn't run when hurt.
var pack: Node var pack: Node
var slot := 0 var slot := 0
var wounded := false ## ran off hurt: stays away until dawn var wounded := false ## ran off hurt: stays away until dawn
func is_alpha() -> bool:
return kind == "alpha"
func display_name() -> String: func display_name() -> String:
return "Wolf" return def.name
func interact_prompt(p: Node) -> String: func interact_prompt(p: Node) -> String:
var nm: String = def.name
if is_dead(): if is_dead():
return "Wolf carcass [E] Butcher" return "%s carcass [E] Butcher" % nm
var s := "Wolf" var s := nm
match state: match state:
"rest": s = "Wolf (resting)" "rest": s = nm + " (resting)"
"alert", "stalk": s = "Wolf (watching you)" "alert", "stalk": s = nm + " (watching you)"
"lunge", "defend": s = "Wolf (attacking!)" "lunge", "defend": s = nm + " (attacking!)"
"bash": s = nm + " (breaking a door!)"
if not Actions.melee_stats(p).armed and not ItemDB.get_item(p.held_item_id()).has("bow"): if not Actions.melee_stats(p).armed and not ItemDB.get_item(p.held_item_id()).has("bow"):
s += " (you need a weapon)" s += " (you need a weapon)"
return s return s
@@ -54,7 +64,9 @@ func _think(dt: float) -> void:
"den": "den":
_think_den(pp, d) _think_den(pp, d)
"hunt": "hunt":
if _aggro > 0.0 and d < 25.0: if is_alpha():
_think_alpha(dt, pp, d)
elif _aggro > 0.0 and d < 25.0:
_attack(pp, d, "defend") _attack(pp, d, "defend")
elif pack.alive_count() <= 1: elif pack.alive_count() <= 1:
_think_alone(pp, d) _think_alone(pp, d)
@@ -105,6 +117,85 @@ func _think_hunt(pp: Vector3, d: float) -> void:
state = "lunge" state = "lunge"
_state_t = 4.0 _state_t = 4.0
# --- the alpha ----------------------------------------------------------------
const BASH_STAND := 1.5 ## stands this far out from a door to hit it
const BASH_REACH := 2.4
const DOOR_SEARCH := 12.0 ## doors within this of the target are worth trying
var _door: Node3D = null ## the door it's working on
var _bash_at := Vector3.ZERO
var _door_look := 0.0
var _entry: Node3D = null ## an open doorway it's going in through
var _entry_out := Vector3.ZERO
var _entry_in := Vector3.ZERO
var _entry_stage := 0 ## 0: to the outside of it, 1: through it
## Straight at you if nothing's in the way (fire or no fire). Otherwise the
## nearest closed door near you, and through it. Otherwise prowl round.
func _think_alpha(dt: float, pp: Vector3, d: float) -> void:
if player.survival.dead:
state = "stalk"
_go_to(pack.circle_point(self))
return
if d < 25.0 and _can_reach_player():
_door = null
_attack(pp, d, "defend")
return
_door_look -= dt
if _door != null and (not is_instance_valid(_door) or _door.is_queued_for_deletion() or not _door.blocks()):
_door = null
if _door == null and _door_look <= 0.0:
_door_look = 1.0
_door = pack.door_for(self, player, DOOR_SEARCH)
if _door != null:
# Stand on the side of the door the alpha is on (the outside).
var n: Vector3 = _door.global_basis.z
var side := 1.0 if (global_position - _door.global_position).dot(n) > 0.0 else -1.0
_bash_at = _door.global_position + n * side * BASH_STAND
if _door == null:
# No shut door left: in through an open (or broken) one, if any.
if _entry == null or not is_instance_valid(_entry) or _entry.blocks():
_entry = pack.door_for(self, player, DOOR_SEARCH, true)
if _entry != null:
var n: Vector3 = _entry.global_basis.z
var side := 1.0 if (global_position - _entry.global_position).dot(n) > 0.0 else -1.0
_entry_out = _entry.global_position + n * side * BASH_STAND
_entry_in = _entry.global_position - n * side * 1.2
_entry_stage = 0
if _entry != null:
state = "bash" # (on its way in: shown the same)
var goal: Vector3 = _entry_out if _entry_stage == 0 else _entry_in
if Vector2(goal.x - global_position.x, goal.z - global_position.z).length() < 0.5:
_entry_stage += 1
if _entry_stage > 1:
_entry = null # inside: next think it can reach you
return
_go_to(goal)
return
state = "stalk"
_go_to(pack.circle_point(self))
return
state = "bash"
var flat := Vector2(_bash_at.x - global_position.x, _bash_at.z - global_position.z).length()
if flat > 0.6:
_go_to(_bash_at)
return
_goal_d = 0.0
var to := _door.global_position - global_position
steer(atan2(-to.x, -to.z))
if _attack_cd <= 0.0 and Vector2(to.x, to.z).length() < BASH_REACH:
_attack_cd = def.attack_cd
pack.bash_door(self, _door, float(def.get("door_damage", def.damage)))
## (Near the door it's battering, the door itself isn't an obstacle.)
func _blocked(dir: Vector3) -> bool:
if state == "bash" and _door != null and global_position.distance_to(_bash_at) < 2.5:
return false
if state == "bash" and _entry != null and is_instance_valid(_entry) and global_position.distance_to(_entry.global_position) < 2.8:
return false # (lined up with the doorway: walking through it)
return super._blocked(dir)
## The last wolf of a pack keeps its distance and won't start a fight. ## The last wolf of a pack keeps its distance and won't start a fight.
func _think_alone(pp: Vector3, d: float) -> void: func _think_alone(pp: Vector3, d: float) -> void:
if d < 10.0: if d < 10.0:
@@ -151,12 +242,16 @@ func _desired_speed() -> float:
"home": return 2.2 if _goal_d < 8.0 else 6.0 "home": return 2.2 if _goal_d < 8.0 else 6.0
"stalk": return 0.0 if _goal_d < 1.0 else (3.4 if _goal_d < 7.0 else 7.5) "stalk": return 0.0 if _goal_d < 1.0 else (3.4 if _goal_d < 7.0 else 7.5)
"lunge", "defend": return def.run "lunge", "defend": return def.run
"bash": return 0.0 if _goal_d < 0.6 else (3.0 if _goal_d < 4.0 else def.run)
"retreat": return 6.0 "retreat": return 6.0
"flee": return def.run "flee": return def.run
return 0.0 return 0.0
func _react(from: Vector3) -> void: func _react(from: Vector3) -> void:
pack.alert(from) pack.alert(from)
if is_alpha():
_aggro = 20.0 # (never runs)
return
if hp < def.hp * 0.4: if hp < def.hp * 0.4:
wounded = true wounded = true
_threat = from _threat = from

View File

@@ -15,8 +15,13 @@ extends Node3D
## While hunting the pack circles you and lets one wolf at a time dart in ## While hunting the pack circles you and lets one wolf at a time dart in
## (every few seconds, slower if you hold a torch). Near a lit campfire it ## (every few seconds, slower if you hold a torch). Near a lit campfire it
## circles wide around the fire and never closes in. ## circles wide around the fire and never closes in.
##
## Every 7th night is a wolf moon (day_night.gd turns the moon red): an
## alpha comes down from the hills with the pack. It leaves at dawn whether
## or not you killed it, and comes back the next wolf moon (wolf.gd).
const WolfScript = preload("res://scripts/creatures/wolf.gd") const WolfScript = preload("res://scripts/creatures/wolf.gd")
const DayNight = preload("res://scripts/world/day_night.gd")
const DEN_DAY := 3 ## wolves arrive in the den const DEN_DAY := 3 ## wolves arrive in the den
const HUNT_DAY := 4 ## first night they hunt const HUNT_DAY := 4 ## first night they hunt
@@ -42,6 +47,14 @@ var _warned := {} ## day -> true once that night's toast was shown
var _howl_left := 0.0 var _howl_left := 0.0
var _audio: AudioStreamPlayer3D var _audio: AudioStreamPlayer3D
static var _howl: AudioStreamWAV static var _howl: AudioStreamWAV
var alpha: Node = null ## tonight's alpha (wolf moon)
var _alpha_day := -1 ## the night it came
var _told := {} ## "door id:outcome" -> true: door toasts already shown
## Night `day` is a wolf moon (once the pack hunts at all). The same rule as
## day_night.gd's red moon.
func wolf_moon(day: int) -> bool:
return day >= hunt_day and DayNight.wolf_moon_day(day)
func setup(w: Node3D, d: Node3D) -> void: func setup(w: Node3D, d: Node3D) -> void:
world = w world = w
@@ -139,6 +152,10 @@ func step(dt: float) -> void:
func _enter(m: String, day: int) -> void: func _enter(m: String, day: int) -> void:
mode = m mode = m
if m == "hunt" and wolf_moon(day) and _alpha_day != day:
spawn_alpha(day)
elif m != "hunt":
_alpha_leaves()
if m == "none": if m == "none":
for w in members: for w in members:
if is_instance_valid(w): if is_instance_valid(w):
@@ -160,6 +177,8 @@ func _warn(day: int) -> void:
Actions.tell_all("Wolves are howling up in the hills. They'll come hunting soon.") Actions.tell_all("Wolves are howling up in the hills. They'll come hunting soon.")
elif day == hunt_day: elif day == hunt_day:
Actions.tell_all("The wolves are hunting tonight. Stay near a fire or behind closed walls.") Actions.tell_all("The wolves are hunting tonight. Stay near a fire or behind closed walls.")
elif wolf_moon(day):
Actions.tell_all("The moon is red: a wolf moon. Something big is hunting with the pack tonight.")
_howl_left = _rng.randf_range(2.0, 6.0) _howl_left = _rng.randf_range(2.0, 6.0)
## Brings the pack up to size, wolves lying in the den. `force` ignores ## Brings the pack up to size, wolves lying in the den. `force` ignores
@@ -189,6 +208,65 @@ func fill_den(force: bool) -> void:
need -= 1 need -= 1
i += 1 i += 1
## The alpha comes out of the den's mouth with the pack.
func spawn_alpha(day: int) -> Node:
_alpha_leaves()
_alpha_day = day
var w: AnimatableBody3D = WolfScript.new()
w.setup("alpha", world, den.mouth_pos(), _rng.randi())
w.pack = self
w.slot = 4
w.state = "stalk"
w.yaw = den_yaw
w.rotation.y = w.yaw
add_child(w)
members.append(w)
alpha = w
return w
## At dawn it goes back into the hills (a carcass stays for butchering).
func _alpha_leaves() -> void:
if alpha != null and is_instance_valid(alpha) and not alpha.is_dead():
members.erase(alpha)
alpha.queue_free()
alpha = null
func alpha_alive() -> bool:
return alpha != null and is_instance_valid(alpha) and not alpha.is_dead()
## The door the alpha `w` should go for to get at `target`: the closed door
## nearest `target` (within `r`), a little biased toward ones nearer `w`.
## `open_ones`: the nearest open or broken door instead (a way in).
func door_for(w: Node3D, target: Node3D, r: float, open_ones: bool = false) -> Node3D:
var best: Node3D = null
var best_s := INF
var tp: Vector3 = target.global_position
for p in world.build.build_pieces():
if not p.has_method("bash") or p.blocks() == open_ones:
continue
var d: float = p.global_position.distance_to(tp)
if d > r:
continue
var score: float = d + 0.3 * p.global_position.distance_to(w.global_position)
if score < best_s:
best_s = score
best = p
return best
## The alpha hits `door`. Tells the player it's after (once per door and
## outcome); on a server, every game hears the door broke.
func bash_door(w: Node, door: Node3D, damage: float) -> void:
var res: String = door.bash(damage)
var key := "%d:%s" % [door.get_instance_id(), res]
if not _told.has(key):
_told[key] = true
match res:
"hit": Actions.tell(w.player, "Something is battering at your door!")
"held": Actions.tell(w.player, "Something is battering at your door, but the iron bands hold.")
"broke": Actions.tell(w.player, "The alpha has smashed your door in!")
if res == "broke" and world.server_mode:
WorldNet.piece_state(door)
func home_slot(w: Node) -> Vector3: func home_slot(w: Node) -> Vector3:
return den.slot_pos(w.slot) return den.slot_pos(w.slot)
@@ -320,8 +398,9 @@ func howl() -> void:
return return
if _audio == null: if _audio == null:
return return
_audio.global_position = (live[0] as Node3D).global_position + Vector3(0, 1, 0) var from: Node3D = alpha if alpha_alive() else live[0] # (the alpha's howl is deeper)
_audio.pitch_scale = _rng.randf_range(0.9, 1.1) _audio.global_position = from.global_position + Vector3(0, 1, 0)
_audio.pitch_scale = _rng.randf_range(0.68, 0.76) if alpha_alive() else _rng.randf_range(0.9, 1.1)
_audio.play() _audio.play()
## A synthesised wolf howl: a rising then falling tone with vibrato and a ## A synthesised wolf howl: a rising then falling tone with vibrato and a

View File

@@ -106,6 +106,12 @@ static func build(key: String) -> ArrayMesh:
"palisade": m = _palisade() "palisade": m = _palisade()
"palisade_gate": m = _palisade_gate(true) "palisade_gate": m = _palisade_gate(true)
"palisade_gate_frame": m = _palisade_gate(false) "palisade_gate_frame": m = _palisade_gate(false)
"iron_door": m = _iron_door(true)
"iron_door_frame": m = _door(false)
"iron_door_panel": m = _iron_door(false)
"alpha_pelt": m = _pelt(ALPHA_FUR)
"alpha_cloak": m = _cloak(ALPHA_FUR)
"wolf_fang": m = _fang()
"berry_bush": m = _berry_bush(true) "berry_bush": m = _berry_bush(true)
"berry_bush_bare": m = _berry_bush(false) "berry_bush_bare": m = _berry_bush(false)
_: _:
@@ -937,3 +943,39 @@ static func _palisade_gate(with_panel: bool) -> ArrayMesh:
if with_panel: if with_panel:
_door_panel_into(st, Vector3(-0.54, 0, 0)) _door_panel_into(st, Vector3(-0.54, 0, 0))
return P._commit(st) return P._commit(st)
# --- P3 the alpha ------------------------------------------------------------------
const ALPHA_FUR := Color(0.17, 0.16, 0.17)
## Iron-banded door: the wooden leaf with three iron straps and studs.
## `in_frame` = the closed door in a wood frame (icon and ghost); otherwise
## just the leaf, hinge at the origin.
static func _iron_door(in_frame: bool) -> ArrayMesh:
var st := P._begin()
var o := Vector3.ZERO
if in_frame:
var r := _rng(79)
for side in [-1.0, 1.0]:
_wall_body(st, r, side * 0.55 if side > 0 else -0.84, 0.84 if side > 0 else -0.55, 0.1, 1.9)
_box(st, Vector3(side * 0.92, 1.0, 0), Vector3(0.16, 2.0, 0.2), BEAM)
_box(st, Vector3(side * 0.6, 0.95, 0), Vector3(0.1, 1.9, 0.2), BEAM)
_box(st, Vector3(0, 1.95, 0), Vector3(1.68, 0.1, 0.2), BEAM)
o = Vector3(-0.54, 0, 0)
var rr := _rng(251)
for i in range(4):
_box(st, o + Vector3(0.135 + 0.27 * i, 0.95, 0), Vector3(0.26, 1.86, 0.08), _board(rr, PLANK_DARK))
for y in [0.3, 0.95, 1.6]:
for z in [0.05, -0.05]:
_box(st, o + Vector3(0.54, y, z), Vector3(1.04, 0.1, 0.02), IRON)
for i in range(4):
_box(st, o + Vector3(0.14 + 0.27 * i, y, z * 1.4), Vector3(0.03, 0.03, 0.02), IRON_BRIGHT)
_box(st, o + Vector3(0.95, 1.0, 0.08), Vector3(0.06, 0.16, 0.05), IRON_BRIGHT)
_box(st, o + Vector3(0.95, 1.0, -0.08), Vector3(0.06, 0.16, 0.05), IRON_BRIGHT)
return P._commit(st)
static func _fang() -> ArrayMesh:
var st := P._begin()
var r := _rng(253)
P._cylinder(st, Vector3(0, 0.02, 0), Vector3(0.02, 0.02, -0.12), 0.022, 0.003, 6, Color(0.88, 0.82, 0.62), r, -1.0, Color(0.6, 0.45, 0.35))
return P._commit(st)

View File

@@ -95,8 +95,8 @@ func _server_tick(w: Node3D, delta: float) -> void:
const PER_PACKET := 40 const PER_PACKET := 40
const ENTRY := 23 const ENTRY := 23
const KIND_IDS := ["rabbit", "deer", "boar", "wolf"] const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha"]
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend"] const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash"]
## One creature in 23 bytes: id, kind, state, position, facing, speed, health. ## One creature in 23 bytes: id, kind, state, position, facing, speed, health.
static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void: static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void:

View File

@@ -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 Identity = preload("res://scripts/net/identity.gd")
const ServerConfig = preload("res://scripts/net/server_config.gd") const ServerConfig = preload("res://scripts/net/server_config.gd")
const PROTOCOL := 6 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6) const PROTOCOL := 7 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors (P3)
const DEFAULT_PORT := 24242 const DEFAULT_PORT := 24242
const HANDSHAKE_TIMEOUT := 10.0 const HANDSHAKE_TIMEOUT := 10.0
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join

View File

@@ -324,6 +324,11 @@ 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)) \ if d != null and d.get("open") != null and _allowed(Rules.use_problem(p.player_id, d)) \
and Actions.toggle_door(p, d): and Actions.toggle_door(p, d):
WorldNet.piece_state(d) WorldNet.piece_state(d)
"door_repair":
var d := WorldNet.piece(_i(a, 0))
if d != null and d.get("broken") != null and _allowed(Rules.use_problem(p.player_id, d)) \
and Actions.repair_door(p, d):
WorldNet.piece_state(d)
"fuel": "fuel":
var f := WorldNet.piece(_i(a, 0)) var f := WorldNet.piece(_i(a, 0))
if f != null and f.get("fuel") != null and Actions.add_fuel(p, f): if f != null and f.get("fuel") != null and Actions.add_fuel(p, f):
@@ -421,7 +426,7 @@ func _run(p: Node3D, what: String, a: Array) -> void:
for m in w.get_tree().get_nodes_in_group("processor"): 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) m.step(float(a[0]) if a.size() > 0 and (a[0] is float or a[0] is int) else 60.0)
"debug_spawn": "debug_spawn":
if cheats and Creature.KINDS.has(_s(a, 0)) and _s(a, 0) != "wolf" and _v(a, 1).is_finite(): if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha"]) and _v(a, 1).is_finite():
w.creatures.spawn(_s(a, 0), _v(a, 1)) w.creatures.spawn(_s(a, 0), _v(a, 1))
"debug_time": "debug_time":
if cheats: if cheats:

View File

@@ -427,8 +427,8 @@ func _s2c_piece_state(nid_: int, extra: Dictionary) -> void:
var p: Node3D = pieces.get(nid_) var p: Node3D = pieces.get(nid_)
if p == null or not is_instance_valid(p): if p == null or not is_instance_valid(p):
return return
if extra.has("open") and p.has_method("set_open"): if p.has_method("net_state"):
p.set_open(bool(extra.open)) p.net_state(extra) # (a door)
elif p.has_method("load_extra"): elif p.has_method("load_extra"):
p.load_extra(extra) p.load_extra(extra)
received += 1 received += 1

View File

@@ -57,6 +57,17 @@ func clock_text() -> String:
func is_night() -> bool: func is_night() -> bool:
return daylight < 0.3 return daylight < 0.3
## Every 7th night is a wolf moon: the moon turns red and an alpha hunts with
## the pack (wolf_pack.gd). Worked out from the day alone, so every game on a
## server agrees without being told.
static func wolf_moon_day(d: int) -> bool:
return d >= 7 and d % 7 == 0
## Tonight's moon is a wolf moon (the small hours count as the night before).
func wolf_moon_now() -> bool:
var h := hours()
return (h >= 19.5 or h < 5.5) and wolf_moon_day(day - (1 if h < 5.5 else 0))
func advance(seconds: float) -> void: func advance(seconds: float) -> void:
time += seconds / day_length time += seconds / day_length
while time >= 1.0: while time >= 1.0:
@@ -103,7 +114,9 @@ func apply(force: bool) -> void:
var moon_dir := Vector3(-to_sun.x, absf(to_sun.y) * 0.8 + 0.25, -to_sun.z * 0.5).normalized() var moon_dir := Vector3(-to_sun.x, absf(to_sun.y) * 0.8 + 0.25, -to_sun.z * 0.5).normalized()
moon.global_basis = Basis.looking_at(-moon_dir, Vector3.FORWARD) moon.global_basis = Basis.looking_at(-moon_dir, Vector3.FORWARD)
moon.light_energy = 0.38 * (1.0 - daylight) * (1.0 - 0.6 * overcast) var red := wolf_moon_now()
moon.light_color = Color(1.0, 0.38, 0.3) if red else Color(0.55, 0.65, 1.0)
moon.light_energy = (0.5 if red else 0.38) * (1.0 - daylight) * (1.0 - 0.6 * overcast)
moon.visible = moon.light_energy > 0.01 moon.visible = moon.light_energy > 0.01
moon.shadow_enabled = moon.visible and not sun.visible moon.shadow_enabled = moon.visible and not sun.visible
@@ -121,7 +134,10 @@ func apply(force: bool) -> void:
for i in range(3): for i in range(3):
var dayc: Color = DAY_COLS[i].lerp(DUSK_COLS[i], low * daylight * (1.0 - overcast * 0.7)) var dayc: Color = DAY_COLS[i].lerp(DUSK_COLS[i], low * daylight * (1.0 - overcast * 0.7))
dayc = dayc.lerp(OVERCAST_COLS[i], overcast * 0.85) dayc = dayc.lerp(OVERCAST_COLS[i], overcast * 0.85)
cols.append(NIGHT_COLS[i].lerp(dayc, daylight)) var night: Color = NIGHT_COLS[i]
if red:
night = night.lerp(Color(0.2, 0.04, 0.04), 0.5) # a blood-red night sky
cols.append(night.lerp(dayc, daylight))
env.fog_light_color = cols[2] env.fog_light_color = cols[2]
if sky_mat != null: if sky_mat != null:
sky_mat.set_shader_parameter("zenith_col", cols[0]) sky_mat.set_shader_parameter("zenith_col", cols[0])

View File

@@ -9,7 +9,7 @@ if [ ! -f .godot/global_script_class_cache.cfg ]; then
echo "Importing the project first (fresh copy)..." echo "Importing the project first (fresh copy)..."
godot --headless --path . --import >/dev/null 2>&1 godot --headless --path . --import >/dev/null 2>&1
fi 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_stone 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_alpha 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="" extra=""
[ "$t" = "test_boundary" ] && extra="--fixed-fps 30" [ "$t" = "test_boundary" ] && extra="--fixed-fps 30"
# A suite that waits on a world that failed to load would hang forever, # A suite that waits on a world that failed to load would hang forever,

265
tests/test_alpha.gd Normal file
View File

@@ -0,0 +1,265 @@
extends SceneTree
## P3 the alpha (docs/PROGRESSION_DESIGN.md §4): the wolf moon every 7th
## night, the alpha that hunts with the pack, wooden doors it breaks and
## iron-banded ones it can't, mending a door, and what the alpha drops.
## Run: godot --headless --path . --script res://tests/test_alpha.gd
var _fails := 0
var _finished := false
var A: Node
var S: Node
var DB: Node
var _toasts: Array[String] = []
var world: Node3D
var player: CharacterBody3D
var pack: Node
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")
A.notify.connect(func(t): _toasts.append(t))
S.root_dir = "user://test_worlds"
for w in S.list_worlds():
S.delete_world(w.id)
S.current_id = S.create_world("Alpha", 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()
player = world.player
player.set_physics_process(false)
player.survival.set_process(false)
world.day_night.frozen = true
world.weather.set_process(false)
world.weather.force("clear")
pack = world.wolves
pack.enabled = false
func _settle() -> void:
for _i in range(3):
await physics_frame
func _set_time(day: int, hours: float) -> void:
world.day_night.day = day
world.day_night.time = hours / 24.0
world.day_night.apply(true)
## Runs the pack and every wolf for up to `secs`, stopping early once
## `until` (if given) returns true. Returns the seconds it took.
func _sim(secs: float, until: Callable = Callable()) -> float:
var t := 0.0
while t < secs:
pack.step(0.1)
for w in pack.alive():
w.set_physics_process(false)
w.step(0.1)
t += 0.1
if until.is_valid() and until.call():
break
if int(t * 10) % 10 == 0:
await physics_frame # (let the physics server catch up with moved bodies)
return t
func _toast_has(s: String) -> bool:
for t in _toasts:
if t.contains(s):
return true
return false
func _body() -> void:
_moon()
await _arrives()
await _doors()
await _dawn()
_drops()
await _saving()
_finished = true
# --- the wolf moon -------------------------------------------------------------------
func _moon() -> void:
print("the wolf moon")
var DN: Script = load("res://scripts/world/day_night.gd")
check(DN.wolf_moon_day(7) and DN.wolf_moon_day(14) and DN.wolf_moon_day(21), "nights 7, 14 and 21 are wolf moons")
check(not DN.wolf_moon_day(6) and not DN.wolf_moon_day(8) and not DN.wolf_moon_day(0), "6, 8 (and day 0) aren't")
var dn: Node = world.day_night
_set_time(7, 22.0)
check(dn.wolf_moon_now() and dn.moon.light_color.r > dn.moon.light_color.b * 2.0, "on night 7 the moon is red (%s)" % dn.moon.light_color)
_set_time(8, 3.0)
check(dn.wolf_moon_now(), "still red in the small hours of day 8")
_set_time(7, 12.0)
check(not dn.wolf_moon_now(), "not by day")
_set_time(8, 22.0)
check(not dn.wolf_moon_now() and dn.moon.light_color.b > dn.moon.light_color.r, "night 8 is an ordinary moon")
check(pack.wolf_moon(7) and not pack.wolf_moon(8), "the pack keeps the same calendar")
# --- the alpha comes -------------------------------------------------------------------
func _arrives() -> void:
print("the alpha")
_set_time(7, 12.0)
await _sim(2.0)
check(pack.mode == "den" and not pack.alpha_alive(), "day 7: the pack rests, no alpha yet")
_toasts.clear()
_set_time(7, 21.0)
await _sim(1.0)
check(pack.mode == "hunt" and pack.alpha_alive(), "night 7: an alpha hunts with the pack")
var al: Node = pack.alpha
check(al.kind == "alpha" and absf(al.hp - 3.0 * al.KINDS.wolf.hp) < 0.01, "with three times a wolf's health (%.0f)" % al.hp)
check(_toast_has("wolf moon"), "a toast warns you (%s)" % [_toasts])
check(pack.alive().has(al) and pack.alive_count() == pack.pack_size(7) + 1, "it runs with the pack (%d wolves and the alpha)" % pack.pack_size(7))
al.take_hit(100.0, al.global_position + Vector3(3, 0, 0))
check(al.state != "flee" and not al.wounded, "badly hurt, it doesn't run (%s)" % al.state)
al.hp = al.def.hp
# --- doors ------------------------------------------------------------------------------
var hut: Dictionary
var door: Node3D
var door_xf: Transform3D
var inside: Vector3
func _doors() -> void:
print("a wooden door")
var bs: Node = world.build
var yaw: float = player.rotation.y
var b := Basis(Vector3.UP, yaw)
var site: Vector3 = bs.debug_find_site(player.global_position - player.global_basis.z * 14.0, 3.0)
hut = await bs.debug_build_hut(site, yaw)
await _settle()
check(hut.placed == 20, "a closed hut (%d pieces, %s)" % [hut.placed, hut.failed])
for p in hut.pieces:
if p.item_id == "wood_door":
door = p
door_xf = door.global_transform
var center: Vector3 = hut.pieces[0].global_transform * Vector3(1, 0, 1)
inside = center + b * Vector3(-0.6, 0.05, -0.6)
player.global_position = inside
var fire: Node3D = world.spawn_piece(DB.get_item("campfire"), Transform3D(b, center + b * Vector3(0.6, 0.0, -0.8)))
fire.fuel = fire.MAX_FUEL
await _settle()
var al: Node3D = pack.alpha
var out_front: Vector3 = door.global_position + door.global_basis.z * 7.0
out_front.y = world.terrain.sample_height(out_front.x, out_front.z)
al.global_position = out_front
for w in pack.alive():
if w != al:
w.global_position = out_front + Vector3(4, 0, 4)
_toasts.clear()
check(pack.fire_near(player) != null, "you're inside by a fire (wolves won't close in)")
var took: float = await _sim(45.0, func(): return door.broken)
check(door.broken and not door.blocks(), "the alpha breaks the door down anyway (%.0f s)" % took)
check(_toast_has("battering") and _toast_has("smashed"), "and you hear it coming (%s)" % [_toasts])
var space := world.get_world_3d().direct_space_state
var q := PhysicsRayQueryParameters3D.create(door.global_transform * Vector3(0, 1.0, 1.5), door.global_transform * Vector3(0, 1.0, -1.5), 1)
check(space.intersect_ray(q).is_empty(), "the doorway is open")
check(door.interact_prompt(player).begins_with("Broken door"), "it shows as broken: \"%s\"" % door.interact_prompt(player))
check(door.bash(50.0) == "", "(a broken door takes no more blows)")
var hp0: float = player.survival.health
var got_in: float = await _sim(20.0, func(): return player.survival.health < hp0)
check(player.survival.health < hp0, "then it comes in through the doorway and bites (%.0f s, health %.0f)" % [got_in, player.survival.health])
player.survival.health = 100.0
al.global_position = out_front
al._entry = null
print("mending it")
player.global_position = door.global_transform * Vector3(0.0, 0.05, -1.0)
player.inventory.load_data(Inventory.new(40).to_data())
check(not A.repair_door(player, door) and door.broken, "no wood, no repair")
player.inventory.add("wood", 3)
check(A.repair_door(player, door) and not door.broken and door.blocks(), "two wood and it's shut and whole again")
check(player.inventory.count("wood") == 1 and door.door_hp == door.DOOR_HP, "(for 2 wood; back to full strength)")
player.global_position = inside
print("an iron-banded door")
player.inventory.add("iron_bar", 2)
player.inventory.add("wood", 4)
player.inventory.add("nails", 4)
var forge: Node3D = world.spawn_piece(DB.get_item("forge"), Transform3D(b, center + b * Vector3(-5.0, 0, 0)))
player.global_position = forge.global_position + b * Vector3(0, 0, 1.5)
check(A.craft(player, "iron_door"), "made at the forge (2 iron, 4 wood, 4 nails)")
world.remove_piece(forge)
world.remove_piece(door)
await _settle()
var idef: Dictionary = DB.get_item("iron_door")
check(bs.check(idef, door_xf) == "", "it goes where the wooden one was")
door = world.spawn_piece(idef, door_xf)
player.global_position = inside
await _settle()
al.global_position = out_front
al._door = null
_toasts.clear()
await _sim(30.0, func(): return door.broken)
check(not door.broken and door.blocks(), "30 s of the alpha's blows and it holds")
check(_toast_has("iron bands hold"), "you hear it holding (%s)" % [_toasts])
check(player.survival.health > 99.0, "and the alpha never got at you (health %.0f)" % player.survival.health)
world.remove_piece(fire)
func _dawn() -> void:
print("dawn")
var al: Node = pack.alpha
_set_time(8, 6.5)
await _sim(1.0)
check(pack.mode == "den" and not pack.alpha_alive() and not pack.members.has(al), "at dawn the alpha goes back into the hills")
_set_time(8, 21.0)
await _sim(1.0)
check(pack.mode == "hunt" and not pack.alpha_alive(), "night 8: just the wolves")
func _drops() -> void:
print("its pelt")
var al: Node = pack.spawn_alpha(14)
al.take_hit(500.0, al.global_position + Vector3(2, 0, 0))
check(al.is_dead(), "an alpha can be killed")
var got: Dictionary = al.butcher_drops()
check(int(got.get("alpha_pelt", 0)) == 1 and int(got.get("wolf_fang", 0)) >= 1 and int(got.get("raw_meat", 0)) >= 2,
"butchered: an alpha pelt, fangs and meat (%s)" % [got])
check(int(DB.get_item("alpha_cloak").wear.warmth) > int(DB.get_item("wolf_cloak").wear.warmth), "the alpha cloak is the warmest thing to wear")
check(DB.recipes.has("alpha_cloak") and DB.recipes.alpha_cloak.inputs.has("alpha_pelt"), "made from its pelt")
print("online")
var CN: Node = root.get_node("CreatureNet")
al.set_meta("cid", 77)
al.state = "bash"
var buf := PackedByteArray()
buf.resize(CN.ENTRY)
CN._encode(buf, 0, al)
var e: Array = CN._decode(buf, 0)
check(e[1] == "alpha" and e[4] == "bash", "it goes over the network as an alpha (%s, %s)" % [e[1], e[4]])
al.queue_free()
func _saving() -> void:
print("saving")
door.queue_free()
await _settle()
var wd: Node3D = world.spawn_piece(DB.get_item("wood_door"), door_xf)
await _settle()
check(wd.bash(250.0) == "broke", "a wooden door, broken")
check(world.save_now(), "saved")
world.queue_free()
await process_frame
await _load()
var back: Node3D = null
for p in world.all_pieces():
if p.item_id == "wood_door" and p.global_position.distance_to(door_xf.origin) < 0.05:
back = p
check(back != null and back.broken and not back.blocks(), "comes back broken")