def control_npc(self, game_state: GameState, npc: NonPlayerCharacter,
                    player_entity: WorldEntity, is_player_invisible: bool,
                    time_passed: Millis):
        if npc.stun_status.is_stunned():
            return
        self._time_since_attack += time_passed
        self._time_since_updated_path += time_passed
        self._time_since_reevaluated += time_passed

        enemy_entity = npc.world_entity
        target: EnemyTarget = get_target(enemy_entity, game_state)

        if self._time_since_updated_path > self._update_path_interval:
            self._time_since_updated_path = 0
            if not is_player_invisible:
                self.pathfinder.update_path_towards_target(
                    enemy_entity, game_state, target.entity)

        new_next_waypoint = self.pathfinder.get_next_waypoint_along_path(
            enemy_entity)

        should_update_waypoint = self.next_waypoint != new_next_waypoint
        if self._time_since_reevaluated > self._reevaluate_next_waypoint_direction_interval:
            self._time_since_reevaluated = 0
            should_update_waypoint = True

        if should_update_waypoint:
            self.next_waypoint = new_next_waypoint
            if self.next_waypoint:
                direction = self.pathfinder.get_dir_towards_considering_collisions(
                    game_state, enemy_entity, self.next_waypoint)
                _move_in_dir(enemy_entity, direction)
            else:
                enemy_entity.set_not_moving()

        if self._time_since_attack > self._attack_interval:
            if not is_player_invisible:
                enemy_position = enemy_entity.get_center_position()
                target_center_pos = target.entity.get_center_position()
                if is_x_and_y_within_distance(enemy_position,
                                              target_center_pos, 200):
                    self._time_since_attack = 0
                    self.randomize_attack_interval()
                    play_sound(SoundId.ENEMY_ATTACK_ICE_WITCH)
                    damage = random.randint(DAMAGE_MIN, DAMAGE_MAX)
                    deal_npc_damage(damage, DamageType.MAGIC, game_state,
                                    enemy_entity, npc, target)
                    game_state.game_world.visual_effects += [
                        (VisualLine((100, 100, 200), enemy_position,
                                    target_center_pos, Millis(120), 3)),
                        (VisualLine((150, 150, 250), enemy_position,
                                    target_center_pos, Millis(240), 2))
                    ]
                    chance_to_resist_slow = game_state.player_state.get_effective_movement_impairing_resist_chance(
                    )
                    # TODO It's error-prone that we have to check this for every negative debuff that can slow player
                    if random.random() > chance_to_resist_slow:
                        game_state.player_state.gain_buff_effect(
                            get_buff_effect(SLOW_BUFF_TYPE), Millis(1500))
    def control_npc(self, game_state: GameState, npc: NonPlayerCharacter,
                    player_entity: WorldEntity, is_player_invisible: bool,
                    time_passed: Millis):
        if npc.stun_status.is_stunned():
            return
        self._time_since_attack += time_passed
        self._time_since_updated_path += time_passed
        self._time_since_reevaluated += time_passed

        enemy_entity = npc.world_entity
        target: EnemyTarget = get_target(enemy_entity, game_state)

        if self._time_since_updated_path > self._update_path_interval:
            self._time_since_updated_path = 0
            if not is_player_invisible:
                self.pathfinder.update_path_towards_target(
                    enemy_entity, game_state, target.entity)

        new_next_waypoint = self.pathfinder.get_next_waypoint_along_path(
            enemy_entity)

        should_update_waypoint = self.next_waypoint != new_next_waypoint
        if self._time_since_reevaluated > self._reevaluate_next_waypoint_direction_interval:
            self._time_since_reevaluated = 0
            should_update_waypoint = True

        if should_update_waypoint:
            self.next_waypoint = new_next_waypoint
            if self.next_waypoint:
                direction = self.pathfinder.get_dir_towards_considering_collisions(
                    game_state, enemy_entity, self.next_waypoint)
                if random.random(
                ) < self.chance_to_stray_from_path and direction:
                    direction = random.choice(
                        get_perpendicular_directions(direction))
                _move_in_dir(enemy_entity, direction)
            else:
                enemy_entity.set_not_moving()

        if self._time_since_attack > self._attack_interval:
            if not is_player_invisible:
                enemy_position = enemy_entity.get_center_position()
                target_center_pos = target.entity.get_center_position()
                if is_x_and_y_within_distance(enemy_position,
                                              target_center_pos, 80):
                    self._time_since_attack = 0
                    self.randomize_attack_interval()
                    deal_npc_damage(self.damage_amount, DamageType.PHYSICAL,
                                    game_state, enemy_entity, npc, target)