Ejemplo n.º 1
0
class GameLoop:
    def __init__(self, saves_path: str, base_path: str, game_xml_path: str,
                 desired_win_size_pixels: Optional[Point],
                 tile_size_pixels: int) -> None:
        # Determine effective window size in both tiles and pixels
        # Initialize the pygame displays
        if desired_win_size_pixels is None:
            screen = pygame.display.set_mode(
                (0, 0), pygame.FULLSCREEN | pygame.NOFRAME
                | pygame.SRCALPHA)  # | pygame.DOUBLEBUF | pygame.HWSURFACE)
            self.win_size_pixels = Point(screen.get_size())
            win_size_tiles = (self.win_size_pixels / tile_size_pixels).floor()
        else:
            win_size_tiles = (desired_win_size_pixels /
                              tile_size_pixels).floor()
            self.win_size_pixels = win_size_tiles * tile_size_pixels
            pygame.display.set_mode(
                self.win_size_pixels.getAsIntTuple(),
                pygame.SRCALPHA)  # | pygame.DOUBLEBUF | pygame.HWSURFACE)

        self.title_image, self.title_music = \
            GameInfo.static_init(base_path, game_xml_path, win_size_tiles, tile_size_pixels)

        self.title_screen('Loading...')

        self.game_state = GameState(saves_path, base_path, game_xml_path,
                                    win_size_tiles, tile_size_pixels)
        self.gde = GameDialogEvaluator(self.game_state.game_info,
                                       self.game_state)
        self.gde.update_default_dialog_font_color()

    def run(self, pc_name_or_file_name: Optional[str] = None) -> None:
        self.game_state.is_running = True
        self.title_screen_loop(pc_name_or_file_name)
        self.exploring_loop()

    def title_screen(self, text: str) -> None:
        # Play title music and display title screen
        AudioPlayer().play_music(self.title_music)

        title_image_size_px = Point(self.title_image.get_size())
        title_image_size_px *= max(
            1,
            int(
                min(self.win_size_pixels.w * 0.8 / title_image_size_px.w,
                    self.win_size_pixels.h * 0.8 / title_image_size_px.h)))
        title_image = pygame.transform.scale(
            self.title_image, title_image_size_px.getAsIntTuple())
        title_image_dest_px = Point(
            (self.win_size_pixels.w - title_image_size_px.w) / 2,
            self.win_size_pixels.h / 2 - title_image_size_px.h)
        screen = pygame.display.get_surface()
        screen.fill('black')
        screen.blit(title_image, title_image_dest_px)
        title_image = GameDialog.font.render(text, GameDialog.anti_alias,
                                             pygame.Color('white'),
                                             pygame.Color('black'))
        title_image_dest_px = Point(
            (self.win_size_pixels.w - title_image.get_width()) / 2,
            3 * self.win_size_pixels.h / 4)
        screen.blit(title_image, title_image_dest_px)
        pygame.display.flip()

    def title_screen_loop(self,
                          pc_name_or_file_name: Optional[str] = None) -> None:
        self.title_screen('Press any key')

        # Wait for user input - any key press
        while self.game_state.is_running:
            waiting_for_user_input = True
            for event in GameEvents.get_events():
                if event.type == pygame.QUIT:
                    self.game_state.handle_quit(force=True)
                elif event.type == pygame.KEYDOWN:
                    AudioPlayer().play_sound('select')
                    waiting_for_user_input = False
                    break
            if waiting_for_user_input:
                pygame.time.wait(25)
            else:
                break

        # Prompt user for new game or to load a saved game
        if pc_name_or_file_name is None:
            # Get a list of the saved games
            saved_game_files = glob.glob(
                os.path.join(self.game_state.saves_path, '*.xml'))
            saved_games = []
            for saved_game_file in saved_game_files:
                saved_games.append(os.path.basename(saved_game_file)[:-4])

            while self.game_state.is_running:
                menu_options = []
                if 0 < len(saved_games):
                    menu_options.append('Continue a Quest')
                menu_options.append('Begin a Quest')
                if 0 < len(saved_games):
                    menu_options.append('Delete a Quest')
                if self.game_state.should_add_math_problems_in_combat():
                    menu_options.append('Combat Mode: Math')
                else:
                    menu_options.append('Combat Mode: Classic')

                message_dialog = GameDialog.create_message_dialog()
                message_dialog.add_menu_prompt(menu_options, 1)
                message_dialog.blit(self.game_state.screen, True)
                menu_result = self.gde.get_menu_result(message_dialog)
                # print('menu_result =', menu_result, flush=True)
                if menu_result == 'Continue a Quest':
                    message_dialog.clear()
                    message_dialog.add_menu_prompt(saved_games, 1)
                    message_dialog.blit(self.game_state.screen, True)
                    menu_result = self.gde.get_menu_result(message_dialog)
                    if menu_result is not None:
                        pc_name_or_file_name = menu_result
                        break
                if menu_result == 'Delete a Quest':
                    message_dialog.clear()
                    message_dialog.add_menu_prompt(saved_games, 1)
                    message_dialog.blit(self.game_state.screen, True)
                    menu_result = self.gde.get_menu_result(message_dialog)
                    if menu_result is not None:
                        message_dialog.add_yes_no_prompt('Are you sure?')
                        message_dialog.blit(self.game_state.screen, True)
                        if self.gde.get_menu_result(message_dialog) == 'YES':
                            saved_games.remove(menu_result)
                            # Delete the save game by archiving it off
                            saved_game_file = os.path.join(
                                self.game_state.saves_path,
                                menu_result + '.xml')
                            self.game_state.archive_saved_game_file(
                                saved_game_file, 'deleted')
                elif menu_result == 'Begin a Quest':
                    message_dialog.clear()
                    pc_name_or_file_name = self.gde.wait_for_user_input(
                        message_dialog, 'What is your name?')[0]

                    if pc_name_or_file_name in saved_games:
                        message_dialog.add_message(
                            'Thou hast already started a quest.  Dost thou want to start over?'
                        )
                        message_dialog.add_yes_no_prompt()
                        message_dialog.blit(self.game_state.screen, True)
                        menu_result = self.gde.get_menu_result(message_dialog)
                        if menu_result == 'YES':
                            # Delete the existing save game by archiving it off
                            saved_game_file = os.path.join(
                                self.game_state.saves_path,
                                pc_name_or_file_name + '.xml')
                            self.game_state.archive_saved_game_file(
                                saved_game_file, 'deleted')
                        elif menu_result != 'NO':
                            continue
                    break
                elif menu_result is not None and menu_result.startswith(
                        'Combat Mode:'):
                    self.game_state.toggle_should_add_math_problems_in_combat()

        # Load the saved game
        self.game_state.load(pc_name_or_file_name)
        self.gde.refresh_game_state()

    def exploring_loop(self) -> None:
        map_name = ''

        while self.game_state.is_running:

            # Generate the map state a mode or map change
            if map_name != self.game_state.get_map_name():
                map_name = self.game_state.get_map_name()

                # Play the music for the map
                AudioPlayer().play_music(self.game_state.game_info.maps[
                    self.game_state.get_map_name()].music)

                # Draw the map to the screen
                self.game_state.draw_map()

            if self.game_state.pending_dialog is not None:
                self.gde.dialog_loop(self.game_state.pending_dialog)
                self.game_state.pending_dialog = None

            # Process events
            # print(datetime.datetime.now(), 'exploring_loop:  Getting events...', flush=True)
            events = GameEvents.get_events(True)
            changed_direction = False

            for event in events:
                # print('exploring_loop:  Processing event', event, flush=True)
                move_direction: Optional[Direction] = None
                menu = False
                talking = False
                searching = False
                opening = False

                if event.type == pygame.QUIT:
                    self.game_state.handle_quit(force=True)
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_ESCAPE:
                        self.game_state.handle_quit()
                    elif event.key == pygame.K_RETURN:
                        menu = True
                    elif event.key == pygame.K_SPACE:
                        # Fast, smart interactions - skip launching the menu
                        if self.game_state.get_npc_to_talk_to() is not None:
                            talking = True
                        elif self.game_state.is_facing_openable_item():
                            opening = True
                        else:
                            searching = True
                    elif event.key == pygame.K_F1:
                        AudioPlayer().play_sound('select')
                        self.game_state.save(quick_save=True)
                    else:
                        move_direction = Direction.get_optional_direction(
                            event.key)
                else:
                    # print('exploring_loop:  Ignoring event', event, flush=True)
                    continue

                # print(datetime.datetime.now(), 'exploring_loop:  Processed event', event, flush=True)

                # Clear queued events upon launching the menu
                GameEvents.clear_events()
                events = []

                if move_direction:
                    if changed_direction or self.game_state.hero_party.members[0].curr_pos_dat_tile != \
                                            self.game_state.hero_party.members[0].dest_pos_dat_tile:
                        # print('Ignoring move as another move is already in progress', flush=True)
                        continue
                    if move_direction != self.game_state.hero_party.members[
                            0].direction:
                        self.game_state.hero_party.members[
                            0].direction = move_direction
                        changed_direction = True
                    else:
                        self.game_state.hero_party.members[0].dest_pos_dat_tile = \
                            self.game_state.hero_party.members[0].curr_pos_dat_tile + move_direction.get_vector()

                if menu:
                    AudioPlayer().play_sound('select')
                    GameDialog.create_exploring_status_dialog(
                        self.game_state.hero_party).blit(
                            self.game_state.screen, False)
                    menu_dialog = GameDialog.create_exploring_menu()
                    menu_dialog.blit(self.game_state.screen, True)
                    menu_result = self.gde.get_menu_result(menu_dialog)
                    # print('menu_result =', menu_result, flush=True)
                    if menu_result == 'TALK':
                        talking = True
                    elif menu_result == 'SEARCH':
                        searching = True
                    elif menu_result == 'OPEN':
                        opening = True
                    elif menu_result == 'STAIRS':
                        if not self.game_state.make_map_transition(
                                self.game_state.get_point_transition()):
                            self.gde.dialog_loop('There are no stairs here.')
                    elif menu_result == 'STATUS':
                        GameDialog.create_full_status_dialog(
                            self.game_state.hero_party).blit(
                                self.game_state.screen, True)
                        self.gde.wait_for_acknowledgement()
                    elif menu_result == 'SPELL':
                        # TODO: Need to choose the actor (spellcaster)
                        actor = self.game_state.hero_party.main_character
                        self.gde.set_actor(actor)
                        available_spell_names = actor.get_available_spell_names(
                        )
                        if len(available_spell_names) == 0:
                            self.gde.dialog_loop(
                                'Thou hast not yet learned any spells.')
                        else:
                            menu_dialog = GameDialog.create_menu_dialog(
                                Point(
                                    -1, menu_dialog.pos_tile.y +
                                    menu_dialog.size_tiles.h + 1), None,
                                'SPELLS', available_spell_names, 1)
                            menu_dialog.blit(self.game_state.screen, True)
                            menu_result = self.gde.get_menu_result(menu_dialog)
                            # print( 'menu_result =', menu_result, flush=True )
                            if menu_result is not None:
                                spell = self.game_state.game_info.spells[
                                    menu_result]
                                if actor.mp >= spell.mp:
                                    # TODO: Depending on the spell may need to select the target(s)
                                    targets = [actor]
                                    actor.mp -= spell.mp
                                    self.gde.set_targets(
                                        cast(List[CombatCharacterState],
                                             targets))
                                    self.gde.dialog_loop(spell.use_dialog)

                                    GameDialog.create_exploring_status_dialog(
                                        self.game_state.hero_party).blit(
                                            self.game_state.screen, False)
                                else:
                                    self.gde.dialog_loop(
                                        'Thou dost not have enough magic to cast the spell.'
                                    )

                        # Restore the default actor and targets after calling the spell
                        self.gde.restore_default_actor_and_targets()

                    elif menu_result == 'ITEM':
                        # TODO: Need to choose the hero to use an item
                        actor = self.game_state.hero_party.main_character
                        self.gde.set_actor(actor)
                        item_cols = 2
                        item_row_data = actor.get_item_row_data()
                        if len(item_row_data) == 0:
                            self.gde.dialog_loop(
                                'Thou dost not have any items.')
                        else:
                            menu_dialog = GameDialog.create_menu_dialog(
                                Point(
                                    -1, menu_dialog.pos_tile.y +
                                    menu_dialog.size_tiles.h + 1), None,
                                'ITEMS', item_row_data, item_cols,
                                GameDialogSpacing.OUTSIDE_JUSTIFIED)
                            menu_dialog.blit(self.game_state.screen, True)
                            item_result = self.gde.get_menu_result(menu_dialog)
                            # print('item_result =', item_result, flush=True)

                            if item_result is not None:
                                item_options = self.game_state.hero_party.main_character.get_item_options(
                                    item_result)
                                if len(item_row_data) == 0:
                                    self.gde.dialog_loop(
                                        "The item vanished in [ACTOR]'s hands."
                                    )
                                else:
                                    menu_dialog = GameDialog.create_menu_dialog(
                                        Point(
                                            -1, menu_dialog.pos_tile.y +
                                            menu_dialog.size_tiles.h + 1),
                                        None, None, item_options,
                                        len(item_options))
                                    menu_dialog.blit(self.game_state.screen,
                                                     True)
                                    action_result = self.gde.get_menu_result(
                                        menu_dialog)
                                    # print('action_result =', action_result, flush=True)
                                    if action_result == 'DROP':
                                        # TODO: Add an are you sure prompt here
                                        self.game_state.hero_party.lose_item(
                                            item_result)
                                    elif action_result == 'EQUIP':
                                        self.game_state.hero_party.main_character.equip_item(
                                            item_result)
                                    elif action_result == 'UNEQUIP':
                                        self.game_state.hero_party.main_character.unequip_item(
                                            item_result)
                                    elif action_result == 'USE':
                                        item = self.game_state.hero_party.get_item(
                                            item_result)
                                        if item is not None and isinstance(
                                                item, Tool
                                        ) and item.use_dialog is not None:
                                            # TODO: Depending on the item may need to select the target(s)
                                            targets = [actor]
                                            self.gde.set_targets(
                                                cast(
                                                    List[CombatCharacterState],
                                                    targets))
                                            self.gde.dialog_loop(
                                                item.use_dialog)
                                        else:
                                            self.gde.dialog_loop(
                                                '[ACTOR] studied the object and was confounded by it.'
                                            )

                        # Restore the default actor and targets after using the item
                        self.gde.restore_default_actor_and_targets()

                    elif menu_result is not None:
                        print('ERROR: Unsupported menu_result =',
                              menu_result,
                              flush=True)

                    # Erase menu
                    self.game_state.draw_map()
                    pygame.display.flip()

                if talking:
                    npc = self.game_state.get_npc_to_talk_to()
                    if npc:
                        if npc.npc_info.dialog is not None:
                            dialog = npc.npc_info.dialog
                            self.game_state.draw_map()
                        else:
                            dialog = ['They pay you no mind.']
                    else:
                        dialog = ['There is no one there.']
                    self.gde.dialog_loop(dialog, npc)

                if searching or opening:
                    decorations = self.game_state.get_decorations()
                    if searching:
                        dialog = [
                            '[NAME] searched the ground and found nothing.'
                        ]
                    else:
                        dialog = ['[NAME] found nothing to open.']
                        dest_tile = self.game_state.hero_party.members[0].curr_pos_dat_tile\
                                    + self.game_state.hero_party.members[0].direction.get_vector()
                        decorations += self.game_state.get_decorations(
                            dest_tile)

                    for decoration in decorations:
                        requires_removal = False

                        if decoration.type is not None:
                            requires_removal = (
                                decoration.type.remove_with_search
                                or decoration.type.remove_with_open
                                or decoration.type.remove_with_key)

                            if requires_removal:
                                if ((searching
                                     and decoration.type.remove_with_search) or
                                    (opening
                                     and decoration.type.remove_with_open)):
                                    if decoration.type.remove_sound is not None:
                                        AudioPlayer().play_sound(
                                            decoration.type.remove_sound)
                                    self.game_state.remove_decoration(
                                        decoration)
                                    self.game_state.draw_map()

                                    if decoration.dialog is not None:
                                        dialog = decoration.dialog
                                    else:
                                        dialog = []
                                    break
                                elif decoration.type.remove_with_key:
                                    key_item = self.game_state.game_info.items[
                                        'Key']
                                    if self.game_state.hero_party.has_item(key_item.name) \
                                            and isinstance(key_item, Tool) \
                                            and key_item.use_dialog is not None:
                                        dialog = [
                                            'It is locked. Do you want to open it with a key?',
                                            {
                                                'Yes': key_item.use_dialog,
                                                'No': None
                                            }
                                        ]
                                    else:
                                        dialog = ['It is locked.']
                                    break

                        if not requires_removal and decoration.dialog is not None:
                            dialog = decoration.dialog

                    self.gde.dialog_loop(dialog)

            if self.game_state.hero_party.members[0].curr_pos_dat_tile != \
               self.game_state.hero_party.members[0].dest_pos_dat_tile:
                self.scroll_tile()
            else:
                # print('advancing one tick in exploring_loop', flush=True)
                self.game_state.advance_tick()

    def scroll_tile(self) -> None:

        transition: Optional[OutgoingTransition] = None

        # Determine the destination tile and pixel count for the scroll
        hero_dest_dat_tile = self.game_state.hero_party.members[
            0].dest_pos_dat_tile

        # Validate if the destination tile is navigable
        movement_allowed = self.game_state.can_move_to_tile(hero_dest_dat_tile)

        # Play a walking sound or bump sound based on whether the movement was allowed
        audio_player = AudioPlayer()
        movement_hp_penalty = 0
        if movement_allowed:
            dest_tile_type = self.game_state.get_tile_info(hero_dest_dat_tile)

            for hero_idx in range(1, len(self.game_state.hero_party.members)):
                hero = self.game_state.hero_party.members[hero_idx]
                hero.dest_pos_dat_tile = self.game_state.hero_party.members[
                    hero_idx - 1].curr_pos_dat_tile
                if hero.curr_pos_dat_tile != hero.dest_pos_dat_tile:
                    hero.direction = Direction.get_direction(
                        hero.dest_pos_dat_tile - hero.curr_pos_dat_tile)

            # Determine if the movement should result in a transition to another map
            map_size = self.game_state.game_map.size()
            leaving_transition = self.game_state.game_info.maps[
                self.game_state.get_map_name()].leaving_transition
            if leaving_transition is not None:
                if leaving_transition.bounding_box:
                    if not leaving_transition.bounding_box.collidepoint(
                            hero_dest_dat_tile.getAsIntTuple()):
                        transition = leaving_transition
                elif (hero_dest_dat_tile[0] == 0 or hero_dest_dat_tile[1] == 0
                      or hero_dest_dat_tile[0] == map_size[0] - 1
                      or hero_dest_dat_tile[1] == map_size[1] - 1):
                    transition = leaving_transition
            if transition is None:
                # TODO: Uncomment following two statements to disable coordinate logging
                #encounter_background = self.game_state.get_encounter_background(hero_dest_dat_tile)
                #print('Check for transitions at', hero_dest_dat_tile, encounter_background, flush=True)

                # See if this tile has any associated transitions
                transition = self.game_state.get_point_transition(
                    hero_dest_dat_tile, filter_to_automatic_transitions=True)
            else:
                # Map leaving transition
                # print('Leaving map', self.gameState.mapState.mapName, flush=True)
                pass

            # Check for tile penalty effects
            if dest_tile_type.hp_penalty > 0 and not self.game_state.hero_party.is_ignoring_tile_penalties(
            ):
                audio_player.play_sound('hit_lvl_1')
                movement_hp_penalty = dest_tile_type.hp_penalty

            # Check for any status effect changes or healing to occur as the party moves
            has_low_health = self.game_state.hero_party.has_low_health()
            dialog_from_inc_step_count = self.game_state.hero_party.inc_step_counter(
            )
            if has_low_health != self.game_state.hero_party.has_low_health():
                # Change default dialog font color
                self.gde.update_default_dialog_font_color()

                # Redraw the map
                self.game_state.draw_map(True)
            if dialog_from_inc_step_count is not None:
                self.gde.dialog_loop(dialog_from_inc_step_count)

        else:
            self.game_state.hero_party.members[0].dest_pos_dat_tile = \
                self.game_state.hero_party.members[0].curr_pos_dat_tile
            audio_player.play_sound('blocked')

        first_frame = True
        while self.game_state.hero_party.members[0].curr_pos_dat_tile != \
              self.game_state.hero_party.members[0].dest_pos_dat_tile:
            # Redraws the characters when movement_allowed is True
            # print('advancing one tick in scroll_tile', flush=True)
            if movement_allowed and movement_hp_penalty > 0 and first_frame:
                flicker_surface = pygame.surface.Surface(
                    self.game_state.screen.get_size())
                flicker_surface.fill('red')
                flicker_surface.set_alpha(128)
                self.game_state.advance_tick(update_map=True,
                                             draw_map=True,
                                             advance_time=False,
                                             flip_buffer=False)
                self.game_state.screen.blit(flicker_surface, (0, 0))
                self.game_state.advance_tick(update_map=False,
                                             draw_map=False,
                                             advance_time=True,
                                             flip_buffer=True)
                pygame.time.wait(20)
                first_frame = False
            else:
                self.game_state.advance_tick()

        if movement_allowed:
            # Apply health penalty and check for player death
            for hero in self.game_state.hero_party.members:
                if not hero.is_ignoring_tile_penalties():
                    hero.hp -= movement_hp_penalty
            self.gde.update_default_dialog_font_color()
            self.game_state.handle_death()

            # At destination - now determine if an encounter should start
            if not self.game_state.make_map_transition(transition):
                # Check for special monster encounters
                if (self.game_state.get_special_monster() is not None or
                    (len(self.game_state.get_tile_monsters()) > 0
                     and random.uniform(0, 1) < dest_tile_type.spawn_rate)):
                    # NOTE: Comment out the following line to disable encounters
                    self.game_state.initiate_encounter()
        else:
            for x in range(CharacterSprite.get_tile_movement_steps()):
                self.game_state.advance_tick()