P3 thieves and barrow: crows and foxes steal food left out; the barrow with its guards, coffin loot and key pieces; blunt club vs bone (protocol 10)
This commit is contained in:
@@ -305,6 +305,24 @@ func toggle_door(player: Node3D, door: Node3D) -> bool:
|
||||
door.set_open(not door.open)
|
||||
return true
|
||||
|
||||
## E on the barrow's coffin (barrow.gd): take what's in it. Opening it wakes
|
||||
## the guards. What doesn't fit goes on the floor by you.
|
||||
func loot_barrow(player: Node3D, barrow: Node) -> bool:
|
||||
if barrow == null or not barrow.found or not _in_reach(player, barrow._coffin, 0.5):
|
||||
return false
|
||||
if _online():
|
||||
return _ask("barrow_loot", [])
|
||||
var got: Dictionary = barrow.loot(player)
|
||||
if got.is_empty():
|
||||
return false
|
||||
for id in got:
|
||||
var left: int = player.inventory.add(id, int(got[id]))
|
||||
if left > 0:
|
||||
barrow.world.drop_item(id, left, Transform3D(Basis(), player.global_position + Vector3(0, 0.3, 0)))
|
||||
item_gained.emit(id, int(got[id]))
|
||||
tell(player, "+%d %s" % [int(got[id]), ItemDB.item_name(id)])
|
||||
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):
|
||||
@@ -359,7 +377,8 @@ func melee_stats(player: Node) -> Dictionary:
|
||||
var w: Dictionary = held.get("weapon", {})
|
||||
if float(w.get("damage", 0)) > 0.0:
|
||||
return {"damage": float(w.damage), "reach": float(w.get("reach", 2.2)),
|
||||
"stamina": float(w.get("stamina", 8)), "cooldown": float(w.get("cooldown", 0.5)), "armed": true}
|
||||
"stamina": float(w.get("stamina", 8)), "cooldown": float(w.get("cooldown", 0.5)), "armed": true,
|
||||
"blunt": bool(w.get("blunt", false))}
|
||||
if held.has("tool"):
|
||||
return {"damage": TOOL_AS_WEAPON, "reach": 2.3, "stamina": 6.0, "cooldown": 0.5, "armed": true}
|
||||
return NO_WEAPON
|
||||
@@ -384,7 +403,9 @@ func melee(player: Node3D, target: Node3D) -> bool:
|
||||
if not player.survival.use_stamina(m.stamina):
|
||||
notify.emit("Too tired")
|
||||
return false
|
||||
target.take_hit(m.damage, player.global_position)
|
||||
# Blunt weapons (the club) crack bone: +50% on skeletons.
|
||||
var bone := bool(target.def.get("bone", false)) if target.get("def") != null else false
|
||||
target.take_hit(m.damage * (1.5 if bone and m.get("blunt", false) else 1.0), player.global_position)
|
||||
use_held(player)
|
||||
if target.is_dead():
|
||||
notify.emit("Killed a %s" % target.display_name().to_lower())
|
||||
|
||||
@@ -61,6 +61,8 @@ func _find_home() -> bool:
|
||||
continue
|
||||
if Vector2(c.x - spawn.x, c.z - spawn.z).length() < 60.0 or Vector2(c.x - den_at.x, c.z - den_at.z).length() < 60.0:
|
||||
continue
|
||||
if world.barrow != null and world.barrow.found and Vector2(c.x - world.barrow.position.x, c.z - world.barrow.position.z).length() < 40.0:
|
||||
continue
|
||||
if terrain.sample_height(c.x, c.z) < terrain.water_level + 1.0:
|
||||
continue
|
||||
var near := 0
|
||||
@@ -202,7 +204,7 @@ func raid(b: Node3D, target: Node3D) -> int:
|
||||
what = "A bear has broken into your chest and eaten the food!"
|
||||
else:
|
||||
eaten = int(target.amount)
|
||||
target.collected()
|
||||
world.take_dropped(target)
|
||||
what = "A bear is eating the food you left out!"
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(target.global_position) < 80.0:
|
||||
|
||||
@@ -46,6 +46,20 @@ const KINDS := {
|
||||
"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]}},
|
||||
# Thieves (thieves.gd): crows by day and foxes at dusk and night come for
|
||||
# food left lying on the ground.
|
||||
"crow": {"name": "Crow", "hp": 6.0, "walk": 1.0, "run": 9.0, "turn": 8.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
|
||||
"drops": {"feather": [1, 2]}},
|
||||
"fox": {"name": "Fox", "hp": 25.0, "walk": 1.5, "run": 7.6, "turn": 7.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
|
||||
"drops": {"raw_meat": [1, 1], "hide": [1, 1]}},
|
||||
# The barrow's skeletal guards (guard.gd, barrow.gd). Slow (you can walk
|
||||
# away from one) but they hit hard; bone, so blunt weapons do +50%.
|
||||
"guard": {"name": "Barrow Guard", "hp": 80.0, "walk": 1.4, "run": 3.4, "turn": 4.0,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 30.0,
|
||||
"damage": 20.0, "attack_range": 2.0, "attack_cd": 2.0, "bone": true,
|
||||
"drops": {"iron_nugget": [1, 2]}},
|
||||
# One bear per island, in the deep forest by the rim (bear.gd, bear_range.gd).
|
||||
# Slower than you sprinting, but it hits like a falling tree.
|
||||
"bear": {"name": "Bear", "hp": 220.0, "walk": 1.3, "run": 7.0, "turn": 3.5,
|
||||
@@ -118,10 +132,10 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
var eyes := MeshInstance3D.new()
|
||||
eyes.mesh = parts.eyes
|
||||
var em := StandardMaterial3D.new()
|
||||
var red := k == "alpha"
|
||||
em.albedo_color = Color(1.0, 0.3, 0.2) if red else Color(1.0, 0.85, 0.4)
|
||||
var eye: Color = {"alpha": Color(1.0, 0.18, 0.1), "guard": Color(0.45, 0.8, 1.0)}.get(k, Color(1.0, 0.75, 0.3))
|
||||
em.albedo_color = eye.lerp(Color.WHITE, 0.2)
|
||||
em.emission_enabled = true
|
||||
em.emission = Color(1.0, 0.18, 0.1) if red else Color(1.0, 0.75, 0.3)
|
||||
em.emission = eye
|
||||
em.emission_energy_multiplier = 2.5
|
||||
eyes.material_override = em
|
||||
eyes.set_meta("keep_mat", true)
|
||||
@@ -141,9 +155,9 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
cap.height = maxf(parts.height, parts.radius * 2.0 + 0.01)
|
||||
cs.shape = cap
|
||||
cs.position = Vector3(0, parts.height * 0.5, 0)
|
||||
if k != "rabbit":
|
||||
if k != "rabbit" and k != "guard": # (rabbits are round, guards stand upright)
|
||||
# Long bodies: lay the capsule along the spine.
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0}.get(k, 1.4)
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0, "fox": 0.8, "crow": 0.3}.get(k, 1.4)
|
||||
cs.rotation.x = PI * 0.5
|
||||
cs.position = Vector3(0, parts.height * 0.55, 0)
|
||||
add_child(cs)
|
||||
|
||||
@@ -20,6 +20,9 @@ static func build(kind: String) -> Dictionary:
|
||||
"wolf": d = _wolf()
|
||||
"alpha": d = _wolf(true)
|
||||
"bear": d = _bear()
|
||||
"fox": d = _scaled(_wolf_parts(Color(0.78, 0.4, 0.15), Color(0.92, 0.88, 0.8), 281), 0.6)
|
||||
"crow": d = _crow()
|
||||
"guard": d = _guard()
|
||||
_: d = _boar()
|
||||
_cache[kind] = d
|
||||
return d
|
||||
@@ -178,3 +181,72 @@ static func _bear() -> Dictionary:
|
||||
return {"body": P._commit(st), "eyes": P._commit(est), "leg": _leg(0.72, 0.13, 0.11, coat, dark, 262),
|
||||
"hips": [Vector3(-0.28, 0.72, -0.5), Vector3(0.28, 0.72, -0.5), Vector3(-0.28, 0.74, 0.6), Vector3(0.28, 0.74, 0.6)],
|
||||
"radius": 0.62, "height": 1.6}
|
||||
|
||||
## Crow: a small black bird facing -Z. The two "legs" are its wings
|
||||
## (crow.gd swings them out sideways and flaps them).
|
||||
static func _crow() -> Dictionary:
|
||||
var black := Color(0.08, 0.08, 0.1)
|
||||
var sheen := Color(0.14, 0.15, 0.2)
|
||||
var beak := Color(0.2, 0.18, 0.15)
|
||||
var st := P._begin()
|
||||
var r := _rng(291)
|
||||
var n := _noise(291, 2.0)
|
||||
P._blob(st, Vector3(0, 0.16, 0), Vector3(0.07, 0.07, 0.14), n, 0.1, black, r, 1)
|
||||
P._blob(st, Vector3(0, 0.22, -0.13), Vector3(0.05, 0.05, 0.055), n, 0.1, sheen, r, 1)
|
||||
P._cylinder(st, Vector3(0, 0.215, -0.17), Vector3(0, 0.2, -0.24), 0.016, 0.002, 4, beak, r)
|
||||
P._cylinder(st, Vector3(0, 0.17, 0.12), Vector3(0, 0.15, 0.26), 0.03, 0.05, 4, black, r)
|
||||
for s in [-1.0, 1.0]:
|
||||
P._cylinder(st, Vector3(s * 0.025, 0.1, 0.0), Vector3(s * 0.03, 0.0, 0.02), 0.006, 0.005, 3, beak, r)
|
||||
for s in [-1.0, 1.0]: # (dull eyes: no glowing "eyes" mesh for a crow)
|
||||
P._blob(st, Vector3(s * 0.035, 0.235, -0.16), Vector3.ONE * 0.008, n, 0.0, Color(0.3, 0.25, 0.2), r, 0)
|
||||
var wst := P._begin()
|
||||
P._blob(wst, Vector3(0, -0.14, 0.02), Vector3(0.012, 0.15, 0.08), n, 0.05, sheen, r, 0)
|
||||
return {"body": P._commit(st), "leg": P._commit(wst),
|
||||
"hips": [Vector3(-0.06, 0.19, -0.01), Vector3(0.06, 0.19, -0.01)],
|
||||
"radius": 0.12, "height": 0.3}
|
||||
|
||||
## Barrow guard: an upright skeleton facing -Z, in a rusted helm, an old
|
||||
## sword in its right hand and a round shield on its left arm. Two legs
|
||||
## (creature.gd walks them like a pair of a quadruped's).
|
||||
static func _guard() -> Dictionary:
|
||||
var bone := Color(0.82, 0.79, 0.68)
|
||||
var old := Color(0.62, 0.58, 0.48)
|
||||
var rust := Color(0.42, 0.24, 0.14)
|
||||
var dark := Color(0.06, 0.06, 0.07)
|
||||
var wood := Color(0.32, 0.22, 0.14)
|
||||
var st := P._begin()
|
||||
var r := _rng(301)
|
||||
var n := _noise(301, 2.2)
|
||||
P._blob(st, Vector3(0, 0.95, 0), Vector3(0.17, 0.07, 0.1), n, 0.2, old, r, 0) # pelvis
|
||||
P._cylinder(st, Vector3(0, 0.97, 0.02), Vector3(0, 1.45, 0.0), 0.035, 0.03, 5, bone, r) # spine
|
||||
for i in range(4): # ribs
|
||||
var y := 1.18 + 0.075 * i
|
||||
P._cylinder(st, Vector3(-0.15 + 0.01 * i, y, -0.02), Vector3(0.15 - 0.01 * i, y, -0.02), 0.018, 0.018, 4, bone, r)
|
||||
for s in [-1.0, 1.0]:
|
||||
P._cylinder(st, Vector3(s * (0.15 - 0.01 * i), y, -0.02), Vector3(s * 0.11, y - 0.02, 0.07), 0.015, 0.015, 3, old, r)
|
||||
P._cylinder(st, Vector3(-0.22, 1.47, 0), Vector3(0.22, 1.47, 0), 0.025, 0.025, 4, bone, r) # shoulders
|
||||
P._blob(st, Vector3(0, 1.66, -0.02), Vector3(0.11, 0.13, 0.12), n, 0.12, bone, r, 1) # skull
|
||||
P._blob(st, Vector3(0, 1.56, -0.08), Vector3(0.07, 0.04, 0.06), n, 0.1, old, r, 0) # jaw
|
||||
P._cylinder(st, Vector3(0, 1.69, -0.01), Vector3(0, 1.82, -0.01), 0.14, 0.06, 7, rust, r, -1.0, rust) # helm
|
||||
P._cylinder(st, Vector3(0, 1.66, -0.12), Vector3(0, 1.5, -0.14), 0.018, 0.012, 4, rust, r) # nose guard
|
||||
for s in [-1.0, 1.0]:
|
||||
P._blob(st, Vector3(s * 0.045, 1.67, -0.115), Vector3(0.03, 0.025, 0.012), n, 0.0, dark, r, 0) # sockets
|
||||
# Right arm forward with the sword, left arm with the shield.
|
||||
P._cylinder(st, Vector3(0.22, 1.47, 0), Vector3(0.28, 1.2, -0.12), 0.025, 0.022, 4, bone, r)
|
||||
P._cylinder(st, Vector3(0.28, 1.2, -0.12), Vector3(0.27, 1.08, -0.38), 0.022, 0.02, 4, bone, r)
|
||||
P._cylinder(st, Vector3(0.27, 1.08, -0.4), Vector3(0.29, 1.5, -0.95), 0.03, 0.006, 4, rust, r) # blade
|
||||
P._cylinder(st, Vector3(0.2, 1.05, -0.38), Vector3(0.34, 1.1, -0.4), 0.02, 0.02, 4, rust, r) # crossguard
|
||||
P._cylinder(st, Vector3(-0.22, 1.47, 0), Vector3(-0.3, 1.2, -0.05), 0.025, 0.022, 4, bone, r)
|
||||
P._cylinder(st, Vector3(-0.3, 1.2, -0.05), Vector3(-0.28, 1.05, -0.25), 0.022, 0.02, 4, bone, r)
|
||||
P._cylinder(st, Vector3(-0.33, 1.12, -0.3), Vector3(-0.31, 1.12, -0.36), 0.26, 0.26, 9, wood, r, -1.0, wood) # shield
|
||||
P._cylinder(st, Vector3(-0.31, 1.12, -0.36), Vector3(-0.305, 1.12, -0.4), 0.06, 0.04, 6, rust, r, -1.0, rust) # boss
|
||||
var est := P._begin()
|
||||
for s in [-1.0, 1.0]:
|
||||
P._blob(est, Vector3(s * 0.045, 1.67, -0.125), Vector3.ONE * 0.012, n, 0.0, Color(0.5, 0.85, 1.0), r, 0)
|
||||
var lst := P._begin()
|
||||
P._cylinder(lst, Vector3(0, 0, 0), Vector3(0, -0.45, 0.02), 0.03, 0.025, 4, bone, r)
|
||||
P._cylinder(lst, Vector3(0, -0.45, 0.02), Vector3(0, -0.9, 0), 0.025, 0.022, 4, bone, r)
|
||||
P._blob(lst, Vector3(0, -0.9, -0.05), Vector3(0.04, 0.025, 0.08), n, 0.1, old, r, 0)
|
||||
return {"body": P._commit(st), "eyes": P._commit(est), "leg": P._commit(lst),
|
||||
"hips": [Vector3(-0.1, 0.93, 0), Vector3(0.1, 0.93, 0)],
|
||||
"radius": 0.3, "height": 1.85}
|
||||
|
||||
115
scripts/creatures/crow.gd
Normal file
115
scripts/creatures/crow.gd
Normal file
@@ -0,0 +1,115 @@
|
||||
extends "res://scripts/creatures/creature.gd"
|
||||
## A crow (docs/PROGRESSION_DESIGN.md §4): sent by thieves.gd, by day, for
|
||||
## food left lying on the ground. It flies in, lands on it and pecks it away
|
||||
## a bit at a time, and takes off again if anyone comes within SCARE. Then
|
||||
## it flies off over the trees and is gone. A closed chest keeps food safe.
|
||||
## An arrow brings it down (feathers).
|
||||
|
||||
const SCARE := 7.0
|
||||
const PECK_EVERY := 3.0
|
||||
const FLY_SPEED := 9.0
|
||||
const CRUISE := 7.0 ## height above the ground while flying in
|
||||
const LEAVE_HEIGHT := 14.0
|
||||
|
||||
var thieves: Node ## thieves.gd
|
||||
var food: Node3D = null
|
||||
var _goal := Vector3.ZERO
|
||||
var _peck_anim := 0.0
|
||||
|
||||
func display_name() -> String:
|
||||
return "Crow"
|
||||
|
||||
func interact_prompt(p: Node) -> String:
|
||||
if is_dead():
|
||||
return "Crow [E] Pluck"
|
||||
return {"peck": "Crow (pecking at your food!)", "fly": "Crow (circling your food)"}.get(state, "Crow")
|
||||
|
||||
func _flying() -> bool:
|
||||
return state in ["fly", "fly_off"]
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
match state:
|
||||
"fly":
|
||||
if not thieves.still_there(food) or thieves.watched(food.global_position, SCARE):
|
||||
_leave()
|
||||
return
|
||||
_goal = food.global_position
|
||||
var ground: float = terrain.sample_height(global_position.x, global_position.z)
|
||||
if Vector2(_goal.x - global_position.x, _goal.z - global_position.z).length() < 0.35 and global_position.y - ground < 0.15:
|
||||
state = "peck"
|
||||
_state_t = PECK_EVERY
|
||||
thieves.landed(self, food)
|
||||
"peck":
|
||||
if not thieves.still_there(food) or thieves.watched(global_position, SCARE):
|
||||
_leave()
|
||||
return
|
||||
_state_t -= dt
|
||||
if _state_t <= 0.0:
|
||||
_state_t = PECK_EVERY
|
||||
_peck_anim = 0.4
|
||||
if thieves.peck(self, food): # the last of it
|
||||
_leave()
|
||||
"fly_off":
|
||||
if thieves.out_of_sight(global_position):
|
||||
thieves.gone(self)
|
||||
_:
|
||||
_leave()
|
||||
|
||||
func _leave() -> void:
|
||||
state = "fly_off"
|
||||
food = null
|
||||
var p: Node3D = world.nearest_player(global_position)
|
||||
var away := Vector3(1, 0, 0)
|
||||
if p != null:
|
||||
away = global_position - p.global_position
|
||||
away.y = 0.0
|
||||
if away.length() < 0.1:
|
||||
away = Vector3(1, 0, 0)
|
||||
_goal = global_position + away.normalized() * 120.0
|
||||
|
||||
## Straight-line flight (no steering round trees: it flies over them).
|
||||
func _move(dt: float) -> void:
|
||||
if not _flying():
|
||||
speed = 0.0
|
||||
return
|
||||
var to := _goal - global_position
|
||||
to.y = 0.0
|
||||
var flat := to.length()
|
||||
var spd := FLY_SPEED
|
||||
if state == "fly" and flat < 6.0:
|
||||
spd = maxf(flat * 1.5, 1.2) # glide in to land
|
||||
var p := global_position
|
||||
if flat > 0.02:
|
||||
yaw = rotate_toward(yaw, atan2(-to.x, -to.z), 8.0 * dt)
|
||||
p += to / flat * minf(spd * dt, flat)
|
||||
var ground: float = terrain.sample_height(p.x, p.z)
|
||||
var alt := LEAVE_HEIGHT
|
||||
if state == "fly":
|
||||
alt = CRUISE * clampf((flat - 0.3) / 8.0, 0.0, 1.0)
|
||||
p.y = maxf(move_toward(global_position.y, maxf(ground, terrain.water_level) + alt, 5.0 * dt), ground)
|
||||
global_position = p
|
||||
rotation.y = yaw
|
||||
speed = spd
|
||||
|
||||
## Wings out and flapping in the air, folded on the ground; a dip of the
|
||||
## head when it pecks.
|
||||
func _animate(dt: float) -> void:
|
||||
var flying := _flying() or (puppet and speed > 0.5)
|
||||
_phase += dt * 16.0
|
||||
for l in _legs:
|
||||
var s := -1.0 if l.position.x < 0.0 else 1.0
|
||||
l.rotation.z = s * ((PI * 0.5 + sin(_phase) * 0.7) if flying else 0.2)
|
||||
_peck_anim = maxf(_peck_anim - dt, 0.0)
|
||||
if is_dead():
|
||||
return
|
||||
_visual.rotation.x = 0.5 * sin(_peck_anim / 0.4 * PI) if state == "peck" else 0.0
|
||||
|
||||
func _react(_from: Vector3) -> void:
|
||||
_leave()
|
||||
|
||||
## Brought down out of the air: it lands where it fell.
|
||||
func _die(from: Vector3) -> void:
|
||||
super._die(from)
|
||||
var p := global_position
|
||||
p.y = terrain.sample_height(p.x, p.z)
|
||||
global_position = p
|
||||
95
scripts/creatures/fox.gd
Normal file
95
scripts/creatures/fox.gd
Normal file
@@ -0,0 +1,95 @@
|
||||
extends "res://scripts/creatures/creature.gd"
|
||||
## A fox (docs/PROGRESSION_DESIGN.md §4): sent by thieves.gd at dusk and
|
||||
## night for food left lying on the ground. It slinks up, grabs the whole
|
||||
## stack in its jaws and runs off with it. Come within WARY while it's
|
||||
## creeping in and it bolts empty-mouthed. Kill it while it's carrying and
|
||||
## it drops what it took; let it get away and the food's gone.
|
||||
|
||||
const WARY := 10.0
|
||||
const SNEAK_SPEED := 3.4
|
||||
const GIVE_UP := 30.0 ## seconds running with it before it's gone for good
|
||||
|
||||
var thieves: Node
|
||||
var food: Node3D = null
|
||||
var carry: Dictionary = {} ## {id, count, extra} once it has the food
|
||||
var _escape := Vector3.ZERO
|
||||
var _goal_d := 0.0
|
||||
|
||||
func display_name() -> String:
|
||||
return "Fox"
|
||||
|
||||
func interact_prompt(p: Node) -> String:
|
||||
if is_dead():
|
||||
return "Fox carcass [E] Butcher"
|
||||
if not carry.is_empty():
|
||||
return "Fox (running off with your %s!)" % ItemDB.item_name(String(carry.id)).to_lower()
|
||||
return "Fox (sneaking up on your food)" if state == "sneak" else "Fox"
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
var pp: Vector3 = player.global_position
|
||||
_state_t -= dt
|
||||
match state:
|
||||
"sneak":
|
||||
if not thieves.still_there(food):
|
||||
_bolt(pp)
|
||||
return
|
||||
if thieves.watched(global_position, WARY):
|
||||
_bolt(pp)
|
||||
return
|
||||
var fp: Vector3 = food.global_position
|
||||
_go_to(fp)
|
||||
if Vector2(fp.x - global_position.x, fp.z - global_position.z).length() < 0.9:
|
||||
carry = thieves.grab(self, food)
|
||||
food = null
|
||||
state = "carry"
|
||||
_state_t = GIVE_UP
|
||||
_escape = _away_point(pp, 60.0)
|
||||
"carry", "flee":
|
||||
_go_to(_escape)
|
||||
if thieves.out_of_sight(global_position) or _state_t <= 0.0 or _goal_d < 1.5:
|
||||
thieves.gone(self)
|
||||
_:
|
||||
_bolt(pp)
|
||||
|
||||
func _bolt(from: Vector3) -> void:
|
||||
state = "flee" if carry.is_empty() else "carry"
|
||||
food = null
|
||||
_state_t = GIVE_UP
|
||||
_escape = _away_point(from, 60.0)
|
||||
|
||||
## A point `d` away from `from`, through the fox, kept inside the valley.
|
||||
func _away_point(from: Vector3, d: float) -> Vector3:
|
||||
var away := global_position - from
|
||||
away.y = 0.0
|
||||
if away.length() < 0.1:
|
||||
away = Vector3(1, 0, 0)
|
||||
away = away.normalized()
|
||||
for turn in [0.0, 0.6, -0.6, 1.2, -1.2, PI]:
|
||||
var p: Vector3 = global_position + away.rotated(Vector3.UP, turn) * d
|
||||
if terrain.inside_boundary(p.x, p.z, 8.0) and terrain.sample_height(p.x, p.z) > terrain.water_level + 0.3:
|
||||
return p
|
||||
return global_position + away * d * 0.3
|
||||
|
||||
func _go_to(p: Vector3) -> void:
|
||||
var to := p - global_position
|
||||
to.y = 0.0
|
||||
if to.length() > 0.3:
|
||||
steer(atan2(-to.x, -to.z))
|
||||
_goal_d = to.length()
|
||||
|
||||
func _desired_speed() -> float:
|
||||
match state:
|
||||
"sneak": return 0.0 if _goal_d < 0.5 else SNEAK_SPEED
|
||||
"carry", "flee": return def.run
|
||||
return 0.0
|
||||
|
||||
func _react(from: Vector3) -> void:
|
||||
_bolt(from)
|
||||
|
||||
## Killed with the food in its jaws: it drops it.
|
||||
func _die(from: Vector3) -> void:
|
||||
super._die(from)
|
||||
if not carry.is_empty() and not puppet:
|
||||
var at := global_position + Vector3(0, 0.3, 0)
|
||||
world.drop_item(String(carry.id), int(carry.count), Transform3D(Basis(), at), carry.get("extra", {}))
|
||||
carry = {}
|
||||
110
scripts/creatures/guard.gd
Normal file
110
scripts/creatures/guard.gd
Normal file
@@ -0,0 +1,110 @@
|
||||
extends "res://scripts/creatures/creature.gd"
|
||||
## A barrow guard (docs/PROGRESSION_DESIGN.md §4): a skeleton standing at its
|
||||
## post in the barrow (barrow.gd) until something wakes it: someone inside
|
||||
## the stone ring at night (or any time after day 10), someone opening the
|
||||
## coffin, or a blow. Then it comes for you. It's slow, so you can walk away
|
||||
## from it, but it hits hard and it isn't afraid of fire or torches. It
|
||||
## won't follow you far from the barrow; it goes back to its post and stands
|
||||
## there again.
|
||||
|
||||
var barrow: Node ## barrow.gd
|
||||
var post := Vector3.ZERO
|
||||
var post_yaw := 0.0
|
||||
var _goal_d := 0.0
|
||||
|
||||
func display_name() -> String:
|
||||
return def.name
|
||||
|
||||
func interact_prompt(p: Node) -> String:
|
||||
if is_dead():
|
||||
return "Bones [E] Search"
|
||||
var s := "Barrow guard"
|
||||
match state:
|
||||
"rest": s = "Barrow guard (still... for now)"
|
||||
"charge": s = "Barrow guard (coming for you!)"
|
||||
"home": s = "Barrow guard (going back to its post)"
|
||||
if not Actions.melee_stats(p).armed and not ItemDB.get_item(p.held_item_id()).has("bow"):
|
||||
s += " (you need a weapon: a club cracks bone)"
|
||||
return s
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
var pp: Vector3 = player.global_position
|
||||
var d := global_position.distance_to(pp)
|
||||
_state_t -= dt
|
||||
match state:
|
||||
"rest":
|
||||
target_yaw = post_yaw
|
||||
if barrow.wakes_for(player):
|
||||
state = "charge"
|
||||
_attack_cd = 0.8
|
||||
"charge":
|
||||
if player.survival.dead or barrow.center().distance_to(pp) > barrow.LEASH or not barrow.alarmed:
|
||||
state = "home"
|
||||
return
|
||||
var to := pp - global_position
|
||||
steer(atan2(-to.x, -to.z))
|
||||
_goal_d = d
|
||||
if d < def.attack_range and _attack_cd <= 0.0 and _can_reach(player):
|
||||
_attack_cd = def.attack_cd
|
||||
player.survival.damage(def.damage * world.difficulty_mult("damage"), global_position, "a barrow guard")
|
||||
"home":
|
||||
if barrow.wakes_for(player) and barrow.center().distance_to(pp) < barrow.LEASH:
|
||||
state = "charge"
|
||||
return
|
||||
var to := post - global_position
|
||||
to.y = 0.0
|
||||
_goal_d = to.length()
|
||||
if _goal_d < 0.4:
|
||||
state = "rest"
|
||||
else:
|
||||
steer(atan2(-to.x, -to.z))
|
||||
_:
|
||||
state = "home"
|
||||
|
||||
func _desired_speed() -> float:
|
||||
match state:
|
||||
"charge": return 0.0 if _goal_d < def.attack_range * 0.8 else def.run
|
||||
"home": return 0.0 if _goal_d < 0.4 else def.walk * 1.5
|
||||
return 0.0
|
||||
|
||||
## Nothing solid between its sword and you.
|
||||
func _can_reach(p: Node3D) -> bool:
|
||||
var from := global_position + Vector3(0, 1.2, 0)
|
||||
var to: Vector3 = p.global_position + Vector3(0, 1.0, 0)
|
||||
var q := PhysicsRayQueryParameters3D.create(from, to, 1)
|
||||
q.exclude = [get_rid(), p.get_rid(), terrain.get_node("TerrainBody").get_rid()]
|
||||
return get_world_3d().direct_space_state.intersect_ray(q).is_empty()
|
||||
|
||||
## In the barrow, what's ahead is measured from the stone floor (the ground
|
||||
## under it is lower, and a probe aimed at that would hit the floor slab).
|
||||
func _blocked(dir: Vector3) -> bool:
|
||||
var probe := global_position + dir * (1.4 + speed * 0.15)
|
||||
var f: float = barrow.floor_y_at(probe)
|
||||
if f == -INF and barrow.floor_y_at(global_position) == -INF:
|
||||
return super._blocked(dir)
|
||||
if not terrain.inside_boundary(probe.x, probe.z, 6.0):
|
||||
return true
|
||||
var h: float = f if f > -INF else terrain.sample_height(probe.x, probe.z)
|
||||
if absf(h - global_position.y) > 1.1:
|
||||
return true
|
||||
var q := PhysicsRayQueryParameters3D.create(global_position + Vector3(0, 0.45, 0), Vector3(probe.x, h + 0.45, probe.z), 1)
|
||||
q.exclude = [get_rid(), terrain.get_node("TerrainBody").get_rid()]
|
||||
return not get_world_3d().direct_space_state.intersect_ray(q).is_empty()
|
||||
|
||||
## Inside the barrow it walks on the stone floor, not the ground under it.
|
||||
func _move(dt: float) -> void:
|
||||
super._move(dt)
|
||||
var f: float = barrow.floor_y_at(global_position)
|
||||
if f > global_position.y:
|
||||
global_position.y = f
|
||||
|
||||
func _react(_from: Vector3) -> void:
|
||||
barrow.alarm()
|
||||
state = "charge"
|
||||
_attack_cd = maxf(_attack_cd, 0.6)
|
||||
|
||||
## Standing upright, a blow knocks it to pieces (no toppling sideways).
|
||||
func _die(from: Vector3) -> void:
|
||||
super._die(from)
|
||||
_visual.rotation = Vector3(-PI * 0.5, 0, 0)
|
||||
_visual.position.y = 0.15
|
||||
150
scripts/creatures/thieves.gd
Normal file
150
scripts/creatures/thieves.gd
Normal file
@@ -0,0 +1,150 @@
|
||||
extends Node3D
|
||||
## Thieves (docs/PROGRESSION_DESIGN.md §4): crows by day and foxes at dusk
|
||||
## and night, after food left lying on the ground.
|
||||
##
|
||||
## Food on the ground that nobody has been within UNWATCHED of for
|
||||
## NOTICE_AFTER seconds draws a thief. It comes from COME_FROM away, out of
|
||||
## sight of everyone. A crow pecks the stack away one at a time; a fox runs
|
||||
## off with all of it. Either one gives up if you come near (crow.gd,
|
||||
## fox.gd). Food in a chest is never touched (that's what chests are for);
|
||||
## the bear (bear_range.gd) is another matter.
|
||||
##
|
||||
## On a server this runs for everyone (thieves are wildlife: creature_net.gd).
|
||||
|
||||
const CrowScript = preload("res://scripts/creatures/crow.gd")
|
||||
const FoxScript = preload("res://scripts/creatures/fox.gd")
|
||||
|
||||
const CHECK_EVERY := 5.0
|
||||
const UNWATCHED := 10.0
|
||||
const NOTICE_AFTER := 20.0
|
||||
const MAX_THIEVES := 3
|
||||
const COME_FROM := 35.0
|
||||
const OUT_OF_SIGHT := 45.0 ## this far from everyone, a thief that's leaving is gone
|
||||
|
||||
signal stolen(thief: Node, id: String, count: int)
|
||||
|
||||
var world: Node3D
|
||||
var enabled := true
|
||||
var _acc := 0.0
|
||||
var _unwatched: Dictionary = {} ## pickup instance id -> seconds left alone
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
|
||||
func setup(w: Node3D) -> void:
|
||||
world = w
|
||||
_rng.seed = w.terrain.noise_seed + 997
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if enabled:
|
||||
step(delta)
|
||||
|
||||
## Living thieves, coming or going.
|
||||
func alive() -> Array:
|
||||
return get_children().filter(func(c): return is_instance_valid(c) and c.has_method("is_dead") and not c.is_dead() and not c.is_queued_for_deletion())
|
||||
|
||||
static func is_food(id: String) -> bool:
|
||||
return ItemDB.get_item(id).has("food")
|
||||
|
||||
func night() -> bool:
|
||||
var h: float = world.day_night.hours()
|
||||
return h >= 17.5 or h < 6.5
|
||||
|
||||
## Someone (alive) within `r` of `pos`.
|
||||
func watched(pos: Vector3, r: float) -> bool:
|
||||
for p in world.players():
|
||||
if not p.survival.dead and p.global_position.distance_to(pos) < r:
|
||||
return true
|
||||
return false
|
||||
|
||||
func out_of_sight(pos: Vector3) -> bool:
|
||||
return not watched(pos, OUT_OF_SIGHT)
|
||||
|
||||
func still_there(food: Node3D) -> bool:
|
||||
return food != null and is_instance_valid(food) and not food.is_queued_for_deletion() and not food.taken and int(food.amount) > 0
|
||||
|
||||
func _targeted(food: Node3D) -> bool:
|
||||
for t in alive():
|
||||
if t.food == food:
|
||||
return true
|
||||
return false
|
||||
|
||||
func step(dt: float) -> void:
|
||||
_acc += dt
|
||||
if _acc < CHECK_EVERY:
|
||||
return
|
||||
var since := _acc
|
||||
_acc = 0.0
|
||||
var seen := {}
|
||||
for d in world.all_dropped():
|
||||
if not is_food(d.item_id):
|
||||
continue
|
||||
var k: int = d.get_instance_id()
|
||||
seen[k] = true
|
||||
if watched(d.global_position, UNWATCHED):
|
||||
_unwatched.erase(k)
|
||||
continue
|
||||
_unwatched[k] = float(_unwatched.get(k, 0.0)) + since
|
||||
if float(_unwatched[k]) >= NOTICE_AFTER and not _targeted(d) and alive().size() < MAX_THIEVES:
|
||||
send(d)
|
||||
for k in _unwatched.keys():
|
||||
if not seen.has(k):
|
||||
_unwatched.erase(k)
|
||||
|
||||
## Sends a thief after `food`: a fox at dusk and night, otherwise a crow.
|
||||
func send(food: Node3D) -> Node3D:
|
||||
var fox := night()
|
||||
var fp: Vector3 = food.global_position
|
||||
var from := Vector3.INF
|
||||
for _i in range(12):
|
||||
var a := _rng.randf() * TAU
|
||||
var p := fp + Vector3(cos(a), 0, sin(a)) * COME_FROM
|
||||
var ground: float = world.terrain.sample_height(p.x, p.z)
|
||||
if world.terrain.inside_boundary(p.x, p.z, 6.0) and (not fox or ground > world.terrain.water_level + 0.3) \
|
||||
and not watched(p, 20.0):
|
||||
from = p
|
||||
break
|
||||
if from == Vector3.INF:
|
||||
return null
|
||||
var t: AnimatableBody3D = (FoxScript if fox else CrowScript).new()
|
||||
t.setup("fox" if fox else "crow", world, from, _rng.randi())
|
||||
t.thieves = self
|
||||
t.food = food
|
||||
t.state = "sneak" if fox else "fly"
|
||||
add_child(t)
|
||||
if not fox:
|
||||
t.global_position.y = world.terrain.sample_height(from.x, from.z) + t.CRUISE
|
||||
var to := fp - from
|
||||
t.yaw = atan2(-to.x, -to.z)
|
||||
t.rotation.y = t.yaw
|
||||
return t
|
||||
|
||||
## Whoever left `food` there (anyone within 60 m) hears about it, once.
|
||||
func _tell(food_pos: Vector3, text: String) -> void:
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(food_pos) < 60.0:
|
||||
Actions.tell(p, text)
|
||||
|
||||
func landed(crow: Node, food: Node3D) -> void:
|
||||
_tell(food.global_position, "A crow is pecking at your %s!" % ItemDB.item_name(food.item_id).to_lower())
|
||||
|
||||
## A crow takes one. True when that was the last of it.
|
||||
func peck(crow: Node, food: Node3D) -> bool:
|
||||
if not still_there(food):
|
||||
return true
|
||||
food.amount = int(food.amount) - 1
|
||||
stolen.emit(crow, food.item_id, 1)
|
||||
if int(food.amount) <= 0:
|
||||
world.take_dropped(food)
|
||||
return true
|
||||
return false
|
||||
|
||||
## A fox takes the lot. Returns what it's carrying.
|
||||
func grab(fox: Node, food: Node3D) -> Dictionary:
|
||||
var c := {"id": String(food.item_id), "count": int(food.amount), "extra": food.extra.duplicate()}
|
||||
_tell(food.global_position, "A fox has run off with your %s!" % ItemDB.item_name(c.id).to_lower())
|
||||
world.take_dropped(food)
|
||||
stolen.emit(fox, c.id, c.count)
|
||||
return c
|
||||
|
||||
## A thief left with what it took (or empty-handed).
|
||||
func gone(t: Node) -> void:
|
||||
t.queue_free()
|
||||
@@ -115,6 +115,7 @@ static func build(key: String) -> ArrayMesh:
|
||||
"bear_hide": m = _pelt(BEAR_FUR)
|
||||
"bear_fat": m = _pebble(271, Color(0.92, 0.86, 0.68), Vector3(0.1, 0.06, 0.08), 0.1)
|
||||
"fur_rug": m = _rug()
|
||||
"barrow_key": m = _barrow_key()
|
||||
"berry_bush": m = _berry_bush(true)
|
||||
"berry_bush_bare": m = _berry_bush(false)
|
||||
_:
|
||||
@@ -1002,3 +1003,19 @@ static func _rug() -> ArrayMesh:
|
||||
_box(st, Vector3(sx, 0.11, -0.8), Vector3(0.08, 0.05, 0.05), P._mul(BEAR_FUR, 0.8))
|
||||
_box(st, Vector3(0, 0.02, 0.92), Vector3(0.12, 0.03, 0.12), BEAR_FUR)
|
||||
return P._commit(st)
|
||||
|
||||
# --- P3 the barrow --------------------------------------------------------------------
|
||||
|
||||
## A third of a carved stone disc, with a groove and a pale inlay.
|
||||
static func _barrow_key() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(311)
|
||||
var segs := 5
|
||||
for i in range(segs):
|
||||
var a0 := -0.55 + 1.1 * i / segs
|
||||
var a1 := -0.55 + 1.1 * (i + 1) / segs
|
||||
var c := Vector3(sin((a0 + a1) * 0.5), 0, cos((a0 + a1) * 0.5)) * 0.09
|
||||
var w := 2.0 * 0.09 * sin((a1 - a0) * 0.5) + 0.006
|
||||
_box(st, c + Vector3(0, 0.02, 0), Vector3(w, 0.04, 0.12), P._mul(Color(0.45, 0.46, 0.44), r.randf_range(0.9, 1.05)), Basis(Vector3.UP, (a0 + a1) * 0.5))
|
||||
_box(st, Vector3(0, 0.042, 0.1), Vector3(0.1, 0.006, 0.02), Color(0.82, 0.8, 0.7))
|
||||
return P._commit(st)
|
||||
|
||||
@@ -19,6 +19,8 @@ extends Node
|
||||
|
||||
const Creature = preload("res://scripts/creatures/creature.gd")
|
||||
const BearScript = preload("res://scripts/creatures/bear.gd")
|
||||
const CrowScript = preload("res://scripts/creatures/crow.gd")
|
||||
const GuardScript = preload("res://scripts/creatures/guard.gd") # (falls apart in its own way)
|
||||
const Bobber = preload("res://scripts/fishing/bobber.gd")
|
||||
const Projectile = preload("res://scripts/creatures/projectile.gd")
|
||||
const WolfPack = preload("res://scripts/creatures/wolf_pack.gd")
|
||||
@@ -96,9 +98,9 @@ func _server_tick(w: Node3D, delta: float) -> void:
|
||||
|
||||
const PER_PACKET := 40
|
||||
const ENTRY := 23
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear"]
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear", "crow", "fox", "guard"]
|
||||
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash",
|
||||
"warn", "raid", "eat"]
|
||||
"warn", "raid", "eat", "fly", "fly_off", "peck", "sneak", "carry"]
|
||||
|
||||
## One creature in 23 bytes: id, kind, state, position, facing, speed, health.
|
||||
static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void:
|
||||
@@ -194,7 +196,9 @@ func _s2c_life(packed: PackedByteArray, fl: Array) -> void:
|
||||
var got: Variant = puppets.get(cid)
|
||||
var c: Node3D = got if is_instance_valid(got) else null
|
||||
if c == null:
|
||||
c = BearScript.new() if kind == "bear" else Creature.new() # (the bear rears up when it warns you)
|
||||
# (Kinds with their own look: the bear rears up, a crow flaps.)
|
||||
var script: Script = {"bear": BearScript, "crow": CrowScript, "guard": GuardScript}.get(kind, Creature)
|
||||
c = script.new()
|
||||
c.puppet = true
|
||||
c.setup(kind, world, e[2], cid)
|
||||
c.set_meta("cid", cid)
|
||||
|
||||
@@ -32,7 +32,7 @@ signal clock(day: int, time: float) ## the server's time of day
|
||||
const Identity = preload("res://scripts/net/identity.gd")
|
||||
const ServerConfig = preload("res://scripts/net/server_config.gd")
|
||||
|
||||
const PROTOCOL := 8 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear (P3)
|
||||
const PROTOCOL := 10 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors; 8: the bear; 9: crows and foxes; 10: the barrow (P3)
|
||||
const DEFAULT_PORT := 24242
|
||||
const HANDSHAKE_TIMEOUT := 10.0
|
||||
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
||||
|
||||
@@ -324,6 +324,9 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
||||
if d != null and d.get("open") != null and _allowed(Rules.use_problem(p.player_id, d)) \
|
||||
and Actions.toggle_door(p, d):
|
||||
WorldNet.piece_state(d)
|
||||
"barrow_loot":
|
||||
if w.barrow != null:
|
||||
Actions.loot_barrow(p, w.barrow)
|
||||
"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)) \
|
||||
@@ -426,7 +429,7 @@ func _run(p: Node3D, what: String, a: Array) -> void:
|
||||
for m in w.get_tree().get_nodes_in_group("processor"):
|
||||
m.step(float(a[0]) if a.size() > 0 and (a[0] is float or a[0] is int) else 60.0)
|
||||
"debug_spawn":
|
||||
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear"]) and _v(a, 1).is_finite():
|
||||
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear", "crow", "fox", "guard"]) and _v(a, 1).is_finite():
|
||||
w.creatures.spawn(_s(a, 0), _v(a, 1))
|
||||
"debug_time":
|
||||
if cheats:
|
||||
|
||||
449
scripts/world/barrow.gd
Normal file
449
scripts/world/barrow.gd
Normal file
@@ -0,0 +1,449 @@
|
||||
extends Node3D
|
||||
## The barrow (docs/PROGRESSION_DESIGN.md §4): an ancient burial mound in a
|
||||
## ring of standing stones, out on open ground. Scenery, like the wolf den:
|
||||
## built from the seed on every machine, so it's the same for everyone.
|
||||
##
|
||||
## A stone-lined passage leads under the mound into a chamber with a stone
|
||||
## coffin at the back. Three skeletal guards (guard.gd) stand inside. They
|
||||
## wake when someone's inside the stone ring at night (or at any time after
|
||||
## day WAKE_DAY), when the coffin is opened, or when one of them is hit.
|
||||
## The coffin holds iron and a barrow key piece (three open the standing
|
||||
## stones on the rim, P5). It fills again RESTOCK_DAYS after it was emptied,
|
||||
## and fallen guards are back at their posts then too.
|
||||
##
|
||||
## Guards and loot run on the server (or in single-player): start().
|
||||
## Saved with the world: when the coffin was emptied and which guards fell.
|
||||
##
|
||||
## Local space: +Z is the passage's mouth (facing the centre of the valley),
|
||||
## the origin is the chamber floor.
|
||||
|
||||
const IM = preload("res://scripts/items/item_meshes.gd")
|
||||
const P = preload("res://scripts/world/proc_mesh.gd")
|
||||
const GuardScript = preload("res://scripts/creatures/guard.gd")
|
||||
|
||||
const MOUND_R := 6.5
|
||||
const MOUND_H := 3.6
|
||||
const RING_R := 10.0 ## the standing stones
|
||||
const CLEAR_R := 11.5
|
||||
const LEASH := 22.0 ## guards don't follow you further than this from the centre
|
||||
const WAKE_DAY := 10 ## after this, they wake by day too
|
||||
const RESTOCK_DAYS := 5
|
||||
const PASSAGE_END := 6.4 ## the mouth, local z
|
||||
const POSTS := [[Vector3(-1.4, 0, -0.4), 0.0], [Vector3(1.4, 0, -0.4), 0.0], [Vector3(0.0, 0, 1.2), 0.0]]
|
||||
const LOOT := {"iron_bar": [1, 3], "iron_nugget": [3, 6], "barrow_key": [1, 1]}
|
||||
|
||||
const STONE := Color(0.5, 0.5, 0.47)
|
||||
const MOSS := Color(0.32, 0.38, 0.22)
|
||||
const TURF := Color(0.3, 0.4, 0.18)
|
||||
const EARTH := Color(0.36, 0.3, 0.2)
|
||||
|
||||
var world: Node3D
|
||||
var terrain: Node3D
|
||||
var found := false
|
||||
var yaw := 0.0
|
||||
var alarmed := false ## guards stirred (the coffin was opened, one was hit)
|
||||
var loot_day := -1 ## the day it was emptied (-1: never)
|
||||
var fallen: Array = [] ## post indices whose guard is dead
|
||||
var guards: Array = []
|
||||
var running := false ## this machine runs the guards (server / single-player)
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
var _mouth_drop := 0.0 ## how far the ground at the mouth is below the floor
|
||||
|
||||
# --- placing it -----------------------------------------------------------------------
|
||||
|
||||
func place(w: Node3D) -> bool:
|
||||
world = w
|
||||
terrain = w.terrain
|
||||
var site := _find_site()
|
||||
if site == Vector3.INF:
|
||||
push_warning("[barrow] no site for the barrow on this seed")
|
||||
return false
|
||||
found = true
|
||||
position = site
|
||||
yaw = atan2(-site.x, -site.z) # mouth toward the middle of the valley
|
||||
rotation.y = yaw
|
||||
var mouth: Vector3 = position + Basis(Vector3.UP, yaw) * Vector3(0, 0, PASSAGE_END)
|
||||
_mouth_drop = maxf(position.y - terrain.sample_height(mouth.x, mouth.z), 0.0)
|
||||
_clear_area(w.props)
|
||||
_build(w.props._rock_mat)
|
||||
w.props.static_cuts.append([Transform3D(Basis(Vector3.UP, yaw), position), MOUND_R + 0.5, MOUND_R + 1.5])
|
||||
return true
|
||||
|
||||
## Open, fairly flat ground away from the spawn, the den and the sea. Where
|
||||
## nothing qualifies (a lumpy island), later passes accept rougher ground
|
||||
## nearer the spawn and the den; the first pass is the original search, so
|
||||
## islands that had a site keep it.
|
||||
func _find_site() -> Vector3:
|
||||
for pass_ in [[1.2, 55.0, 70.0], [2.0, 45.0, 50.0], [3.0, 40.0, 40.0]]:
|
||||
var c: Vector3 = _search(float(pass_[0]), float(pass_[1]), float(pass_[2]))
|
||||
if c != Vector3.INF:
|
||||
return c
|
||||
return Vector3.INF
|
||||
|
||||
func _search(max_spread: float, from_spawn: float, from_den: float) -> Vector3:
|
||||
_rng.seed = terrain.noise_seed + 555
|
||||
var spawn: Vector3 = world._spawn
|
||||
var den_at: Vector3 = world.den.position if world.den != null and world.den.found else Vector3(1e9, 0, 1e9)
|
||||
var best := Vector3.INF
|
||||
var best_score := INF
|
||||
for _i in range(600):
|
||||
var a := _rng.randf() * TAU
|
||||
var rr := _rng.randf_range(40.0, terrain.play_radius - 20.0)
|
||||
var c := Vector2(cos(a), sin(a)) * rr
|
||||
if not terrain.inside_boundary(c.x, c.y, CLEAR_R + 4.0) or terrain.sea_weight(c.x, c.y) > 0.02:
|
||||
continue
|
||||
if c.distance_to(Vector2(spawn.x, spawn.z)) < from_spawn or c.distance_to(Vector2(den_at.x, den_at.z)) < from_den:
|
||||
continue
|
||||
var hs: Array[float] = []
|
||||
for k in range(8):
|
||||
var b := TAU * k / 8.0
|
||||
for rad in [MOUND_R * 0.5, PASSAGE_END + 0.5]:
|
||||
hs.append(terrain.sample_height(c.x + cos(b) * rad, c.y + sin(b) * rad))
|
||||
var h0: float = terrain.sample_height(c.x, c.y)
|
||||
hs.append(h0)
|
||||
if hs.min() < terrain.water_level + 1.0:
|
||||
continue
|
||||
var spread: float = hs.max() - hs.min()
|
||||
if spread > max_spread:
|
||||
continue
|
||||
var trees := 0
|
||||
for n in world.props.resource_nodes.values():
|
||||
if Vector2(n.global_position.x - c.x, n.global_position.z - c.y).length() < CLEAR_R:
|
||||
trees += 1
|
||||
var score: float = spread * 4.0 + trees
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
# Floor level: the highest ground inside the chamber, so the
|
||||
# terrain doesn't come up through the floor.
|
||||
var top := h0
|
||||
for d in [Vector2(-2, -2), Vector2(2, -2), Vector2(-2, 2), Vector2(2, 2)]:
|
||||
top = maxf(top, terrain.sample_height(c.x + d.x, c.y + d.y))
|
||||
best = Vector3(c.x, top + 0.03, c.y)
|
||||
return best
|
||||
|
||||
func _clear_area(props: Node3D) -> void:
|
||||
for dict in [props.resource_nodes, props.pickups, props.forage]:
|
||||
for uid in dict.keys():
|
||||
var n: Node3D = dict[uid]
|
||||
if not is_instance_valid(n):
|
||||
continue
|
||||
if Vector2(n.global_position.x - position.x, n.global_position.z - position.z).length() < CLEAR_R:
|
||||
if n.has_method("_set_instance"):
|
||||
n._set_instance(Transform3D(Basis(), n.global_position + Vector3(0, -1000, 0)))
|
||||
dict.erase(uid)
|
||||
n.queue_free()
|
||||
|
||||
# --- building it -----------------------------------------------------------------------
|
||||
|
||||
var _body: StaticBody3D
|
||||
var _st: SurfaceTool
|
||||
var _stone_rng := RandomNumberGenerator.new()
|
||||
|
||||
## A dressed stone block: drawn and solid.
|
||||
func _block(c: Vector3, s: Vector3, basis: Basis = Basis()) -> void:
|
||||
var col := P._mul(STONE, _stone_rng.randf_range(0.8, 1.05)).lerp(MOSS, _stone_rng.randf() * 0.35)
|
||||
IM._box(_st, c, s, col, basis)
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = s
|
||||
cs.shape = box
|
||||
cs.transform = Transform3D(basis, c)
|
||||
_body.add_child(cs)
|
||||
|
||||
func _build(rock_mat: Material) -> void:
|
||||
_stone_rng.seed = terrain.noise_seed + 556
|
||||
_body = StaticBody3D.new()
|
||||
_body.name = "Stones"
|
||||
_body.collision_layer = 1 | 4 # solid, and a roof for shelter checks
|
||||
add_child(_body)
|
||||
_st = P._begin()
|
||||
# Chamber: 4 x 4 m inside, 2.4 m high, a slab roof.
|
||||
_block(Vector3(0, -0.15, 0), Vector3(4.8, 0.36, 4.8)) # floor (top at 0.03)
|
||||
_block(Vector3(0, 1.2, -2.2), Vector3(4.8, 2.4, 0.4)) # back
|
||||
for s in [-1.0, 1.0]:
|
||||
_block(Vector3(s * 2.2, 1.2, 0), Vector3(0.4, 2.4, 4.0)) # sides
|
||||
_block(Vector3(s * 1.4, 1.2, 2.2), Vector3(1.6, 2.4, 0.4)) # front, either side of the passage
|
||||
_block(Vector3(0, 2.6, 0), Vector3(4.8, 0.4, 4.8)) # roof
|
||||
_block(Vector3(0, 2.15, 2.2), Vector3(1.2, 0.5, 0.4)) # over the passage's inner end
|
||||
# Passage: 1.2 m wide, 1.95 m high, from the chamber to the mouth. Its
|
||||
# floor slopes down to the ground at the mouth.
|
||||
var plen := PASSAGE_END - 2.4
|
||||
var mid := 2.4 + plen * 0.5
|
||||
var tilt := atan2(_mouth_drop, plen)
|
||||
_block(Vector3(0, -0.15 - _mouth_drop * 0.5, mid), Vector3(1.6, 0.36, sqrt(plen * plen + _mouth_drop * _mouth_drop)), Basis(Vector3.RIGHT, tilt))
|
||||
for s in [-1.0, 1.0]:
|
||||
_block(Vector3(s * 0.8, 1.0 - _mouth_drop * 0.5, mid), Vector3(0.4, 2.0 + _mouth_drop, plen))
|
||||
_block(Vector3(0, 2.1, mid), Vector3(2.0, 0.3, plen))
|
||||
# The mouth: two tall jambs and a lintel.
|
||||
for s in [-1.0, 1.0]:
|
||||
_block(Vector3(s * 0.95, 1.1 - _mouth_drop * 0.5, PASSAGE_END + 0.1), Vector3(0.5, 2.6 + _mouth_drop, 0.5))
|
||||
_block(Vector3(0, 2.45, PASSAGE_END + 0.1), Vector3(2.6, 0.45, 0.6))
|
||||
# The coffin at the back: a stone chest with a lid.
|
||||
_coffin = Coffin.new()
|
||||
_coffin.barrow = self
|
||||
_coffin.collision_layer = 1
|
||||
_coffin.position = Vector3(0, 0, -1.45) # (its origin is what reach is measured to)
|
||||
add_child(_coffin)
|
||||
var ccs := CollisionShape3D.new()
|
||||
var cbox := BoxShape3D.new()
|
||||
cbox.size = Vector3(1.9, 0.85, 0.8)
|
||||
ccs.shape = cbox
|
||||
ccs.position = Vector3(0, 0.45, 0)
|
||||
_coffin.add_child(ccs)
|
||||
IM._box(_st, Vector3(0, 0.4, -1.45), Vector3(1.8, 0.75, 0.75), P._mul(STONE, 0.85))
|
||||
IM._box(_st, Vector3(0, 0.82, -1.45), Vector3(1.95, 0.12, 0.85), STONE)
|
||||
IM._box(_st, Vector3(0, 0.9, -1.45), Vector3(0.9, 0.05, 0.3), MOSS)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = P._commit(_st)
|
||||
mi.material_override = ItemDB.item_material()
|
||||
add_child(mi)
|
||||
_build_mound()
|
||||
_build_ring(rock_mat)
|
||||
|
||||
## The grass mound over the chamber: a dome that meets the ground at its
|
||||
## edge, open where the passage goes in. Its surface is solid (you can climb
|
||||
## onto it); inside it you see the stone.
|
||||
func _build_mound() -> void:
|
||||
var st := P._begin()
|
||||
var faces := PackedVector3Array()
|
||||
var rings := 9
|
||||
var segs := 36
|
||||
var nz := FastNoiseLite.new()
|
||||
nz.seed = terrain.noise_seed + 557
|
||||
var b := Basis(Vector3.UP, yaw)
|
||||
var pt := func(i: int, j: int) -> Vector3:
|
||||
var rr: float = MOUND_R * float(i) / rings
|
||||
var a: float = TAU * float(j) / segs
|
||||
var x: float = sin(a) * rr
|
||||
var z: float = cos(a) * rr
|
||||
var w: Vector3 = position + b * Vector3(x, 0, z)
|
||||
var ground: float = terrain.sample_height(w.x, w.z) - position.y
|
||||
var k: float = rr / MOUND_R
|
||||
return Vector3(x, lerpf(MOUND_H, ground - 0.3, k * k) + nz.get_noise_2d(x * 3.0, z * 3.0) * 0.12 * (1.0 - k), z)
|
||||
for i in range(rings):
|
||||
for j in range(segs):
|
||||
var q := [pt.call(i, j), pt.call(i + 1, j), pt.call(i + 1, j + 1), pt.call(i, j + 1)]
|
||||
for tri in [[q[0], q[1], q[2]], [q[0], q[2], q[3]]]:
|
||||
var c: Vector3 = (tri[0] + tri[1] + tri[2]) / 3.0
|
||||
if absf(c.x) < 1.05 and c.z > 2.0:
|
||||
continue # the passage
|
||||
if (tri[0] as Vector3).distance_to(tri[1]) < 0.001 or (tri[1] as Vector3).distance_to(tri[2]) < 0.001:
|
||||
continue # (degenerate, at the top)
|
||||
var col := TURF.lerp(EARTH, clampf(0.4 - c.y * 0.12, 0.0, 0.5))
|
||||
P._tri(st, tri[0], tri[1], tri[2], c - Vector3(0, -3, 0), P._mul(col, 0.9 + 0.2 * nz.get_noise_2d(c.x * 7.0, c.z * 7.0)))
|
||||
var nrm: Vector3 = ((tri[1] as Vector3) - tri[0]).cross((tri[2] as Vector3) - tri[0])
|
||||
if nrm.dot(c - Vector3(0, -3, 0)) < 0.0:
|
||||
faces.append_array(PackedVector3Array([tri[0], tri[2], tri[1]]))
|
||||
else:
|
||||
faces.append_array(PackedVector3Array([tri[0], tri[1], tri[2]]))
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.name = "Mound"
|
||||
mi.mesh = P._commit(st)
|
||||
mi.material_override = ItemDB.item_material()
|
||||
add_child(mi)
|
||||
var shape := ConcavePolygonShape3D.new()
|
||||
shape.set_faces(faces)
|
||||
shape.backface_collision = true
|
||||
var cs := CollisionShape3D.new()
|
||||
cs.shape = shape
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "MoundBody"
|
||||
body.collision_layer = 1 | 4
|
||||
body.add_child(cs)
|
||||
add_child(body)
|
||||
|
||||
## Standing stones round the mound, with a gap in front of the mouth.
|
||||
func _build_ring(rock_mat: Material) -> void:
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "Ring"
|
||||
body.collision_layer = 1
|
||||
add_child(body)
|
||||
var n := 11
|
||||
for i in range(n):
|
||||
var a := TAU * (i + 0.5) / n
|
||||
if absf(wrapf(a, -PI, PI)) < 0.3:
|
||||
continue # (the mouth faces +Z: a = 0)
|
||||
var lp := Vector3(sin(a) * RING_R, 0, cos(a) * RING_R)
|
||||
var w: Vector3 = position + Basis(Vector3.UP, yaw) * lp
|
||||
var radii := Vector3(0.55, 1.5 + 0.35 * sin(i * 2.3), 0.4)
|
||||
var y: float = terrain.sample_height(w.x, w.z) - position.y + radii.y * 0.8 - 0.3
|
||||
var mesh := P.rock(terrain.noise_seed + 5200 + i, false)
|
||||
var turn := Basis(Vector3.UP, a + 0.2 * sin(i * 1.7)) * Basis(Vector3.RIGHT, 0.05 * sin(i * 3.1))
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
mi.material_override = rock_mat
|
||||
mi.transform = Transform3D(turn.scaled(radii), Vector3(lp.x, y, lp.z))
|
||||
add_child(mi)
|
||||
var hull := mesh.create_convex_shape(true, true) as ConvexPolygonShape3D
|
||||
var pts := PackedVector3Array()
|
||||
for p: Vector3 in hull.points:
|
||||
pts.append(turn * (p * radii))
|
||||
hull.points = pts
|
||||
var cs := CollisionShape3D.new()
|
||||
cs.shape = hull
|
||||
cs.position = Vector3(lp.x, y, lp.z)
|
||||
body.add_child(cs)
|
||||
|
||||
# --- where things are --------------------------------------------------------------------
|
||||
|
||||
func center() -> Vector3:
|
||||
return position
|
||||
|
||||
func to_world(lp: Vector3) -> Vector3:
|
||||
return position + Basis(Vector3.UP, yaw) * lp
|
||||
|
||||
func to_local_pos(p: Vector3) -> Vector3:
|
||||
return Basis(Vector3.UP, yaw).inverse() * (p - position)
|
||||
|
||||
## Inside the chamber or passage.
|
||||
func inside(p: Vector3) -> bool:
|
||||
var l := to_local_pos(p)
|
||||
return (absf(l.x) < 2.0 and absf(l.z) < 2.0 or absf(l.x) < 0.6 and l.z > 1.9 and l.z < PASSAGE_END + 0.3) and l.y > -1.0 and l.y < 2.6
|
||||
|
||||
## The stone floor's height at `p` (world), or -INF outside.
|
||||
func floor_y_at(p: Vector3) -> float:
|
||||
var l := to_local_pos(p)
|
||||
if absf(l.x) < 2.0 and absf(l.z) < 2.0:
|
||||
return position.y + 0.03
|
||||
if absf(l.x) < 0.6 and l.z >= 2.0 and l.z < PASSAGE_END:
|
||||
return position.y + 0.03 - _mouth_drop * (l.z - 2.4) / (PASSAGE_END - 2.4) * float(l.z > 2.4)
|
||||
return -INF
|
||||
|
||||
func mouth() -> Vector3:
|
||||
var m := to_world(Vector3(0, 0, PASSAGE_END + 1.2))
|
||||
m.y = terrain.sample_height(m.x, m.z)
|
||||
return m
|
||||
|
||||
# --- guards ---------------------------------------------------------------------------
|
||||
|
||||
func is_night() -> bool:
|
||||
var h: float = world.day_night.hours()
|
||||
return h >= 19.5 or h < 5.5
|
||||
|
||||
## Whether being where `p` is wakes the guards right now.
|
||||
func wakes_for(p: Node3D) -> bool:
|
||||
if alarmed:
|
||||
return true
|
||||
if p.survival.dead:
|
||||
return false
|
||||
var near := Vector2(p.global_position.x - position.x, p.global_position.z - position.z).length() < RING_R
|
||||
if near and (is_night() or world.day_night.day > WAKE_DAY):
|
||||
alarmed = true
|
||||
return alarmed
|
||||
|
||||
func alarm() -> void:
|
||||
alarmed = true
|
||||
|
||||
func guards_alive() -> Array:
|
||||
return guards.filter(func(g): return is_instance_valid(g) and not g.is_dead())
|
||||
|
||||
## Starts the guards (on the machine that runs creatures).
|
||||
func start() -> void:
|
||||
if not found:
|
||||
return
|
||||
running = true
|
||||
_spawn_guards()
|
||||
|
||||
func _spawn_guards() -> void:
|
||||
for g in guards:
|
||||
if is_instance_valid(g) and not g.is_dead():
|
||||
g.queue_free()
|
||||
guards.clear()
|
||||
for i in range(POSTS.size()):
|
||||
if fallen.has(i):
|
||||
continue
|
||||
var g: AnimatableBody3D = GuardScript.new()
|
||||
var post: Vector3 = to_world(POSTS[i][0])
|
||||
g.setup("guard", world, post, terrain.noise_seed + 600 + i)
|
||||
g.barrow = self
|
||||
g.post = Vector3(post.x, position.y + 0.03, post.z)
|
||||
# Facing the mouth (+Z local); a creature's forward is -Z, so turn by PI.
|
||||
g.post_yaw = yaw + PI + float(POSTS[i][1])
|
||||
g.state = "rest"
|
||||
g.yaw = g.post_yaw
|
||||
g.rotation.y = g.yaw
|
||||
g.set_meta("post", i)
|
||||
g.died.connect(_on_guard_died)
|
||||
add_child(g)
|
||||
g.global_position = g.post
|
||||
guards.append(g)
|
||||
|
||||
func _on_guard_died(g: Node) -> void:
|
||||
var i := int(g.get_meta("post", -1))
|
||||
if i >= 0 and not fallen.has(i):
|
||||
fallen.append(i)
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if running:
|
||||
step()
|
||||
|
||||
func step() -> void:
|
||||
# Everyone's gone: the guards settle (they walk back to their posts).
|
||||
if alarmed:
|
||||
var anyone := false
|
||||
for p in world.players():
|
||||
if not p.survival.dead and p.global_position.distance_to(position) < LEASH:
|
||||
anyone = true
|
||||
if not anyone:
|
||||
alarmed = false
|
||||
# The coffin fills again (and the fallen are back) a few days after it
|
||||
# was emptied, at dawn, with nobody about.
|
||||
if loot_day >= 0 and world.day_night.day >= loot_day + RESTOCK_DAYS:
|
||||
var h: float = world.day_night.hours()
|
||||
if h >= 6.0 and h < 10.0:
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(position) < 30.0:
|
||||
return
|
||||
loot_day = -1
|
||||
fallen.clear()
|
||||
alarmed = false
|
||||
_spawn_guards()
|
||||
|
||||
# --- the coffin ---------------------------------------------------------------------------
|
||||
|
||||
var _coffin: StaticBody3D
|
||||
|
||||
func stocked() -> bool:
|
||||
return loot_day < 0
|
||||
|
||||
func days_to_restock() -> int:
|
||||
return maxi(loot_day + RESTOCK_DAYS - int(world.day_night.day), 1)
|
||||
|
||||
## Opens the coffin for `player` (Actions.loot_barrow). Wakes the guards.
|
||||
## Returns what was taken ({} if it was empty).
|
||||
func loot(player: Node3D) -> Dictionary:
|
||||
alarm()
|
||||
if not stocked():
|
||||
Actions.tell(player, "The coffin is empty. Come back in %d days." % days_to_restock())
|
||||
return {}
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = terrain.noise_seed * 31 + int(world.day_night.day)
|
||||
var got := {}
|
||||
for id in LOOT:
|
||||
got[id] = r.randi_range(int(LOOT[id][0]), int(LOOT[id][1]))
|
||||
loot_day = int(world.day_night.day)
|
||||
Actions.tell(player, "You lift the coffin lid... and something stirs behind you!")
|
||||
return got
|
||||
|
||||
func coffin_prompt() -> String:
|
||||
return "Stone coffin [E] Open"
|
||||
|
||||
func save_data() -> Dictionary:
|
||||
return {"loot_day": loot_day, "fallen": fallen.duplicate()} if found else {}
|
||||
|
||||
func load_data(d: Dictionary) -> void:
|
||||
if not found or d.is_empty():
|
||||
return
|
||||
loot_day = int(d.get("loot_day", -1))
|
||||
fallen = []
|
||||
for i in d.get("fallen", []):
|
||||
fallen.append(int(i))
|
||||
if running:
|
||||
_spawn_guards()
|
||||
|
||||
class Coffin extends StaticBody3D:
|
||||
var barrow: Node
|
||||
func interact_prompt(_p: Node) -> String:
|
||||
return barrow.coffin_prompt()
|
||||
func interact(p: Node) -> void:
|
||||
Actions.loot_barrow(p, barrow)
|
||||
@@ -64,6 +64,8 @@ var den: Node3D ## den.gd: the wolves' cave at the foot of the r
|
||||
var bobber: Node3D ## bobber.gd: the cast fishing float, if any
|
||||
var wolves: Node3D ## wolf_pack.gd (null if this seed has no den site)
|
||||
var bears: Node3D ## bear_range.gd: the island's bear (null online as a client)
|
||||
var thieves: Node3D ## thieves.gd: crows and foxes after food left out
|
||||
var barrow: Node3D ## barrow.gd: the burial mound and its guards (scenery; guards where creatures run)
|
||||
var _saved_bear: Dictionary = {} ## its saved state, kept until bears exists (and re-saved if it never does)
|
||||
var net_sync: Node ## net_sync.gd when playing on a server, else null
|
||||
var has_bed_spawn := false
|
||||
@@ -340,6 +342,13 @@ func _on_terrain_ready() -> void:
|
||||
if den.place(self, props._rock_mat):
|
||||
props.static_cuts.append([den.global_transform, 2.2, 3.4])
|
||||
_update_grass_cuts()
|
||||
# The barrow: scenery too (P3), out on open ground. Its guards only run
|
||||
# where creatures do (_setup_bear).
|
||||
barrow = preload("res://scripts/world/barrow.gd").new()
|
||||
barrow.name = "Barrow"
|
||||
add_child(barrow)
|
||||
if barrow.place(self):
|
||||
_update_grass_cuts()
|
||||
if DisplayServer.get_name() != "headless":
|
||||
var fish := preload("res://scripts/fishing/lake_fish.gd").new()
|
||||
fish.name = "LakeFish"
|
||||
@@ -392,9 +401,17 @@ func _on_terrain_ready() -> void:
|
||||
_setup_bear()
|
||||
_maybe_capture()
|
||||
|
||||
## The island's bear and its patch (bear_range.gd). The server runs it for
|
||||
## everyone; players' games see it through creature_net.gd.
|
||||
## The island's bear and its patch (bear_range.gd), and the crows and foxes
|
||||
## after food left out (thieves.gd). The server runs them for everyone;
|
||||
## players' games see them through creature_net.gd.
|
||||
func _setup_bear() -> void:
|
||||
thieves = preload("res://scripts/creatures/thieves.gd").new()
|
||||
thieves.name = "Thieves"
|
||||
add_child(thieves)
|
||||
thieves.setup(self)
|
||||
thieves.enabled = not "--no-creatures" in OS.get_cmdline_user_args()
|
||||
if barrow != null and barrow.found and thieves.enabled:
|
||||
barrow.start()
|
||||
bears = preload("res://scripts/creatures/bear_range.gd").new()
|
||||
bears.name = "Bear"
|
||||
add_child(bears)
|
||||
@@ -526,6 +543,7 @@ func collect_save() -> Dictionary:
|
||||
"pickups_taken": taken,
|
||||
"pieces": pieces,
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
"barrow": barrow.save_data() if barrow != null else {},
|
||||
}
|
||||
|
||||
func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, dropped: Array, pieces: Array) -> Dictionary:
|
||||
@@ -538,6 +556,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
|
||||
"pickups_taken": taken,
|
||||
"pieces": pieces,
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
"barrow": barrow.save_data() if barrow != null else {},
|
||||
}
|
||||
|
||||
## The live pieces (placed and not being removed).
|
||||
@@ -664,6 +683,8 @@ func _apply_save(s: Dictionary, with_player: bool) -> void:
|
||||
props.pickups[uid].collected()
|
||||
# Regrowth timers (older saves have none: everything gone starts a fresh timer).
|
||||
props.restore_regrow(s.get("regrow", {}))
|
||||
if barrow != null:
|
||||
barrow.load_data(s.get("barrow", {}))
|
||||
# The bear (bear_range.gd): applied when it's set up, after loading.
|
||||
_saved_bear = s.get("bear", {})
|
||||
if bears != null:
|
||||
@@ -816,6 +837,13 @@ func drop_item(id: String, count: int, xf: Transform3D, extra: Dictionary = {})
|
||||
return null
|
||||
return _make_dropped(id, count, xf, extra)
|
||||
|
||||
## Something other than a player took a dropped item (the bear ate it, a fox
|
||||
## ran off with it). On a server, everyone's game is told it's gone.
|
||||
func take_dropped(pk: Node3D) -> void:
|
||||
if server_mode and pk.has_meta("nid"):
|
||||
WorldNet.taken("#%d" % int(pk.get_meta("nid")))
|
||||
pk.collected()
|
||||
|
||||
func _make_dropped(id: String, count: int, xf: Transform3D, extra: Dictionary = {}) -> Node3D:
|
||||
var pk: Area3D = preload("res://scripts/world/pickup.gd").new()
|
||||
pk.setup(id, ItemDB.mesh_for(id), ItemDB.item_material(), 0.3)
|
||||
|
||||
Reference in New Issue
Block a user