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
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
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)
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)
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()