P3 bear: one bear per island raids unattended food, palisades and closed huts keep it out, warns then charges, hide/fat/meat, fur rug (Cozy), death saved (protocol 8)
This commit is contained in:
@@ -63,7 +63,7 @@ func add_fuel(seconds: float) -> void:
|
||||
_set_lit(true)
|
||||
|
||||
func interact_prompt(player: Node) -> String:
|
||||
var has_fuel: bool = player.inventory.count("wood") + player.inventory.count("stick") > 0
|
||||
var has_fuel: bool = player.inventory.count("wood") + player.inventory.count("stick") + player.inventory.count("bear_fat") > 0
|
||||
if _lit:
|
||||
var left := "%d:%02d" % [int(fuel) / 60, int(fuel) % 60]
|
||||
var hiss := ", hissing in the rain" if burn_rate() > 1.2 else ""
|
||||
|
||||
22
scripts/building/rug.gd
Normal file
22
scripts/building/rug.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
extends "res://scripts/building/piece.gd"
|
||||
## Fur rug: a bearskin on the floor. In a sheltered room it makes you Cozy
|
||||
## (survival.gd): a little warmer, and Rested twice as fast. Flat, so you
|
||||
## walk over it (it's only on the interactables layer, for X to take it up).
|
||||
|
||||
const COZY_RADIUS := 4.0
|
||||
|
||||
func _make_collision(mi: MeshInstance3D) -> void:
|
||||
var aabb := mi.mesh.get_aabb()
|
||||
var cs := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = Vector3(aabb.size.x, 0.06, aabb.size.z)
|
||||
cs.shape = box
|
||||
cs.position = Vector3(aabb.get_center().x, 0.03, aabb.get_center().z)
|
||||
add_child(cs)
|
||||
collision_layer = 2
|
||||
|
||||
func _build() -> void:
|
||||
add_to_group("rug")
|
||||
|
||||
func interact_prompt(_player: Node) -> String:
|
||||
return "Fur rug (cozy in a sheltered room)"
|
||||
@@ -619,7 +619,7 @@ func add_fuel(player: Node3D, fire: Node3D) -> bool:
|
||||
if fire.fuel >= fire.MAX_FUEL - 1.0:
|
||||
notify.emit("The fire is fully stoked")
|
||||
return false
|
||||
for id in ["wood", "stick", "fiber"]:
|
||||
for id in ["wood", "bear_fat", "stick", "fiber"]:
|
||||
if player.inventory.count(id) > 0:
|
||||
player.inventory.remove(id, 1)
|
||||
fire.add_fuel(float(ItemDB.get_item(id).get("fuel", 10)))
|
||||
|
||||
216
scripts/creatures/bear.gd
Normal file
216
scripts/creatures/bear.gd
Normal file
@@ -0,0 +1,216 @@
|
||||
extends "res://scripts/creatures/creature.gd"
|
||||
## The bear (docs/PROGRESSION_DESIGN.md §4): one per island, living in the
|
||||
## deep forest by the rim (bear_range.gd finds the spot and brings it back
|
||||
## after it's killed).
|
||||
##
|
||||
## By day it ambles round its patch. From day 3 it sniffs out food left
|
||||
## unattended: food in a chest, or lying on the ground, with nobody about.
|
||||
## It walks over, eats every bit of food (nothing else) and wanders off. A
|
||||
## palisade ring or a closed hut keeps it out: it can't get at food it can't
|
||||
## walk to, and it doesn't break things.
|
||||
##
|
||||
## It won't start a fight, but it won't be crowded: closer than 8 m it rears
|
||||
## up and warns you, closer than 6 m after that and it charges. Hit it and it
|
||||
## charges too. Badly hurt, it runs home. At night it sleeps at its home.
|
||||
|
||||
const WARN_DIST := 8.0
|
||||
const CHARGE_DIST := 6.0
|
||||
const EAT_SECONDS := 8.0
|
||||
const RAID_COOLDOWN := 40.0 ## after a meal, before it goes looking again
|
||||
const SNIFF_EVERY := 2.0
|
||||
const STUCK_GIVE_UP := 15.0 ## no closer for this long: forget that food for a while
|
||||
|
||||
var range_: Node ## bear_range.gd
|
||||
var food: Node3D = null ## what it's going for
|
||||
var _raid_cd := 0.0
|
||||
var _sniff := 0.0
|
||||
var _eat_t := 0.0
|
||||
var _stuck_t := 0.0
|
||||
var _last_d := INF
|
||||
var _skip := {} ## food instance id -> seconds to ignore it
|
||||
var _goal_d := 0.0
|
||||
|
||||
func display_name() -> String:
|
||||
return def.name
|
||||
|
||||
func interact_prompt(p: Node) -> String:
|
||||
if is_dead():
|
||||
return "Bear carcass [E] Butcher"
|
||||
var s := "Bear"
|
||||
match state:
|
||||
"rest": s = "Bear (asleep)"
|
||||
"raid": s = "Bear (following its nose)"
|
||||
"eat": s = "Bear (eating)"
|
||||
"warn": s = "Bear (warning you off!)"
|
||||
"charge": s = "Bear (angry!)"
|
||||
"flee": s = "Bear (running off)"
|
||||
if not Actions.melee_stats(p).armed and not ItemDB.get_item(p.held_item_id()).has("bow"):
|
||||
s += " (you need a weapon)"
|
||||
return s
|
||||
|
||||
func _think(dt: float) -> void:
|
||||
var pp: Vector3 = player.global_position
|
||||
var d := global_position.distance_to(pp)
|
||||
_state_t -= dt
|
||||
_raid_cd = maxf(_raid_cd - dt, 0.0)
|
||||
for k in _skip.keys():
|
||||
_skip[k] = float(_skip[k]) - dt
|
||||
if float(_skip[k]) <= 0.0:
|
||||
_skip.erase(k)
|
||||
if state == "flee":
|
||||
var away := global_position - _threat
|
||||
steer(atan2(-away.x, -away.z))
|
||||
if _state_t <= 0.0:
|
||||
state = "wander"
|
||||
return
|
||||
if _aggro > 0.0:
|
||||
if player.survival.dead or d > 40.0:
|
||||
_aggro = 0.0
|
||||
state = "wander"
|
||||
else:
|
||||
state = "charge"
|
||||
var to := pp - global_position
|
||||
steer(atan2(-to.x, -to.z))
|
||||
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 bear")
|
||||
return
|
||||
if not range_.awake():
|
||||
_go_home_to_sleep()
|
||||
return
|
||||
# Too close: rear up, then charge if you don't back off.
|
||||
if d < WARN_DIST and not player.survival.dead and _can_reach(player):
|
||||
var to := pp - global_position
|
||||
steer(atan2(-to.x, -to.z))
|
||||
if state != "warn":
|
||||
state = "warn"
|
||||
_state_t = 2.5
|
||||
elif _state_t <= 0.0 and d < CHARGE_DIST:
|
||||
_aggro = 12.0
|
||||
_attack_cd = 0.3
|
||||
state = "charge"
|
||||
return
|
||||
if state == "warn":
|
||||
state = "wander"
|
||||
match state:
|
||||
"eat":
|
||||
_eat_t -= dt
|
||||
if _eat_t <= 0.0:
|
||||
state = "wander"
|
||||
_raid_cd = RAID_COOLDOWN
|
||||
"raid":
|
||||
_raid_step(dt)
|
||||
_:
|
||||
_sniff -= dt
|
||||
if _sniff <= 0.0 and _raid_cd <= 0.0 and range_.raids_now():
|
||||
_sniff = SNIFF_EVERY
|
||||
var f: Node3D = range_.find_food(self, _skip)
|
||||
if f != null:
|
||||
food = f
|
||||
state = "raid"
|
||||
_last_d = INF
|
||||
_stuck_t = 0.0
|
||||
return
|
||||
_amble(dt)
|
||||
|
||||
func _raid_step(dt: float) -> void:
|
||||
if food == null or not is_instance_valid(food) or not range_.raidable(food):
|
||||
food = null
|
||||
state = "wander"
|
||||
return
|
||||
var fp: Vector3 = food.global_position
|
||||
var fd := Vector2(fp.x - global_position.x, fp.z - global_position.z).length()
|
||||
if fd < 1.9:
|
||||
range_.raid(self, food)
|
||||
food = null
|
||||
state = "eat"
|
||||
_eat_t = EAT_SECONDS
|
||||
_goal_d = 0.0
|
||||
return
|
||||
_go_to(fp)
|
||||
if fd > _last_d - 0.05 * dt * 10.0:
|
||||
_stuck_t += dt
|
||||
else:
|
||||
_stuck_t = 0.0
|
||||
_last_d = minf(_last_d, fd)
|
||||
if _stuck_t > STUCK_GIVE_UP:
|
||||
_skip[food.get_instance_id()] = 120.0
|
||||
food = null
|
||||
state = "wander"
|
||||
|
||||
## Idles and wanders like any animal, but never far from home.
|
||||
func _amble(dt: float) -> void:
|
||||
var home: Vector3 = range_.home
|
||||
if Vector2(home.x - global_position.x, home.z - global_position.z).length() > range_.HOME_RADIUS:
|
||||
state = "home"
|
||||
_go_to(home)
|
||||
return
|
||||
if state == "home":
|
||||
state = "wander"
|
||||
_state_t = rng.randf_range(3.0, 8.0)
|
||||
if _state_t <= 0.0:
|
||||
if state == "idle":
|
||||
state = "wander"
|
||||
target_yaw = yaw + rng.randf_range(-1.6, 1.6)
|
||||
_state_t = rng.randf_range(4.0, 9.0)
|
||||
else:
|
||||
state = "idle"
|
||||
_state_t = rng.randf_range(3.0, 7.0)
|
||||
|
||||
func _go_home_to_sleep() -> void:
|
||||
var home: Vector3 = range_.home
|
||||
if Vector2(home.x - global_position.x, home.z - global_position.z).length() > 2.0:
|
||||
state = "home"
|
||||
_go_to(home)
|
||||
else:
|
||||
state = "rest"
|
||||
|
||||
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:
|
||||
"wander": return def.walk
|
||||
"raid": return 0.0 if _goal_d < 1.0 else 3.2
|
||||
"home": return 0.0 if _goal_d < 1.0 else 3.0
|
||||
"charge", "flee": return def.run
|
||||
return 0.0
|
||||
|
||||
## (The chest it's going for isn't in its way: it walks right up to it.)
|
||||
func _blocked(dir: Vector3) -> bool:
|
||||
if state == "raid" and food != null and is_instance_valid(food) and global_position.distance_to(food.global_position) < 3.5:
|
||||
var probe := global_position + dir * 1.4
|
||||
var h: float = terrain.sample_height(probe.x, probe.z)
|
||||
return h < terrain.water_level + 0.2 or absf(h - global_position.y) > 1.1
|
||||
return super._blocked(dir)
|
||||
|
||||
## Rearing up on its hind legs when it warns you (also on puppets online).
|
||||
func _animate(dt: float) -> void:
|
||||
if state == "warn":
|
||||
_visual.rotation.x = lerpf(_visual.rotation.x, 0.6, minf(dt * 4.0, 1.0))
|
||||
_visual.position.y = lerpf(_visual.position.y, 0.25, minf(dt * 4.0, 1.0))
|
||||
return
|
||||
super._animate(dt)
|
||||
|
||||
## No wall, door or rock between the bear and `p`.
|
||||
func _can_reach(p: Node3D) -> bool:
|
||||
var from := global_position + Vector3(0, 0.9, 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()
|
||||
|
||||
func _react(from: Vector3) -> void:
|
||||
if hp < def.hp * 0.25:
|
||||
_aggro = 0.0
|
||||
_threat = from
|
||||
state = "flee"
|
||||
_state_t = 10.0
|
||||
return
|
||||
_aggro = 25.0
|
||||
_attack_cd = maxf(_attack_cd, 0.4)
|
||||
state = "charge"
|
||||
211
scripts/creatures/bear_range.gd
Normal file
211
scripts/creatures/bear_range.gd
Normal file
@@ -0,0 +1,211 @@
|
||||
extends Node3D
|
||||
## The bear's patch (docs/PROGRESSION_DESIGN.md §4): where the island's one
|
||||
## bear lives, bringing it back after it's killed, and the food it can get at.
|
||||
##
|
||||
## Home is the densest forest near the rim, away from the spawn and the wolf
|
||||
## den, picked from the seed (the same on every machine). The bear is there
|
||||
## from day 1; raids start on day 3. Killed, it's back RESPAWN_DAYS later,
|
||||
## at dawn, when nobody's near its home.
|
||||
##
|
||||
## Food it can raid: a chest with food in it, or food left on the ground,
|
||||
## within SMELL of the bear, with nobody within GUARD of it, and not fenced
|
||||
## in. Fenced in = every one of 32 level rays from it hits a building piece
|
||||
## (a palisade ring, the walls of a closed hut; an open door lets it in).
|
||||
|
||||
const BearScript = preload("res://scripts/creatures/bear.gd")
|
||||
|
||||
const RAID_DAY := 3
|
||||
const RESPAWN_DAYS := 3
|
||||
const SMELL := 60.0
|
||||
const GUARD := 12.0
|
||||
const HOME_RADIUS := 35.0
|
||||
const FENCE_RAY := 25.0
|
||||
const FENCE_RAYS := 32 ## fine enough that an open doorway lets one through
|
||||
|
||||
signal raided(target: Node3D, eaten: int)
|
||||
|
||||
var world: Node3D
|
||||
var home := Vector3.ZERO
|
||||
var found := false
|
||||
var bear: Node3D = null
|
||||
var enabled := true
|
||||
var dead_day := -1 ## the day it was killed (-1: alive or never)
|
||||
var _rng := RandomNumberGenerator.new()
|
||||
|
||||
func setup(w: Node3D) -> void:
|
||||
world = w
|
||||
_rng.seed = w.terrain.noise_seed + 881
|
||||
found = _find_home()
|
||||
if found:
|
||||
spawn_bear()
|
||||
|
||||
## The densest stand of trees near the rim, at least 60 m from the spawn
|
||||
## and the den.
|
||||
func _find_home() -> bool:
|
||||
var terrain: Node3D = world.terrain
|
||||
var trees: Array = []
|
||||
for n in world.props.resource_nodes.values():
|
||||
if n.kind == "tree":
|
||||
trees.append((n as Node3D).global_position)
|
||||
if trees.is_empty():
|
||||
return false
|
||||
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 := -1.0
|
||||
var r := RandomNumberGenerator.new()
|
||||
r.seed = _rng.seed
|
||||
for _i in range(120):
|
||||
var c: Vector3 = trees[r.randi() % trees.size()]
|
||||
var rim: float = world.props._near_rim(c.x, c.z)
|
||||
if rim < 0.1 or rim > 0.9 or not terrain.inside_boundary(c.x, c.z, 10.0):
|
||||
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 terrain.sample_height(c.x, c.z) < terrain.water_level + 1.0:
|
||||
continue
|
||||
var near := 0
|
||||
for t: Vector3 in trees:
|
||||
if absf(t.x - c.x) < 18.0 and absf(t.z - c.z) < 18.0:
|
||||
near += 1
|
||||
var score := near + rim * 6.0
|
||||
if score > best:
|
||||
best = score
|
||||
# Stand just beside the tree, not in its trunk.
|
||||
home = c + Vector3(1.6, 0, 0)
|
||||
home.y = terrain.sample_height(home.x, home.z)
|
||||
return best > 0.0
|
||||
|
||||
func spawn_bear() -> Node3D:
|
||||
var b: AnimatableBody3D = BearScript.new()
|
||||
b.setup("bear", world, home, _rng.randi())
|
||||
b.range_ = self
|
||||
b.state = "rest" if not awake() else "idle"
|
||||
add_child(b)
|
||||
b.died.connect(func(_c): dead_day = world.day_night.day)
|
||||
bear = b
|
||||
dead_day = -1
|
||||
return b
|
||||
|
||||
func alive() -> bool:
|
||||
return bear != null and is_instance_valid(bear) and not bear.is_dead()
|
||||
|
||||
## Saved with the world: whether it's dead (and since when), and how hurt
|
||||
## it is. Where it is isn't kept: it goes home over a reload.
|
||||
func save_data() -> Dictionary:
|
||||
if not found:
|
||||
return {}
|
||||
return {"dead_day": dead_day, "hp": float(bear.hp) if alive() else 0.0}
|
||||
|
||||
func load_data(d: Dictionary) -> void:
|
||||
if not found or d.is_empty():
|
||||
return
|
||||
var dd := int(d.get("dead_day", -1))
|
||||
if dd >= 0:
|
||||
if bear != null and is_instance_valid(bear):
|
||||
bear.queue_free()
|
||||
bear = null
|
||||
dead_day = dd
|
||||
return
|
||||
if not alive():
|
||||
spawn_bear()
|
||||
var hp := float(d.get("hp", 0.0))
|
||||
if hp > 0.0:
|
||||
bear.hp = minf(hp, float(bear.def.hp))
|
||||
|
||||
func awake() -> bool:
|
||||
var h: float = world.day_night.hours()
|
||||
return h >= 6.0 and h < 19.5
|
||||
|
||||
func raids_now() -> bool:
|
||||
return world.day_night.day >= RAID_DAY and awake()
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
if enabled:
|
||||
step()
|
||||
|
||||
## Brings the bear back a few days after it was killed (at dawn, unseen).
|
||||
func step() -> void:
|
||||
if not found or alive() or dead_day < 0:
|
||||
return
|
||||
var h: float = world.day_night.hours()
|
||||
if world.day_night.day >= dead_day + RESPAWN_DAYS and h >= 6.0 and h < 9.0:
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(home) < 40.0:
|
||||
return
|
||||
spawn_bear()
|
||||
|
||||
# --- food --------------------------------------------------------------------------
|
||||
|
||||
static func is_food(id: String) -> bool:
|
||||
return ItemDB.get_item(id).has("food")
|
||||
|
||||
## Food that's worth the bear's while: `target` still has food in it.
|
||||
func has_food(target: Node3D) -> bool:
|
||||
if target.get("inventory") != null:
|
||||
for s in target.inventory.slots:
|
||||
if s != null and is_food(s.id):
|
||||
return true
|
||||
return false
|
||||
return target.get("item_id") != null and is_food(target.item_id) and not target.taken
|
||||
|
||||
## Food the bear could go for now: has food, nobody guarding it, not fenced in.
|
||||
func raidable(target: Node3D) -> bool:
|
||||
if not is_instance_valid(target) or target.is_queued_for_deletion() or not has_food(target):
|
||||
return false
|
||||
for p in world.players():
|
||||
if p.global_position.distance_to(target.global_position) < GUARD and not p.survival.dead:
|
||||
return false
|
||||
return not fenced(target.global_position)
|
||||
|
||||
## Walled in on every side (a palisade ring, a closed hut).
|
||||
func fenced(pos: Vector3) -> bool:
|
||||
var from := pos + Vector3(0, 0.6, 0)
|
||||
var space := world.get_world_3d().direct_space_state
|
||||
for i in range(FENCE_RAYS):
|
||||
var a := TAU * i / FENCE_RAYS
|
||||
var q := PhysicsRayQueryParameters3D.create(from, from + Vector3(cos(a), 0, sin(a)) * FENCE_RAY, world.BuildPiece.BUILD_LAYER)
|
||||
if space.intersect_ray(q).is_empty():
|
||||
return false
|
||||
return true
|
||||
|
||||
## The nearest food `b` can smell and get at (null: none).
|
||||
func find_food(b: Node3D, skip: Dictionary = {}) -> Node3D:
|
||||
var best: Node3D = null
|
||||
var best_d := SMELL
|
||||
var cands: Array = []
|
||||
for p in world.all_pieces():
|
||||
if p.item_id == "chest":
|
||||
cands.append(p)
|
||||
for d in world.all_dropped():
|
||||
if is_food(d.item_id):
|
||||
cands.append(d)
|
||||
for c in cands:
|
||||
if skip.has(c.get_instance_id()):
|
||||
continue
|
||||
var dist: float = c.global_position.distance_to(b.global_position)
|
||||
if dist < best_d and raidable(c):
|
||||
best_d = dist
|
||||
best = c
|
||||
return best
|
||||
|
||||
## The bear eats every bit of food in `target`. Tells anyone nearby.
|
||||
func raid(b: Node3D, target: Node3D) -> int:
|
||||
var eaten := 0
|
||||
var what := ""
|
||||
if target.get("inventory") != null:
|
||||
var inv: Inventory = target.inventory
|
||||
for i in range(inv.size()):
|
||||
var s = inv.get_slot(i)
|
||||
if s != null and is_food(s.id):
|
||||
eaten += int(s.count)
|
||||
inv.set_slot(i, null)
|
||||
what = "A bear has broken into your chest and eaten the food!"
|
||||
else:
|
||||
eaten = int(target.amount)
|
||||
target.collected()
|
||||
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:
|
||||
Actions.tell(p, what)
|
||||
raided.emit(target, eaten)
|
||||
return eaten
|
||||
@@ -46,6 +46,12 @@ 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]}},
|
||||
# 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,
|
||||
"notice": 0.0, "notice_loud": 0.0, "calm": 40.0,
|
||||
"damage": 24.0, "attack_range": 2.3, "attack_cd": 1.8,
|
||||
"drops": {"raw_meat": [5, 7], "bear_hide": [1, 1], "bear_fat": [2, 3]}},
|
||||
}
|
||||
const CARCASS_SECONDS := 300.0
|
||||
|
||||
@@ -137,7 +143,7 @@ func setup(k: String, w: Node3D, pos: Vector3, seed: int) -> void:
|
||||
cs.position = Vector3(0, parts.height * 0.5, 0)
|
||||
if k != "rabbit":
|
||||
# Long bodies: lay the capsule along the spine.
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65}.get(k, 1.4)
|
||||
cap.height = {"deer": 1.5, "boar": 1.4, "wolf": 1.25, "alpha": 1.65, "bear": 2.0}.get(k, 1.4)
|
||||
cs.rotation.x = PI * 0.5
|
||||
cs.position = Vector3(0, parts.height * 0.55, 0)
|
||||
add_child(cs)
|
||||
|
||||
@@ -19,6 +19,7 @@ static func build(kind: String) -> Dictionary:
|
||||
"deer": d = _deer()
|
||||
"wolf": d = _wolf()
|
||||
"alpha": d = _wolf(true)
|
||||
"bear": d = _bear()
|
||||
_: d = _boar()
|
||||
_cache[kind] = d
|
||||
return d
|
||||
@@ -153,3 +154,27 @@ static func _wolf_parts(coat: Color, pale: Color, seed: int) -> Dictionary:
|
||||
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)],
|
||||
"radius": 0.32, "height": 1.0}
|
||||
|
||||
## Brown bear: a heavy humped body, a broad head with a pale muzzle and
|
||||
## round ears, thick short legs.
|
||||
static func _bear() -> Dictionary:
|
||||
var coat := Color(0.36, 0.24, 0.15)
|
||||
var hump := Color(0.42, 0.29, 0.18)
|
||||
var muzzle := Color(0.58, 0.45, 0.32)
|
||||
var dark := Color(0.1, 0.08, 0.07)
|
||||
var st := P._begin()
|
||||
var r := _rng(261)
|
||||
var n := _noise(261, 1.3)
|
||||
P._blob(st, Vector3(0, 0.95, 0.15), Vector3(0.5, 0.5, 0.85), n, 0.14, coat, r, 1) # body
|
||||
P._blob(st, Vector3(0, 1.22, -0.35), Vector3(0.44, 0.38, 0.42), n, 0.18, hump, r, 1) # shoulder hump
|
||||
P._blob(st, Vector3(0, 1.05, -0.95), Vector3(0.3, 0.27, 0.3), n, 0.12, coat, r, 1) # head
|
||||
P._cylinder(st, Vector3(0, 0.98, -1.12), Vector3(0, 0.92, -1.36), 0.15, 0.1, 6, muzzle, r, -1.0, dark)
|
||||
for s in [-1.0, 1.0]:
|
||||
P._blob(st, Vector3(s * 0.2, 1.3, -0.9), Vector3(0.08, 0.08, 0.05), n, 0.1, P._mul(coat, 0.85), r, 0) # ears
|
||||
P._blob(st, Vector3(0, 0.98, 0.98), Vector3(0.08, 0.08, 0.07), n, 0.2, coat, r, 0) # stub tail
|
||||
var est := P._begin()
|
||||
for s in [-1.0, 1.0]:
|
||||
P._blob(est, Vector3(s * 0.13, 1.12, -1.18), Vector3.ONE * 0.025, n, 0.0, Color(0.9, 0.6, 0.25), r, 0)
|
||||
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}
|
||||
|
||||
@@ -112,6 +112,9 @@ static func build(key: String) -> ArrayMesh:
|
||||
"alpha_pelt": m = _pelt(ALPHA_FUR)
|
||||
"alpha_cloak": m = _cloak(ALPHA_FUR)
|
||||
"wolf_fang": m = _fang()
|
||||
"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()
|
||||
"berry_bush": m = _berry_bush(true)
|
||||
"berry_bush_bare": m = _berry_bush(false)
|
||||
_:
|
||||
@@ -979,3 +982,23 @@ static func _fang() -> ArrayMesh:
|
||||
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)
|
||||
|
||||
# --- P3 the bear ---------------------------------------------------------------------
|
||||
|
||||
const BEAR_FUR := Color(0.36, 0.24, 0.15)
|
||||
|
||||
## A bearskin laid flat: body, four splayed legs and the head toward -Z.
|
||||
static func _rug() -> ArrayMesh:
|
||||
var st := P._begin()
|
||||
var r := _rng(273)
|
||||
_box(st, Vector3(0, 0.015, 0.1), Vector3(1.0, 0.03, 1.5), BEAR_FUR)
|
||||
_box(st, Vector3(0, 0.02, 0.1), Vector3(0.8, 0.03, 1.3), P._mul(BEAR_FUR, 1.12))
|
||||
for sx in [-1.0, 1.0]:
|
||||
for sz in [-0.45, 0.6]:
|
||||
_box(st, Vector3(sx * 0.62, 0.012, sz), Vector3(0.42, 0.024, 0.24), _board(r, BEAR_FUR), Basis(Vector3.UP, sx * (0.5 if sz < 0 else -0.5)))
|
||||
_box(st, Vector3(0, 0.05, -0.85), Vector3(0.38, 0.1, 0.34), P._mul(BEAR_FUR, 0.95))
|
||||
_box(st, Vector3(0, 0.045, -1.06), Vector3(0.18, 0.08, 0.14), Color(0.58, 0.45, 0.32))
|
||||
for sx in [-0.14, 0.14]:
|
||||
_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)
|
||||
|
||||
@@ -18,6 +18,7 @@ extends Node
|
||||
## missing from the snapshots for a second (despawned, butchered) goes.
|
||||
|
||||
const Creature = preload("res://scripts/creatures/creature.gd")
|
||||
const BearScript = preload("res://scripts/creatures/bear.gd")
|
||||
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")
|
||||
@@ -95,8 +96,9 @@ func _server_tick(w: Node3D, delta: float) -> void:
|
||||
|
||||
const PER_PACKET := 40
|
||||
const ENTRY := 23
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha"]
|
||||
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash"]
|
||||
const KIND_IDS := ["rabbit", "deer", "boar", "wolf", "alpha", "bear"]
|
||||
const STATE_IDS := ["idle", "wander", "flee", "charge", "dead", "rest", "alert", "stalk", "lunge", "retreat", "home", "defend", "bash",
|
||||
"warn", "raid", "eat"]
|
||||
|
||||
## One creature in 23 bytes: id, kind, state, position, facing, speed, health.
|
||||
static func _encode(buf: PackedByteArray, o: int, c: Node3D) -> void:
|
||||
@@ -192,7 +194,7 @@ 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 = Creature.new()
|
||||
c = BearScript.new() if kind == "bear" else Creature.new() # (the bear rears up when it warns you)
|
||||
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 := 7 ## 3: shared world; 4: the server decides; 5: wildlife; 6: rules and chat (M2.6); 7: the alpha, broken doors (P3)
|
||||
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 DEFAULT_PORT := 24242
|
||||
const HANDSHAKE_TIMEOUT := 10.0
|
||||
const JOIN_TIMEOUT := 20.0 ## includes the server building its terrain on first join
|
||||
|
||||
@@ -426,7 +426,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"]) and _v(a, 1).is_finite():
|
||||
if cheats and Creature.KINDS.has(_s(a, 0)) and not (_s(a, 0) in ["wolf", "alpha", "bear"]) and _v(a, 1).is_finite():
|
||||
w.creatures.spawn(_s(a, 0), _v(a, 1))
|
||||
"debug_time":
|
||||
if cheats:
|
||||
|
||||
@@ -75,6 +75,14 @@ func balanced_diet() -> bool:
|
||||
|
||||
var stone_room := false ## sheltered by mostly stone walls (P2): a little warmer
|
||||
const STONE_ROOM_WARMTH := 6.0
|
||||
var cozy := false ## sheltered with a fur rug near (P3, the bear)
|
||||
const COZY_WARMTH := 4.0
|
||||
|
||||
func _rug_near(p: Vector3) -> bool:
|
||||
for r in world.get_tree().get_nodes_in_group("rug"):
|
||||
if (r as Node3D).global_position.distance_to(p) < r.COZY_RADIUS:
|
||||
return true
|
||||
return false
|
||||
|
||||
var player: Node3D ## the local player, or a server_player.gd on a server
|
||||
var world: Node3D
|
||||
@@ -96,12 +104,14 @@ func step(dt: float) -> void:
|
||||
roofed = world.build.roofed_at(p)
|
||||
sheltered = roofed and world.build.enclosure_at(p) >= 0.74
|
||||
stone_room = sheltered and world.build.stone_walls_at(p) >= 0.5
|
||||
cozy = sheltered and _rug_near(p)
|
||||
# Rain soaks you unless there's a roof over your head.
|
||||
if not roofed and world.weather.rain_at(p) > 0.25:
|
||||
wet = WET_SECONDS
|
||||
# Resting: a while by a fire with a roof and walls around you.
|
||||
# Resting: a while by a fire with a roof and walls around you (half the
|
||||
# time on a fur rug).
|
||||
if sheltered and heat > 0.25:
|
||||
_rest_build += dt
|
||||
_rest_build += dt * (2.0 if cozy else 1.0)
|
||||
if _rest_build >= REST_AFTER:
|
||||
rested = maxf(rested, REST_SECONDS)
|
||||
else:
|
||||
@@ -111,6 +121,8 @@ func step(dt: float) -> void:
|
||||
var target: float = world.air_warmth(p, sheltered) + heat * 70.0 + float(wear.get("warmth", 0.0))
|
||||
if stone_room:
|
||||
target += STONE_ROOM_WARMTH
|
||||
if cozy:
|
||||
target += COZY_WARMTH
|
||||
if wet > 0.0:
|
||||
target -= 22.0 * float(wear.get("wet_chill", 1.0))
|
||||
wet = maxf(wet - dt * (3.0 if heat > 0.3 else 1.0), 0.0) # fires dry you off
|
||||
@@ -186,6 +198,8 @@ func _update_statuses() -> void:
|
||||
statuses.append("Balanced diet")
|
||||
if sheltered:
|
||||
statuses.append("Sheltered")
|
||||
if cozy:
|
||||
statuses.append("Cozy")
|
||||
if rested > 0.0:
|
||||
statuses.append("Rested")
|
||||
for id in buffs:
|
||||
|
||||
@@ -28,6 +28,7 @@ const PIECES := {
|
||||
"smelter": preload("res://scripts/building/processor.gd"),
|
||||
"forge": preload("res://scripts/building/forge.gd"),
|
||||
"hearth": preload("res://scripts/building/hearth.gd"),
|
||||
"rug": preload("res://scripts/building/rug.gd"),
|
||||
"stonecutter": preload("res://scripts/building/stonecutter.gd"),
|
||||
}
|
||||
const BuildPiece = preload("res://scripts/building/build_piece.gd")
|
||||
@@ -62,6 +63,8 @@ var _dropped: Node3D ## arrows, spears and other dropped items
|
||||
var den: Node3D ## den.gd: the wolves' cave at the foot of the rim
|
||||
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 _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
|
||||
var bed_spawn := Vector3.ZERO ## position of the bed you respawn at
|
||||
@@ -361,6 +364,7 @@ func _on_terrain_ready() -> void:
|
||||
wolves.name = "Wolves"
|
||||
add_child(wolves)
|
||||
wolves.setup(self, den)
|
||||
_setup_bear()
|
||||
apply_rules()
|
||||
return
|
||||
_respawn()
|
||||
@@ -384,8 +388,22 @@ func _on_terrain_ready() -> void:
|
||||
wolves.name = "Wolves"
|
||||
add_child(wolves)
|
||||
wolves.setup(self, den)
|
||||
if not "--no-creatures" in OS.get_cmdline_user_args():
|
||||
_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.
|
||||
func _setup_bear() -> void:
|
||||
bears = preload("res://scripts/creatures/bear_range.gd").new()
|
||||
bears.name = "Bear"
|
||||
add_child(bears)
|
||||
bears.setup(self)
|
||||
bears.load_data(_saved_bear)
|
||||
bears.enabled = not "--no-creatures" in OS.get_cmdline_user_args()
|
||||
if not bears.enabled and bears.bear != null:
|
||||
bears.bear.queue_free() # (a server started without wildlife)
|
||||
|
||||
## A server's rules for the island (at start-up, and after an admin's /set).
|
||||
func apply_rules() -> void:
|
||||
var R = preload("res://scripts/net/rules.gd")
|
||||
@@ -507,6 +525,7 @@ func collect_save() -> Dictionary:
|
||||
"harvested": harvested,
|
||||
"pickups_taken": taken,
|
||||
"pieces": pieces,
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
}
|
||||
|
||||
func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, dropped: Array, pieces: Array) -> Dictionary:
|
||||
@@ -518,6 +537,7 @@ func _shared_state(harvested: Dictionary, regrow: Dictionary, taken: Array, drop
|
||||
"harvested": harvested,
|
||||
"pickups_taken": taken,
|
||||
"pieces": pieces,
|
||||
"bear": bears.save_data() if bears != null else _saved_bear,
|
||||
}
|
||||
|
||||
## The live pieces (placed and not being removed).
|
||||
@@ -644,6 +664,10 @@ 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", {}))
|
||||
# The bear (bear_range.gd): applied when it's set up, after loading.
|
||||
_saved_bear = s.get("bear", {})
|
||||
if bears != null:
|
||||
bears.load_data(_saved_bear)
|
||||
var sp: Dictionary = s.get("spawn", {})
|
||||
has_bed_spawn = bool(sp.get("bed", false))
|
||||
if sp.has("pos"):
|
||||
|
||||
Reference in New Issue
Block a user