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:
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
Actions.toggle_door(player, self)
|
if broken:
|
||||||
|
Actions.repair_door(player, self)
|
||||||
|
else:
|
||||||
|
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_open(bool(d.get("open", false)), false)
|
set_broken(bool(d.get("broken", false)))
|
||||||
|
if not broken:
|
||||||
|
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)))
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
265
tests/test_alpha.gd
Normal 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")
|
||||||
Reference in New Issue
Block a user