class_name BotDirector extends RefCounted var game: Node3D var routes: BotRoutes var steering: Dictionary[int, BotSteering] = {} func _init(owner_game: Node3D) -> void: game = owner_game routes = BotRoutes.new(game.arena) func update(p: Fighter, dt: float) -> void: if p.hp <= 0: return p.bot_think -= dt if not steering.has(p.peer_id): steering[p.peer_id] = BotSteering.new() var motion: BotSteering = steering[p.peer_id] var skill := BotSkill.profile(int(game.config.get("difficulty", 0))) var enemy: Fighter = null var nearest: float = skill.range for candidate: Fighter in game.players.values(): if not game.enemies(p, candidate) or candidate.hp <= 0: continue var distance := p.global_position.distance_to(candidate.global_position) if distance < nearest and game.visible_between(p.eye(), candidate.eye(), [p.get_rid(), candidate.get_rid()]): nearest = distance enemy = candidate var shooting := false if enemy: if p.bot_enemy != enemy.peer_id: p.bot_enemy = enemy.peer_id p.bot_acquired = 0 p.bot_acquired += dt # Aim at the torso; difficulty affects aim and decisions, never damage/HP. var direction := (enemy.eye() - Vector3.UP * 0.25 - p.eye()).normalized() var target_yaw := atan2(-direction.x, -direction.z) var target_pitch := asin(direction.y) var clock := Time.get_ticks_msec() * 0.0025 + p.peer_id * 1.7 p.yaw = lerp_angle(p.yaw, target_yaw + sin(clock) * float(skill.error), minf(dt * float(skill.tracking), 1)) p.pitch = lerpf(p.pitch, target_pitch + cos(clock * 1.3) * float(skill.error), minf(dt * float(skill.tracking), 1)) var burst_time := fmod(maxf(0, p.bot_acquired - float(skill.reaction)), float(skill.burst) + float(skill.pause)) shooting = p.bot_acquired >= float(skill.reaction) and burst_time < float(skill.burst) and absf(angle_difference(p.yaw, target_yaw)) < 0.12 else: p.bot_enemy = 0 p.bot_acquired = 0 var movement := Vector2.ZERO var wants_movement := not enemy or nearest > 12 or absf(enemy.global_position.y - p.global_position.y) > 2 if wants_movement and motion.needs_repath(p, dt): p.bot_path.clear() p.bot_think = 0 if motion.repaths >= 2: p.bot_phase += 1 if p.bot_think <= 0: p.bot_think = randf_range(0.8, 1.3) if enemy and wants_movement and motion.repaths < 2: p.bot_target = enemy.global_position elif p.bot_path.is_empty(): p.bot_phase += 1 p.bot_target = routes.patrol(p.peer_id, p.bot_phase) if wants_movement: p.bot_path = routes.path(p.global_position, p.bot_target) while not p.bot_path.is_empty() and BotSteering.reached(p.global_position, p.bot_path[0]): p.bot_path.remove_at(0) if not p.bot_path.is_empty() and wants_movement: var next: Vector3 = p.bot_path[0] var world_dir := BotSteering.avoid(p, next - p.global_position, game.players.values()) if not enemy and world_dir.length_squared() > 0.01: p.yaw = lerp_angle(p.yaw, atan2(-world_dir.x, -world_dir.z), 1 - exp(-dt * 9)) p.pitch = lerpf(p.pitch, 0, 1 - exp(-dt * 5)) var local_dir := Basis(Vector3.UP, -p.yaw) * world_dir movement = Vector2(local_dir.x, local_dir.z) p.input_data = {"move": movement, "fire": shooting, "ads": enemy != null, "sprint": enemy == null, "reload": p.magazines[p.weapon] == 0} if enemy and p.bot_acquired > 1.5 and nearest > 7 and nearest < 20 and p.grenades > 0 and p.bot_grenade_cooldown <= 0: p.input_data["grenade"] = true p.bot_grenade_cooldown = 12 if p.flash_left > 0.8: p.input_data["fire"] = false p.input_data["grenade"] = false