214 lines
7.0 KiB
GDScript
214 lines
7.0 KiB
GDScript
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 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
|
|
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)
|
|
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:
|
|
Actions.tell(p, what)
|
|
raided.emit(target, eaten)
|
|
return eaten
|