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