def get_next_waypoint_along_path(self, agent_entity: WorldEntity) -> Optional[Tuple[int, int]]:
        if self.path:
            # -----------------------------------------------
            # 1: Remove first waypoint if close enough to it
            # -----------------------------------------------
            # TODO: Does this cause problems for specific entity sizes / movement speeds?
            closeness_margin = 50
            if is_x_and_y_within_distance(agent_entity.get_position(), self.path[0], closeness_margin):
                # print("Popping " + str(self.path[0]) + " as I'm so close to it.")
                self.path.pop(0)
                if self.path:
                    # print("After popping, returning " + str(self.path[0]))
                    return self.path[0]
                else:
                    # print("no path after popping. stopping.")
                    return None

            # -----------------------------------------------
            # 2: Remove first waypoint if it's opposite direction of second waypoint
            # -----------------------------------------------
            if len(self.path) >= 2:
                dir_to_waypoint_0 = get_directions_to_position(agent_entity, self.path[0])[0]
                dir_to_waypoint_1 = get_directions_to_position(agent_entity, self.path[1])[0]
                if dir_to_waypoint_0 == get_opposite_direction(dir_to_waypoint_1):
                    # print("Not gonna go back. Popping " + str(self.path[0]))
                    self.path.pop(0)
                    # print("Popped first position. Next waypoint: " + str(self.path[0]))
                    return self.path[0]
                if self.path:
                    return self.path[0]
        else:
            # print("no path found. stopping.")
            return None
        # print("Leaked through. returning none")
        return None
Beispiel #2
0
 def get_dir_towards_considering_collisions(
         game_state: GameState, agent_entity: WorldEntity,
         destination: Tuple[int, int]) -> Optional[Direction]:
     if DEBUG_RENDER_PATHFINDING:
         _add_visual_line_to_next_waypoint(destination, agent_entity,
                                           game_state)
     directions = get_directions_to_position(agent_entity, destination)
     if directions:
         # TODO Refactor collision checking
         if _would_collide_with_dir(directions[0], agent_entity,
                                    game_state):
             if len(directions) > 1 and directions[1]:
                 # print("Colliding in main direction (" + str(directions[0]) + ")")
                 if not _would_collide_with_dir(directions[1], agent_entity,
                                                game_state):
                     # print("Will use other direction")
                     return directions[1]
                 else:
                     # print("Both directions collide...")
                     return None
             else:
                 # print("Colliding in main direction (" + str(directions[0]) + ") but there is no other choice")
                 return None
         else:
             return directions[0]
     return None
Beispiel #3
0
 def update(self, npc: NonPlayerCharacter, game_state: GameState,
            time_passed: Millis):
     self._time_since_attack += time_passed
     if self._time_since_attack > self._attack_interval:
         self._time_since_attack = 0
         self._update_attack_interval()
         directions_to_player = get_directions_to_position(
             npc.world_entity,
             game_state.game_world.player_entity.get_position())
         new_direction = directions_to_player[0]
         if random.random(
         ) < self._chance_to_shoot_other_direction and directions_to_player[
                 1] is not None:
             new_direction = directions_to_player[1]
         npc.world_entity.direction = new_direction
         npc.world_entity.set_not_moving()
         center_position = npc.world_entity.get_center_position()
         distance_from_enemy = 35
         projectile_pos = translate_in_direction(
             get_position_from_center_position(center_position,
                                               self._projectile_size),
             npc.world_entity.direction, distance_from_enemy)
         projectile = self._create_projectile(projectile_pos,
                                              npc.world_entity.direction)
         game_state.game_world.projectile_entities.append(projectile)
         play_sound(self._sound_id)
    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._summon_trait.update(npc, game_state, time_passed)
        self._random_walk_trait.update(npc, game_state, time_passed)

        self._time_since_healing += time_passed
        self._time_since_shoot += time_passed

        if self._time_since_healing > self._healing_cooldown:
            self._time_since_healing = 0
            self._healing_cooldown = self._random_healing_cooldown()
            necro_center_pos = npc.world_entity.get_center_position()
            nearby_hurt_enemies = [
                e for e in game_state.game_world.non_player_characters
                if e.is_enemy and is_x_and_y_within_distance(
                    necro_center_pos, e.world_entity.get_center_position(),
                    200) and e != npc and not e.health_resource.is_at_max()
            ]
            if nearby_hurt_enemies:
                healing_target = nearby_hurt_enemies[0]
                healing_target.health_resource.gain(5)
                healing_target_pos = healing_target.world_entity.get_center_position(
                )
                visual_line = VisualLine((80, 200, 150), necro_center_pos,
                                         healing_target_pos, Millis(350), 3)
                game_state.game_world.visual_effects.append(visual_line)
                play_sound(SoundId.ENEMY_NECROMANCER_HEAL)

        if self._time_since_shoot > self._shoot_cooldown:
            self._time_since_shoot = 0
            self._shoot_cooldown = self._random_shoot_cooldown()
            npc.world_entity.direction = get_directions_to_position(
                npc.world_entity, player_entity.get_position())[0]
            npc.world_entity.set_not_moving()
            center_position = npc.world_entity.get_center_position()
            distance_from_enemy = 35
            projectile_pos = translate_in_direction(
                get_position_from_center_position(center_position,
                                                  PROJECTILE_SIZE),
                npc.world_entity.direction, distance_from_enemy)
            projectile_speed = 0.2
            projectile_entity = WorldEntity(projectile_pos, PROJECTILE_SIZE,
                                            Sprite.NONE,
                                            npc.world_entity.direction,
                                            projectile_speed)
            projectile = Projectile(
                projectile_entity,
                create_projectile_controller(PROJECTILE_TYPE))
            game_state.game_world.projectile_entities.append(projectile)
            play_sound(SoundId.ENEMY_ATTACK_NECRO)
    def control_npc(self, game_state: GameState, npc: NonPlayerCharacter, player_entity: WorldEntity,
                    is_player_invisible: bool, time_passed: Millis):
        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() < 0.5 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:
            self._time_since_attack = 0
            self._update_attack_interval()
            directions_to_player = get_directions_to_position(npc.world_entity, player_entity.get_position())
            new_direction = directions_to_player[0]
            if random.random() < 0.3 and directions_to_player[1] is not None:
                new_direction = directions_to_player[1]
            npc.world_entity.direction = new_direction
            npc.world_entity.set_not_moving()
            center_position = npc.world_entity.get_center_position()
            distance_from_enemy = 35
            projectile_pos = translate_in_direction(
                get_position_from_center_position(center_position, PROJECTILE_SIZE),
                npc.world_entity.direction, distance_from_enemy)
            projectile_speed = 0.11
            projectile_entity = WorldEntity(projectile_pos, PROJECTILE_SIZE, PROJECTILE_SPRITE,
                                            npc.world_entity.direction, projectile_speed)
            projectile = Projectile(projectile_entity, create_projectile_controller(PROJECTILE_TYPE))
            game_state.projectile_entities.append(projectile)
            play_sound(SoundId.ENEMY_ATTACK_GOBLIN_WARLOCK)
Beispiel #6
0
    def control_npc(self, game_state: GameState, npc: NonPlayerCharacter,
                    player_entity: WorldEntity, _is_player_invisible: bool,
                    time_passed: Millis):
        self._time_since_decision += time_passed
        self._time_since_healing += time_passed
        self._time_since_shoot += time_passed
        if self._time_since_healing > self._healing_cooldown:
            self._time_since_healing = 0
            self._healing_cooldown = self._random_healing_cooldown()
            if not npc.health_resource.is_at_max():
                healing_amount = random.randint(10, 20)
                npc.health_resource.gain(healing_amount)
                circle_effect = VisualCircle(
                    (80, 200, 150), npc.world_entity.get_center_position(), 30,
                    50, Millis(350), 3)
                game_state.visual_effects.append(circle_effect)
                number_effect = create_visual_healing_text(
                    npc.world_entity, healing_amount)
                game_state.visual_effects.append(number_effect)
                play_sound(SoundId.ENEMY_SKELETON_MAGE_HEAL)

        if self._time_since_shoot > self._shoot_cooldown:
            self._time_since_shoot = 0
            self._shoot_cooldown = self._random_shoot_cooldown()
            npc.world_entity.direction = get_directions_to_position(
                npc.world_entity, player_entity.get_position())[0]
            npc.world_entity.set_not_moving()
            center_position = npc.world_entity.get_center_position()
            distance_from_enemy = 35
            projectile_pos = translate_in_direction(
                get_position_from_center_position(center_position,
                                                  PROJECTILE_SIZE),
                npc.world_entity.direction, distance_from_enemy)
            projectile_speed = 0.2
            projectile_entity = WorldEntity(projectile_pos, PROJECTILE_SIZE,
                                            Sprite.NONE,
                                            npc.world_entity.direction,
                                            projectile_speed)
            projectile = Projectile(
                projectile_entity,
                create_projectile_controller(PROJECTILE_TYPE))
            game_state.projectile_entities.append(projectile)
            play_sound(SoundId.ENEMY_ATTACK_SKELETON_MAGE)

        if self._time_since_decision > self._decision_interval:
            self._time_since_decision = 0
            if random.random() < 0.2:
                direction = random_direction()
                npc.world_entity.set_moving_in_dir(direction)
            else:
                npc.world_entity.set_not_moving()
    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
        super().control_npc(game_state, npc, player_entity, is_player_invisible, time_passed)

        self._time_since_state_change += time_passed

        if self._state == State.BASE:
            if self._time_since_state_change > NpcMind.STATE_DURATION_BASE:
                self._time_since_state_change -= NpcMind.STATE_DURATION_BASE
                self._state = State.FIRING

                npc.gain_buff_effect(get_buff_effect(BUFF_STUNNED),
                                     Millis(NpcMind.STATE_DURATION_FIRING))
                return
        elif self._state == State.FIRING:
            if self._time_since_state_change > NpcMind.STATE_DURATION_FIRING:
                self._time_since_state_change -= NpcMind.STATE_DURATION_FIRING
                self._state = State.BASE
                return
            self._time_since_fired += time_passed
            if self._time_since_fired > NpcMind.FIRE_COOLDOWN:
                self._time_since_fired -= NpcMind.FIRE_COOLDOWN
                directions_to_player = get_directions_to_position(npc.world_entity, player_entity.get_position())
                new_direction = directions_to_player[0]
                if random.random() < 0.1 and directions_to_player[1] is not None:
                    new_direction = directions_to_player[1]
                npc.world_entity.direction = new_direction
                npc.world_entity.set_not_moving()
                center_position = npc.world_entity.get_center_position()
                distance_from_enemy = 35
                projectile_pos = translate_in_direction(
                    get_position_from_center_position(center_position, PROJECTILE_SIZE),
                    npc.world_entity.direction, distance_from_enemy)
                projectile_speed = 0.3
                projectile_entity = WorldEntity(projectile_pos, PROJECTILE_SIZE, Sprite.NONE,
                                                npc.world_entity.direction, projectile_speed)
                projectile = Projectile(projectile_entity, create_projectile_controller(PROJECTILE_TYPE))
                game_state.game_world.projectile_entities.append(projectile)
                play_sound(SoundId.ENEMY_MAGIC_SKELETON_BOSS)
Beispiel #8
0
    def handle_user_input(self,
                          events: List[Any]) -> Optional[SceneTransition]:

        transition_to_pause = False

        events_triggered_from_ui: List[EventTriggeredFromUi] = []

        # TODO handle dialog/no dialog more explicitly as states, and delegate more things to them (?)

        if self.ui_view.has_open_dialog():
            self.game_state.game_world.player_entity.set_not_moving()
            user_actions = get_dialog_actions(events)
            for action in user_actions:
                if isinstance(action, ActionChangeDialogOption):
                    npc_type, previous_index, new_index = self.ui_view.change_dialog_option(
                        action.index_delta)
                    self._handle_dialog_change_option(npc_type, previous_index,
                                                      new_index)
                if isinstance(action, ActionPressSpaceKey):
                    result = self.ui_view.handle_space_click()
                    if result:
                        npc_in_dialog, option_index = result
                        npc_type = npc_in_dialog.npc_type
                        blur_npc_action(npc_type, option_index,
                                        self.game_state, self.ui_view)
                        message = select_npc_action(npc_type, option_index,
                                                    self.game_engine)
                        if message:
                            self.ui_view.info_message.set_message(message)
                        npc_in_dialog.stun_status.remove_one()

                        # User may have been holding down a key when starting dialog, and then releasing it while in
                        # dialog. It's safer then to treat all keys as released when we exit the dialog.
                        self.user_input_handler.forget_held_down_keys()
                if isinstance(action, ActionMouseMovement):
                    self.ui_view.handle_mouse_movement_in_dialog(
                        action.mouse_screen_position)
                    # we handle "normal UI" mouse movement here primarily so that you can hover your equipment
                    # while in a dialog. (especially important when buying from an NPC)
                    self.ui_view.handle_mouse_movement(
                        action.mouse_screen_position)
                if isinstance(action, ActionMouseClicked):
                    result = self.ui_view.handle_mouse_click_in_dialog()
                    if result:
                        npc_type, previous_index, new_index = result
                        self._handle_dialog_change_option(
                            npc_type, previous_index, new_index)

        else:
            user_actions = self.user_input_handler.get_actions(events)
            for action in user_actions:
                if isinstance(action, ActionToggleRenderDebugging):
                    self.render_hit_and_collision_boxes = not self.render_hit_and_collision_boxes
                    # TODO: Handle this better than accessing a global variable from here
                    pythongame.core.pathfinding.npc_pathfinding.DEBUG_RENDER_PATHFINDING = \
                        not pythongame.core.pathfinding.npc_pathfinding.DEBUG_RENDER_PATHFINDING
                elif isinstance(action, ActionTryUseAbility):
                    self.game_engine.try_use_ability(action.ability_type)
                elif isinstance(action, ActionTryUsePotion):
                    self.game_engine.try_use_consumable(action.slot_number)
                elif isinstance(action, ActionMoveInDirection):
                    self.game_engine.move_in_direction(action.direction)
                elif isinstance(action, ActionStopMoving):
                    self.game_engine.stop_moving()
                elif isinstance(action, ActionPauseGame):
                    transition_to_pause = True
                elif isinstance(action, ActionMouseMovement):
                    self.ui_view.handle_mouse_movement(
                        action.mouse_screen_position)
                elif isinstance(action, ActionMouseClicked):
                    events_triggered_from_ui += self.ui_view.handle_mouse_click(
                    )
                elif isinstance(action, ActionMouseReleased):
                    events_triggered_from_ui += self.ui_view.handle_mouse_release(
                    )
                elif isinstance(action, ActionRightMouseClicked):
                    events_triggered_from_ui += self.ui_view.handle_mouse_right_click(
                    )
                elif isinstance(action, ActionPressSpaceKey):
                    ready_entity = self.player_interactions_state.get_entity_to_interact_with(
                    )
                    if ready_entity is not None:
                        if isinstance(ready_entity, NonPlayerCharacter):
                            ready_entity.world_entity.direction = get_directions_to_position(
                                ready_entity.world_entity,
                                self.game_state.game_world.player_entity.
                                get_center_position())[0]
                            ready_entity.world_entity.set_not_moving()
                            ready_entity.stun_status.add_one()
                            npc_type = ready_entity.npc_type
                            dialog_data = get_dialog_data(
                                npc_type, self.game_state)
                            option_index = self.ui_view.start_dialog_with_npc(
                                ready_entity, dialog_data)
                            play_sound(SoundId.DIALOG)
                            hover_npc_action(npc_type, option_index,
                                             self.game_state, self.ui_view)
                        elif isinstance(ready_entity, LootableOnGround):
                            self.game_engine.try_pick_up_loot_from_ground(
                                ready_entity)
                        elif isinstance(ready_entity, Portal):
                            self.game_engine.interact_with_portal(ready_entity)
                        elif isinstance(ready_entity, WarpPoint):
                            self.game_engine.use_warp_point(ready_entity)
                        elif isinstance(ready_entity, Chest):
                            self.game_engine.open_chest(ready_entity)
                        elif isinstance(ready_entity, Shrine):
                            self.game_engine.interact_with_shrine(ready_entity)
                        elif isinstance(ready_entity, DungeonEntrance):
                            has_key = self.game_state.player_state.item_inventory.has_item_in_inventory(
                                plain_item_id(ItemType.PORTAL_KEY))
                            if has_key:
                                entering_dungeon_scene = self.scene_factory.switching_game_world(
                                    self.game_engine, self.character_file,
                                    self.total_time_played_on_character,
                                    self._create_dungeon_engine_and_behavior)
                                return SceneTransition(entering_dungeon_scene)
                            else:
                                self.ui_view.info_message.set_message(
                                    "There is a keyhole on the side!")
                        else:
                            raise Exception("Unhandled entity: " +
                                            str(ready_entity))
                elif isinstance(action, ActionPressKey):
                    events_triggered_from_ui += self.ui_view.handle_key_press(
                        action.key)

        # TODO Much noise below around playing sounds. Perhaps game_engine should play the sounds in these cases?
        for event in events_triggered_from_ui:
            if isinstance(event, StartDraggingItemOrConsumable):
                play_sound(SoundId.UI_START_DRAGGING_ITEM)
            elif isinstance(event, DragItemBetweenInventorySlots):
                did_switch_succeed = self.game_engine.drag_item_between_inventory_slots(
                    event.from_slot, event.to_slot)
                if did_switch_succeed:
                    play_sound(SoundId.UI_ITEM_WAS_MOVED)
                else:
                    play_sound(SoundId.INVALID_ACTION)
            elif isinstance(event, DropItemOnGround):
                world_position = _get_mouse_world_pos(self.game_state,
                                                      event.screen_position)
                self.game_engine.drop_inventory_item_on_ground(
                    event.from_slot, world_position)
                play_sound(SoundId.UI_ITEM_WAS_DROPPED_ON_GROUND)
            elif isinstance(event, DragConsumableBetweenInventorySlots):
                self.game_engine.drag_consumable_between_inventory_slots(
                    event.from_slot, event.to_slot)
                play_sound(SoundId.UI_ITEM_WAS_MOVED)
            elif isinstance(event, DropConsumableOnGround):
                world_position = _get_mouse_world_pos(self.game_state,
                                                      event.screen_position)
                self.game_engine.drop_consumable_on_ground(
                    event.from_slot, world_position)
                play_sound(SoundId.UI_ITEM_WAS_DROPPED_ON_GROUND)
            elif isinstance(event, PickTalent):
                name_of_picked = pick_talent(self.game_state, event.tier_index,
                                             event.option_index)
                if not self.game_state.player_state.has_unpicked_talents():
                    self.ui_view.close_talent_window()
                self.ui_view.info_message.set_message("Talent picked: " +
                                                      name_of_picked)
                play_sound(SoundId.EVENT_PICKED_TALENT)
            elif isinstance(event, TrySwitchItemInInventory):
                did_switch_succeed = self.game_engine.try_switch_item_at_slot(
                    event.slot)
                if did_switch_succeed:
                    play_sound(SoundId.UI_ITEM_WAS_MOVED)
                else:
                    play_sound(SoundId.INVALID_ACTION)
            elif isinstance(event, ToggleSound):
                toggle_muted()
            elif isinstance(event, SaveGame):
                self._save_game()
            elif isinstance(event, ToggleFullscreen):
                self.toggle_fullscreen_callback()
            elif isinstance(event, ToggleWindow):
                play_sound(SoundId.UI_TOGGLE)
            else:
                raise Exception("Unhandled event: " + str(event))

        if transition_to_pause:
            return SceneTransition(
                PausedScene(self, self.world_view, self.ui_view,
                            self.game_state))
    def control_npc(self, game_state: GameState, npc: NonPlayerCharacter, player_entity: WorldEntity,
                    _is_player_invisible: bool, time_passed: Millis):
        self._time_since_decision += time_passed
        self._time_since_summoning += time_passed
        self._time_since_healing += time_passed
        self._time_since_shoot += time_passed
        if self._time_since_summoning > self._summoning_cooldown:
            necro_center_pos = npc.world_entity.get_center_position()
            self._time_since_summoning = 0
            self._alive_summons = [summon for summon in self._alive_summons
                                   if summon in game_state.non_player_characters]
            if len(self._alive_summons) < 3:
                relative_pos_from_summoner = (random.randint(-150, 150), random.randint(-150, 150))
                summon_center_pos = sum_of_vectors(necro_center_pos, relative_pos_from_summoner)
                summon_type = random.choice([NpcType.ZOMBIE, NpcType.MUMMY])
                summon_size = NON_PLAYER_CHARACTERS[summon_type].size
                summon_pos = game_state.get_within_world(
                    get_position_from_center_position(summon_center_pos, summon_size), summon_size)
                summon_enemy = create_npc(summon_type, summon_pos)
                is_wall_blocking = game_state.walls_state.does_rect_intersect_with_wall(
                    rect_from_corners(necro_center_pos, summon_center_pos))
                is_position_blocked = game_state.would_entity_collide_if_new_pos(summon_enemy.world_entity, summon_pos)
                if not is_wall_blocking and not is_position_blocked:
                    self._summoning_cooldown = self._random_summoning_cooldown()
                    game_state.add_non_player_character(summon_enemy)
                    self._alive_summons.append(summon_enemy)
                    game_state.visual_effects.append(
                        VisualCircle((80, 150, 100), necro_center_pos, 40, 70, Millis(120), 3))
                    game_state.visual_effects.append(
                        VisualCircle((80, 150, 100), summon_center_pos, 40, 70, Millis(120), 3))
                    play_sound(SoundId.ENEMY_NECROMANCER_SUMMON)
                else:
                    # Failed to summon, so try again without waiting full duration
                    self._summoning_cooldown = 500
            else:
                self._summoning_cooldown = self._random_summoning_cooldown()

        if self._time_since_healing > self._healing_cooldown:
            self._time_since_healing = 0
            self._healing_cooldown = self._random_healing_cooldown()
            necro_center_pos = npc.world_entity.get_center_position()
            nearby_hurt_enemies = [
                e for e in game_state.non_player_characters
                if e.is_enemy
                   and is_x_and_y_within_distance(necro_center_pos, e.world_entity.get_center_position(), 200)
                   and e != npc and not e.health_resource.is_at_max()
            ]
            if nearby_hurt_enemies:
                healing_target = nearby_hurt_enemies[0]
                healing_target.health_resource.gain(5)
                healing_target_pos = healing_target.world_entity.get_center_position()
                visual_line = VisualLine((80, 200, 150), necro_center_pos, healing_target_pos, Millis(350), 3)
                game_state.visual_effects.append(visual_line)
                play_sound(SoundId.ENEMY_NECROMANCER_HEAL)

        if self._time_since_shoot > self._shoot_cooldown:
            self._time_since_shoot = 0
            self._shoot_cooldown = self._random_shoot_cooldown()
            npc.world_entity.direction = get_directions_to_position(npc.world_entity, player_entity.get_position())[0]
            npc.world_entity.set_not_moving()
            center_position = npc.world_entity.get_center_position()
            distance_from_enemy = 35
            projectile_pos = translate_in_direction(
                get_position_from_center_position(center_position, PROJECTILE_SIZE),
                npc.world_entity.direction, distance_from_enemy)
            projectile_speed = 0.2
            projectile_entity = WorldEntity(projectile_pos, PROJECTILE_SIZE, Sprite.NONE, npc.world_entity.direction,
                                            projectile_speed)
            projectile = Projectile(projectile_entity, create_projectile_controller(PROJECTILE_TYPE))
            game_state.projectile_entities.append(projectile)
            play_sound(SoundId.ENEMY_ATTACK_NECRO)

        if self._time_since_decision > self._decision_interval:
            self._time_since_decision = 0
            if random.random() < 0.2:
                direction = random_direction()
                npc.world_entity.set_moving_in_dir(direction)
            else:
                npc.world_entity.set_not_moving()