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. Nor ## is the person it's charging: it runs into them, not round them.) func _blocked(dir: Vector3) -> bool: if state == "charge" and player != null and global_position.distance_to(player.global_position) < 6.0: var probe := global_position + dir * (1.4 + speed * 0.15) var h: float = terrain.sample_height(probe.x, probe.z) if not terrain.inside_boundary(probe.x, probe.z, 6.0) or h < terrain.water_level + 0.2 or absf(h - global_position.y) > 1.1: return true var q := PhysicsRayQueryParameters3D.create(global_position + Vector3(0, 0.45, 0), Vector3(probe.x, h + 0.45, probe.z), 1) q.exclude = [get_rid(), player.get_rid(), terrain.get_node("TerrainBody").get_rid()] return not get_world_3d().direct_space_state.intersect_ray(q).is_empty() 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"