262 lines
8.9 KiB
GDScript
262 lines
8.9 KiB
GDScript
extends "res://scripts/creatures/creature.gd"
|
|
## A wolf: a member of the pack that lives in the den (wolf_pack.gd decides
|
|
## what the pack is doing; this decides what one wolf does about it).
|
|
##
|
|
## Wary pack hunters rather than berserkers:
|
|
## - In the den by day they rest, but they defend it. Come within 14 m and
|
|
## they get up and watch you; within 8 m (or hit one) and they attack.
|
|
## - At night the pack stalks you. They circle at a distance and take turns
|
|
## darting in to bite, one wolf at a time.
|
|
## - Fire keeps them back: near a lit campfire they circle wide and never
|
|
## close in. A held torch makes them hesitate.
|
|
## - They can't bite through walls (a line-of-sight check), so a closed hut
|
|
## is safe.
|
|
## - A wolf on its own is timid: it keeps its distance and backs off.
|
|
## - A badly hurt wolf runs, and comes back another night.
|
|
##
|
|
## The alpha (kind "alpha", wolf moon nights only) is different: it isn't
|
|
## afraid of fire, it doesn't take turns, and if it can't get at you it
|
|
## goes for your door. A wooden door gives way after a few blows (door.gd);
|
|
## an iron-banded one holds. It doesn't run when hurt.
|
|
|
|
var pack: Node
|
|
var slot := 0
|
|
var wounded := false ## ran off hurt: stays away until dawn
|
|
|
|
func is_alpha() -> bool:
|
|
return kind == "alpha"
|
|
|
|
func display_name() -> String:
|
|
return def.name
|
|
|
|
func interact_prompt(p: Node) -> String:
|
|
var nm: String = def.name
|
|
if is_dead():
|
|
return "%s carcass [E] Butcher" % nm
|
|
var s := nm
|
|
match state:
|
|
"rest": s = nm + " (resting)"
|
|
"alert", "stalk": s = nm + " (watching you)"
|
|
"lunge", "defend": s = nm + " (attacking!)"
|
|
"bash": s = nm + " (breaking a door!)"
|
|
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 _pick_target() -> Node3D:
|
|
return pack.target_for(self)
|
|
|
|
func _think(dt: float) -> void:
|
|
var pp: Vector3 = player.global_position
|
|
var d := global_position.distance_to(pp)
|
|
_state_t -= dt
|
|
if state == "flee":
|
|
var away := global_position - _threat
|
|
steer(atan2(-away.x, -away.z))
|
|
if _state_t <= 0.0 and d > 30.0:
|
|
state = "home"
|
|
return
|
|
if wounded and pack.mode == "hunt":
|
|
_go_to(pack.home_slot(self))
|
|
state = "home"
|
|
return
|
|
match pack.mode:
|
|
"den":
|
|
_think_den(pp, d)
|
|
"hunt":
|
|
if is_alpha():
|
|
_think_alpha(dt, pp, d)
|
|
elif _aggro > 0.0 and d < 25.0:
|
|
_attack(pp, d, "defend")
|
|
elif pack.alive_count() <= 1:
|
|
_think_alone(pp, d)
|
|
else:
|
|
_think_hunt(pp, d)
|
|
_:
|
|
_think_den(pp, d)
|
|
|
|
## Daytime (and the howling night before hunts begin): rest at the den,
|
|
## defend it if you come too close.
|
|
func _think_den(pp: Vector3, d: float) -> void:
|
|
var den_d: float = pack.den_pos.distance_to(pp)
|
|
if (_aggro > 0.0 or den_d < pack.DEFEND_RADIUS) and d < 25.0:
|
|
_attack(pp, d, "defend")
|
|
return
|
|
if den_d < pack.ALERT_RADIUS:
|
|
state = "alert"
|
|
var to := pp - global_position
|
|
target_yaw = atan2(-to.x, -to.z)
|
|
return
|
|
var home: Vector3 = pack.home_slot(self)
|
|
if Vector2(home.x - global_position.x, home.z - global_position.z).length() > 1.2:
|
|
state = "home"
|
|
_go_to(home)
|
|
else:
|
|
state = "rest"
|
|
target_yaw = pack.den_yaw + (slot - 1.5) * 0.6
|
|
|
|
## Night, with the pack: circle, and dart in when it's your turn.
|
|
func _think_hunt(pp: Vector3, d: float) -> void:
|
|
match state:
|
|
"lunge":
|
|
# Break off first if you've reached a fire or it's taking too long.
|
|
if _state_t <= 0.0 or not pack.may_close_in(player):
|
|
state = "stalk"
|
|
pack.lunge_done(self)
|
|
else:
|
|
_attack(pp, d, "lunge")
|
|
"retreat":
|
|
var away := global_position - pp
|
|
steer(atan2(-away.x, -away.z))
|
|
if _state_t <= 0.0:
|
|
state = "stalk"
|
|
_:
|
|
state = "stalk"
|
|
_go_to(pack.circle_point(self))
|
|
if pack.request_lunge(self, player):
|
|
state = "lunge"
|
|
_state_t = 4.0
|
|
|
|
# --- the alpha ----------------------------------------------------------------
|
|
|
|
const BASH_STAND := 1.5 ## stands this far out from a door to hit it
|
|
const BASH_REACH := 2.4
|
|
const DOOR_SEARCH := 12.0 ## doors within this of the target are worth trying
|
|
|
|
var _door: Node3D = null ## the door it's working on
|
|
var _bash_at := Vector3.ZERO
|
|
var _door_look := 0.0
|
|
var _entry: Node3D = null ## an open doorway it's going in through
|
|
var _entry_out := Vector3.ZERO
|
|
var _entry_in := Vector3.ZERO
|
|
var _entry_stage := 0 ## 0: to the outside of it, 1: through it
|
|
|
|
## Straight at you if nothing's in the way (fire or no fire). Otherwise the
|
|
## nearest closed door near you, and through it. Otherwise prowl round.
|
|
func _think_alpha(dt: float, pp: Vector3, d: float) -> void:
|
|
if player.survival.dead:
|
|
state = "stalk"
|
|
_go_to(pack.circle_point(self))
|
|
return
|
|
if d < 25.0 and _can_reach_player():
|
|
_door = null
|
|
_attack(pp, d, "defend")
|
|
return
|
|
_door_look -= dt
|
|
if _door != null and (not is_instance_valid(_door) or _door.is_queued_for_deletion() or not _door.blocks()):
|
|
_door = null
|
|
if _door == null and _door_look <= 0.0:
|
|
_door_look = 1.0
|
|
_door = pack.door_for(self, player, DOOR_SEARCH)
|
|
if _door != null:
|
|
# Stand on the side of the door the alpha is on (the outside).
|
|
var n: Vector3 = _door.global_basis.z
|
|
var side := 1.0 if (global_position - _door.global_position).dot(n) > 0.0 else -1.0
|
|
_bash_at = _door.global_position + n * side * BASH_STAND
|
|
if _door == null:
|
|
# No shut door left: in through an open (or broken) one, if any.
|
|
if _entry == null or not is_instance_valid(_entry) or _entry.blocks():
|
|
_entry = pack.door_for(self, player, DOOR_SEARCH, true)
|
|
if _entry != null:
|
|
var n: Vector3 = _entry.global_basis.z
|
|
var side := 1.0 if (global_position - _entry.global_position).dot(n) > 0.0 else -1.0
|
|
_entry_out = _entry.global_position + n * side * BASH_STAND
|
|
_entry_in = _entry.global_position - n * side * 1.2
|
|
_entry_stage = 0
|
|
if _entry != null:
|
|
state = "bash" # (on its way in: shown the same)
|
|
var goal: Vector3 = _entry_out if _entry_stage == 0 else _entry_in
|
|
if Vector2(goal.x - global_position.x, goal.z - global_position.z).length() < 0.5:
|
|
_entry_stage += 1
|
|
if _entry_stage > 1:
|
|
_entry = null # inside: next think it can reach you
|
|
return
|
|
_go_to(goal)
|
|
return
|
|
state = "stalk"
|
|
_go_to(pack.circle_point(self))
|
|
return
|
|
state = "bash"
|
|
var flat := Vector2(_bash_at.x - global_position.x, _bash_at.z - global_position.z).length()
|
|
if flat > 0.6:
|
|
_go_to(_bash_at)
|
|
return
|
|
_goal_d = 0.0
|
|
var to := _door.global_position - global_position
|
|
steer(atan2(-to.x, -to.z))
|
|
if _attack_cd <= 0.0 and Vector2(to.x, to.z).length() < BASH_REACH:
|
|
_attack_cd = def.attack_cd
|
|
pack.bash_door(self, _door, float(def.get("door_damage", def.damage)))
|
|
|
|
## (Near the door it's battering, the door itself isn't an obstacle.)
|
|
func _blocked(dir: Vector3) -> bool:
|
|
if state == "bash" and _door != null and global_position.distance_to(_bash_at) < 2.5:
|
|
return false
|
|
if state == "bash" and _entry != null and is_instance_valid(_entry) and global_position.distance_to(_entry.global_position) < 2.8:
|
|
return false # (lined up with the doorway: walking through it)
|
|
return super._blocked(dir)
|
|
|
|
## The last wolf of a pack keeps its distance and won't start a fight.
|
|
func _think_alone(pp: Vector3, d: float) -> void:
|
|
if d < 10.0:
|
|
_threat = pp
|
|
state = "flee"
|
|
_state_t = 3.0
|
|
return
|
|
state = "stalk"
|
|
_go_to(pp + (global_position - pp).normalized() * 18.0)
|
|
|
|
## Run at the player and bite when in range with nothing in between.
|
|
func _attack(pp: Vector3, d: float, as_state: String) -> void:
|
|
state = as_state
|
|
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 " + String(def.name).to_lower())
|
|
if as_state == "lunge":
|
|
state = "retreat"
|
|
_state_t = 1.6
|
|
pack.lunge_done(self)
|
|
|
|
## No wall, door or rock between the wolf's jaws and the player.
|
|
func _can_reach_player() -> bool:
|
|
var from := global_position + Vector3(0, 0.8, 0)
|
|
var to: Vector3 = player.global_position + Vector3(0, 1.0, 0)
|
|
var q := PhysicsRayQueryParameters3D.create(from, to, 1)
|
|
q.exclude = [get_rid(), player.get_rid(), terrain.get_node("TerrainBody").get_rid()]
|
|
return get_world_3d().direct_space_state.intersect_ray(q).is_empty()
|
|
|
|
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()
|
|
|
|
var _goal_d := 0.0
|
|
|
|
func _desired_speed() -> float:
|
|
match state:
|
|
"rest", "alert": return 0.0
|
|
"home": return 2.2 if _goal_d < 8.0 else 6.0
|
|
"stalk": return 0.0 if _goal_d < 1.0 else (3.4 if _goal_d < 7.0 else 7.5)
|
|
"lunge", "defend": return def.run
|
|
"bash": return 0.0 if _goal_d < 0.6 else (3.0 if _goal_d < 4.0 else def.run)
|
|
"retreat": return 6.0
|
|
"flee": return def.run
|
|
return 0.0
|
|
|
|
func _react(from: Vector3) -> void:
|
|
pack.alert(from)
|
|
if is_alpha():
|
|
_aggro = 20.0 # (never runs)
|
|
return
|
|
if hp < def.hp * 0.4:
|
|
wounded = true
|
|
_threat = from
|
|
state = "flee"
|
|
_state_t = 6.0
|
|
else:
|
|
_aggro = 20.0
|