P3 alpha: wolf moon every 7th night, the alpha breaks wooden doors, iron-banded door, broken-door repair, alpha pelt and cloak (protocol 7)

This commit is contained in:
Paul Bailey
2026-11-05 01:40:52 +00:00
parent 411bfef34a
commit fc58c8d0e1
18 changed files with 742 additions and 40 deletions

View File

@@ -13,22 +13,32 @@ extends "res://scripts/creatures/creature.gd"
## 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 "Wolf"
return def.name
func interact_prompt(p: Node) -> String:
var nm: String = def.name
if is_dead():
return "Wolf carcass [E] Butcher"
var s := "Wolf"
return "%s carcass [E] Butcher" % nm
var s := nm
match state:
"rest": s = "Wolf (resting)"
"alert", "stalk": s = "Wolf (watching you)"
"lunge", "defend": s = "Wolf (attacking!)"
"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
@@ -54,7 +64,9 @@ func _think(dt: float) -> void:
"den":
_think_den(pp, d)
"hunt":
if _aggro > 0.0 and d < 25.0:
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)
@@ -105,6 +117,85 @@ func _think_hunt(pp: Vector3, d: float) -> void:
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:
@@ -151,12 +242,16 @@ func _desired_speed() -> float:
"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