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:
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
|
||||
Reference in New Issue
Block a user