Pygameを使って RPGを作る（19. 戦闘画面:敵の反撃とGame Over）

# 概要
敵の反撃を追加
PlayerがHPゼロになった場合はGame Overにする

反撃
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/0492d3ac-32ee-4a53-e8c1-b181c396b319.png)

Playerが倒される
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/cebb2d74-07cd-452a-a111-f00f4e325233.png)

Game Over
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/576081/34ff867e-713e-0f04-4513-1aacad5f1d10.png)

# Mainクラス
```py
import pygame as pg
from settings import *

from map import Map
from groups import AllSprites
from battle import BattleScreen
from game_over import GameOver

class Game:
    def __init__(self):
        pg.init()

        # screen
        self.display_surface = pg.display.set_mode((WIDTH, HEIGHT))
        # title
        pg.display.set_caption(TITLE)

        # clock
        self.clock = pg.time.Clock()
        self.running = True

        # all sprite
        self.all_sprites = AllSprites()

        self.battle_sprites = AllSprites()

        # self.game_stage = 'main'
        self.game_stage = 'game_over'

        # バトル管理
        self.battle = None  # BattleScreenクラスのインスタンスを保持
        self.init_battle = True  # バトル画面初期化フラグ
        self.update_message_flag = False

        # バトル初期化
        self.battle = BattleScreen(self.battle_sprites)

        # 
        self.game_over_flag = False

        # Map
        self.player, self.current_map = Map(self.all_sprites).create()
        
    def run(self):
        """ゲームループ"""
        dt = self.clock.tick(FPS) / 1000

        while self.running:
            # events
            self.events()
            self.game_stage = self.player.game_stage
            if self.game_stage == 'main':
                self.main_screen(dt)

            elif self.game_stage == 'battle':
                # 敵と衝突した後の画面
                self.show_battle_screen()

            elif self.game_stage == 'game_over':
                self.show_game_over()

            pg.display.flip()

        pg.quit()

    def main_screen(self, dt):
        self.all_sprites.update(dt, self.current_map)
        self.display_surface.fill(BLUE)
        self.all_sprites.draw()

    def show_battle_screen(self):

        # バトル画面の初期化
        if self.init_battle:
            self.battle.battle_message = []
            self.init_battle = False  

            # バトル画面描画
            self.battle.draw(self.player, self.display_surface)
            self.battle_sprites.draw_battle()
        
        # 戦闘時メッセージの更新
        if self.update_message_flag:
            self.battle.update_message(self.display_surface)
            self.battle_sprites.draw_battle()
            self.update_message_flag = False

        # 戦闘コマンドの描画(マウスホーバーを検知するため、ループの外側で実装)
        if self.battle.battle_active:
            self.battle.draw_buttons()

        self.battle.status.draw_status(self.display_surface)
            

    def show_game_over(self):
        self.display_surface.fill((0, 0, 0))  # RGBで黒 (0, 0, 0)
        self.game = GameOver()
        self.game.text.draw(self.display_surface)
        self.game.text2.draw(self.display_surface)

    # メインステージ、敵の数、Playerの数上手くリセットできていない
    def reset_game_state(self):
        # 全てのスプライトを再生成
        self.all_sprites = AllSprites()
        self.battle_sprites = AllSprites()

        # プレイヤーとマップを再生成
        self.player, self.current_map = Map(self.all_sprites).create()

        # バトルの状態を完全にリセット
        self.battle = BattleScreen(self.battle_sprites)
        self.init_battle = True
        self.battle.reset()

        # ゲームステージを"main"に戻す
        self.game_stage = 'main'


    def events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.running = False

            if event.type == pg.KEYDOWN:
                # バトル終了後、メイン画面に戻る処理
                if event.key == pg.K_SPACE and self.game_stage == 'battle':
                    if self.battle.get_battle_result() == 0:
                        self.player.game_stage = 'game_over'
                    elif self.battle.get_battle_result() == 1:
                        self.init_battle = True
                        self.battle.battle_active = True
                        self.player.game_stage = "main"  # "main"に戻す
                    
                # Game Overからメイン画面に戻る処理
                if event.key == pg.K_SPACE and self.game_stage == 'game_over':
                    self.reset_game_state()

            # BattleScreenのマウスイベントを処理
            self.update_message_flag = self.battle.handle_mouse_event(event)

if __name__ == "__main__":
    new_game = Game()
    new_game.run()
　
```

# Battleクラス
```py 
import pygame as pg
from settings import *
from utils import Button, TextSprite
from status import PlayerStatus

class BattleMenu:
    def __init__(self, actions):
        self.display_surface = pg.display.get_surface()
        
        self.px = 30
        self.main_pos_y = [30, 80, 530]
        self.sub_pos_y = [30, 80, 130] 

        self.buttons = {
            "main": self.create_buttons(actions['main'], self.px, self.main_pos_y),
            "sub": self.create_buttons(actions['sub'], self.px, self.sub_pos_y),
        }
        
        self.currend_command = "main"

    def create_buttons(self, actions, x, y_positons):
        btn_width, btn_height = 100, 40
        return [
            Button(x, y_positons[i], btn_width, btn_height, name, action)
            for i, (name, action) in enumerate(actions)
        ]

    # メインコマンドを表示
    def show_main_commands(self):
        self.currend_command = 'main'

    def show_sub_commands(self):
        self.currend_command = 'sub'

    # 描画
    def draw_buttons(self):
        for button in self.buttons[self.currend_command]:
            button.draw(self.display_surface)

    def handle_mouse_event(self, event):
        if event.type == pg.MOUSEBUTTONDOWN:
            mouse_pos = event.pos  # クリックした位置を取得
            for button in self.buttons[self.currend_command]:
                if button.check_click(mouse_pos):  # ボタンがクリックされたか判定
                    button.action()  # ボタンに設定された関数を呼び出し
                    return True

class BattleLayout:
    def __init__(self):
        self.display_surface = pg.display.get_surface()
        self.back_ground_img = pg.image.load('../battle/bg.png')
        self.back_ground_img = pg.transform.scale(self.back_ground_img, (819, HEIGHT))

        self.bg_size = self.back_ground_img.get_size()
        self.rect = self.back_ground_img.get_rect()
        self.off_set = pg.Vector2()
        self.off_set.x = -int((self.rect.centerx - WIDTH /2))
        self.off_set.y = -int((self.rect.centery - HEIGHT /2))
    
    def draw_background(self, screen):
        self.screen = screen
        self.screen.fill((0, 0, 0))
        self.screen.blit(self.back_ground_img, self.rect.topleft + self.off_set)

    def draw_menu_backgroud(self):
        pg.draw.rect(self.display_surface, '#8E7698', [2, 2, self.off_set.x -2, HEIGHT-2])
        pg.draw.rect(self.display_surface,'#D3DED0', [0, 0, self.off_set.x, HEIGHT],5,border_radius=5)

    def draw_background_text_area(self):
        """必要最小限の透明マスクを新規作成し、配置する"""
        self.text_area_rect = pg.Rect(250, 430, self.bg_size[0] * 0.95, self.bg_size[1] * 0.3)

        # 半透明の背景色を設定
        semi_transparent_surface = pg.Surface(self.text_area_rect.size, pg.SRCALPHA)
        semi_transparent_surface.fill((0, 0, 255, 50))  # 半透明の青色
        
        # 角の描画ですこし小さくする
        self.display_surface.blit(semi_transparent_surface, 
                                  [self.text_area_rect.x + 2,
                                   self.text_area_rect.y + 2,
                                   self.text_area_rect.width -2,
                                   self.text_area_rect.height -2])

        # 枠線を再描画
        pg.draw.rect(self.display_surface, (255, 255, 255), self.text_area_rect, 3, border_radius=5)

class BattleScreen(pg.sprite.Sprite):
    def __init__(self, battle_sprites):
        super().__init__()
        self.display_surface = pg.display.get_surface()

        self.font = pg.font.Font("../battle/Meiryo.ttf", MESSAGE_FONT_SIZE)
        # self.battle_message = []

        self.enemy = None

        self.battle_sprites = battle_sprites

        # Layout
        self.layout = BattleLayout()

        self.current_command = 0

        # menu
        self.menu = BattleMenu(self.get_actions())

        # player status
        self.status = PlayerStatus((BG_SIZE_WIDTH, HEIGHT), self.battle_sprites)
        
        # messege
        self.text_sprites = TextSprite('', self.font, (255,255,255),  (0,0,255), 250, 430, self.battle_sprites)

        # Battle state
        self.battle_active = True  # 戦闘がアクティブかどうかを管理するフラグ

    def reset(self):
        # バトル関連の状態をリセット
        self.battle_message = []
        self.battle_active = True
        self.current_command = None
        self.enemy = None


    def get_actions(self):
        return {
            "main": [
                ("攻撃", self.attack),
                ("魔法", self.show_sub_commands),
                ("逃げる", self.escape),
            ],
            "sub": [
                ("ホイミ", self.hoimi),
                ("メラ", self.mera),
                ("ヒャド", self.hyado),
            ],
        }

    def attack(self):
        # 攻撃力は HP* 0.8
        damege = int(self.status.infact_status['ATK'] / 2) - int(self.enemy.mob_info['DEF']/4)
        self.enemy.mob_info['HP'] -= damege
        
        mes = f'{self.status.view_status['name']}は攻撃しました。' + \
            f'{self.enemy.name}は{damege}のダメージを受けました。'
        
        self.general_message([mes])
        
        if self.enemy.mob_info['HP'] <= 0:
            mes_02 = f'{self.enemy.name}を倒しました。'
            self.general_message([mes_02])
            self.battle_active = False  # 戦闘終了フラグを設定        
        # 反撃(一回でPlayerが倒される)
        else:
            p_damege = 10
            self.status.view_status['HP'] -= p_damege
            mes = f'{self.enemy.name}の攻撃、' + \
            f'{self.status.view_status['name']}は{p_damege}のダメージを受けました。'
            self.general_message([mes])
            if self.status.view_status['HP'] <=0:
                mes_02 = f'{self.status.view_status['name']}は倒れました。'
                self.general_message([mes_02])
                self.battle_active = False  # 戦闘終了フラグを設定


    def get_battle_result(self):
        if self.enemy.mob_info['HP'] <= 0:
            return 1
        elif self.status.view_status['HP'] <=0:
            return 0
        else:
            return 3

    def escape(self):
        self.battle_message.append('逃げる')

    def hoimi(self):
        self.battle_message.append('ホイミ')
        self.menu.show_main_commands()

    def mera(self):
        self.battle_message.append('メラ')
        self.menu.show_main_commands()

    def hyado(self):
        self.battle_message.append('ヒャド')
        self.menu.show_main_commands()

    # サブコマンドを表示
    def show_sub_commands(self):
        self.menu.show_sub_commands()

    def draw(self, player, screen):
        self.enemy = player.collided_enemy
        self.mob_pos =  ((WIDTH - 128) /2,HEIGHT /8)
        self.mob_surface = self.enemy.battle_surface.copy()
        # メッセージ
        self.battle_message.append(f'{self.enemy.name}が現れました!') 
        self.render_scene(screen)

    def render_scene(self, screen):
        self.layout.draw_background(screen)

        if self.battle_active:
            self.display_surface.blit(self.enemy.battle_surface, self.mob_pos)
        elif self.status.view_status['HP'] <=0:
            self.display_surface.blit(self.enemy.battle_surface, self.mob_pos)

        self.layout.draw_background_text_area()

        # ここのテキストはうまく更新できている　
        self.draw_text(screen)

        self.layout.draw_menu_backgroud()

        # player status(この部分が前回の描画したテキストが残っている)
        self.status.draw_status(screen)

    def update_message(self, screen):
        self.render_scene(screen)

    def general_message(self, message_list):
        for m in message_list:
            self.battle_message.append(m)
            if len(self.battle_message) > MAX_MESSAGE:
                del self.battle_message[0]

    def set_message(self):
        if len(self.battle_message) > MAX_MESSAGE:
            del self.battle_message[0]

        view_message = ['  ' + item for item in self.battle_message]
        return '\n'.join(view_message)

    def draw_text(self, screen):
        self.text_sprites.update_message(screen, self.set_message())

    def handle_mouse_event(self, event):
        # 戦闘中のみボタンの押下処理を有効化
        if self.battle_active:
            return self.menu.handle_mouse_event(event)
        return False
    
    def draw_buttons(self):
        self.menu.draw_buttons()

```
# GameOverクラス
```py 
import pygame as pg

from settings import *
from utils import TextSprite

class GameOver:
    def __init__(self):
        self.game_over_sprites = pg.sprite.Group()
        self.font = pg.font.Font("../battle/Meiryo.ttf", 36)
        self.text = TextSprite('Game Over', self.font, 
                               (255,255,255),  
                               (0,0,255), 
                               WIDTH / 2 -100, HEIGHT /2, self.game_over_sprites)
        self.font = pg.font.Font("../battle/Meiryo.ttf", 20)
        self.text2 = TextSprite('SPACE KEY TO CONTINUE...', self.font, 
                               (255,255,255),  
                               (0,0,255), 
                               WIDTH / 2 -150, HEIGHT /2 + HEIGHT/4, self.game_over_sprites)

```