217 lines
6.5 KiB
GDScript
217 lines
6.5 KiB
GDScript
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"
|