ドラクエ風RPGを作る (第十八回) 戦闘処理の実装

 # はじめに

今回は戦闘処理を実装します。
コマンドウィンドウで「たたかう」を選択すると、敵と戦えます。

# コードの場所

GitHub に置いています。

[作成したスクリプト](https://github.com/PythonMan3/PythonGames/tree/master/ShortTaleStory/18)

# 実行画面

![screen.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/629087/20996944-c335-9c73-e36f-e02fc3d7f452.jpeg)

# 今回の内容

## 戦闘の実装

戦闘の実装するために、状態ハンドラ内のコードを修正します。
戦闘が終わった状態として、BATTLE_END を追加します。
以下は、状態ハンドラの呼び出し部分のコードです。
(変更点は前回から、BATTLE_END を追加しただけです)

```main.py
class PyRPG:
    ...
    def check_event(self):
        # 表示されているウィンドウに応じてイベントハンドラを変更
        if game_state == TITLE:
            ....
        elif game_state == BATTLE_INIT:
            self.battle_init_handler(event)
        elif game_state == BATTLE_COMMAND:
            self.battle_cmd_handler(event)
        elif game_state == BATTLE_PROCESS:
            self.battle_proc_handler(event)
        elif game_state == BATTLE_END:
            self.battle_end_handler(event)
```

次に、戦闘の状態ハンドラのコード部分です。
長いですが、全体を見てみます。

```main.py
class PyRPG:
    ...
    def calculate_damage(self, attack, defense):
        # 基本ダメージ = (攻撃力 / 2) - (守備力 / 4)
        # 最終ダメージ = 基本ダメージ ± (0～4のランダムな値)
        # 但し、0以下にならないようにする
        damage = attack // 2 - defense // 4
        damage = max(damage + random.randint(0, 4) * random.choice([-1, 1]), 1)
        return damage
    def battle_cmd_handler(self, event):
        """戦闘コマンドウィンドウが出ているときのイベントハンドラ"""
        global game_state
        # バトルコマンドのカーソル移動
        ...
        # バトルコマンドの決定
        if event.type == KEYDOWN and event.key == K_SPACE:
            if not self.msgwnd.next():
                #sounds["pi"].play()
                if self.battle.cmdwnd.command == BattleCommandWindow.ATTACK:  # たたかう
                    damage = self.calculate_damage(self.battle.player.player_status["attack"],
                        self.battle.monster_status["defense"])
                    self.battle.monster_status["hp"] -= damage
                    if self.battle.monster_status["hp"] < 0:  # 負にならないようにする
                        self.battle.monster_status["hp"] = 0
                    self.msgwnd.set(f"{self.battle.monster_status['name']}に{damage}のダメージ！")
                    if self.battle.monster_status["hp"] <= 0:
                        self.battle.is_monster_dead = True
                        self.battle.is_player_turn = False
                        self.battle.cmdwnd.hide()
                        game_state = BATTLE_PROCESS
                        return
                elif self.battle.cmdwnd.command == BattleCommandWindow.SPELL:  # じゅもん
                    self.msgwnd.set("じゅもんを　おぼえていない。")
                elif self.battle.cmdwnd.command == BattleCommandWindow.ITEM:  # どうぐ
                    self.msgwnd.set("どうぐを　もっていない。")
                elif self.battle.cmdwnd.command == BattleCommandWindow.ESCAPE:  # にげる
                    self.msgwnd.set("ゆうしゃは　にげだした。")
                self.battle.is_player_turn = False
                self.battle.cmdwnd.hide()
                game_state = BATTLE_PROCESS
    def battle_proc_handler(self, event):
        """戦闘の処理"""
        global game_state
        if event.type == KEYDOWN and event.key == K_SPACE:
            if not self.msgwnd.next():
                self.msgwnd.hide()
                if self.battle.is_monster_dead:
                    self.msgwnd.set(f"{self.battle.monster_status['name']}をたおした！")
                    game_state = BATTLE_END  # 戦闘終了してフィールドに戻る
                    return
                elif self.battle.cmdwnd.command == BattleCommandWindow.ESCAPE:
                    # フィールドへ戻る
                    #self.fieldMap.play_bgm()
                    game_state = FIELD
                else:
                    if self.battle.is_player_turn:
                        game_state = BATTLE_COMMAND
                        self.battle.cmdwnd.show()
                    else:
                        # モンスターが攻撃してくる
                        damage = self.calculate_damage(self.battle.monster_status["attack"],
                            self.battle.player.player_status["defense"])
                        self.battle.player.player_status["hp"] -= damage  # プレイヤーのHPを減らす
                        if self.battle.player.player_status["hp"] < 0:  # HPが負にならないようにする
                            self.battle.player.player_status["hp"] = 0
                        self.msgwnd.set(f"{self.battle.monster_status['name']}のこうげき！ ゆうしゃに{damage}のダメージ！")
                        if self.battle.player.player_status["hp"] <= 0:
                            self.battle.is_player_deading = True
                            game_state = BATTLE_END
                        self.battle.is_player_turn = True
    def battle_end_handler(self, event):
        global game_state
        """バトルが終了したときの処理を行う"""
        # フィールドに戻る処理
        if event.type == KEYDOWN and event.key == K_SPACE:
            if not self.msgwnd.next():
                if self.battle.is_player_deading:
                    if self.battle.is_player_dead:
                        self.msgwnd.hide()
                        game_state = TITLE  # ゲーム状態をとりあえずタイトルに戻す
                    else:
                        # 一度に１回しか文字列を設定出来ないのでis_player_deadingとis_player_deadの２つに分ける
                        self.msgwnd.set("ゆうしゃはしんでしまった！")
                        self.battle.is_player_dead = True
                else:
                    game_state = FIELD  # ゲーム状態をフィールドに戻す
                    self.battle.cmdwnd.hide()  # コマンドウィンドウを隠す
```

ゲーム状態によって、処理を分岐しています。
また、ゲーム状態だけで分岐出来ない場合、例えば、self.battle.is_player_deading (プレイヤー死に途中) や self.battle.is_player_dead (プレイヤー死亡) などのフラグでも処理を分岐しています。

これは、メッセージ処理が少し使いづらい事に起因しています。
メッセージ処理は以下のように、まず self.msgwnd.set(...) で、メッセージウィンドウの文字列を設定します。これは１回だけ呼ばれるようにします。
次に、game_state を変更して(つまり、状態を分岐して)、「スペースキーが押されて」かつ「メッセージが空」の場合に、次の処理に進みます。
このような手順になるため、状態分岐が大量に発生します。
今後、攻撃回避や魔法処理など、もっとコードを追加していく必要があるので、より複雑になります。
その対策として次回では、「イベントスクリプト」を導入します。

```py
def battle_cmd_handler(self, event):
    ...
    self.msgwnd.set(f"{self.battle.monster_status['name']}に{damage}のダメージ！")
    ...
    game_state = BATTLE_PROCESS

def battle_proc_handler(self, event):
    ...
    if event.type == KEYDOWN and event.key == K_SPACE:
        if not self.msgwnd.next():
            # ここで次の処理
```

## プレイヤーのステータスの追加

次のようなプレイヤーのステータス情報をデータ化しておきます。
GPT に聞いたら、ドラクエ１は最大レベルが３０でステータスがデータ化されているとの事です。
本当かどうかは不明ですが、GPT にデータ表を作ってもらいました。

```player_level_data.py
# プレイヤーのレベル1から30までのステータス(最大Lv30)
MAX_LEVEL = 30
level_stats = {
    1: {"hp": 15, "attack": 4, "defense": 2, "mp": 0, "experience": 0, "speed": 5},
    2: {"hp": 20, "attack": 5, "defense": 4, "mp": 2, "experience": 7, "speed": 6},
    3: {"hp": 25, "attack": 8, "defense": 6, "mp": 3, "experience": 23, "speed": 7},
    ...
    30: {"hp": 350, "attack": 140, "defense": 120, "mp": 60, "experience": 143000, "speed": 34},
}
```

次のように、ゲーム開始時にプレイヤーのステータスをリセットします。
Plyaer クラスに start() というメソッドを追加して、ゲーム開始時に呼び出します。

```main.py
class Player(pygame.sprite.Sprite):
    def __init__(self, filename):
        ...
        self.start()
    def get_player_level_status(self, level):
        return {"name": "ゆうしゃ",
            "lv": level,
            "hp": player_level_data.level_stats[level]["hp"],
            "mp": player_level_data.level_stats[level]["mp"],
            "attack": player_level_data.level_stats[level]["attack"],
            "defense": player_level_data.level_stats[level]["defense"],
            "speed": player_level_data.level_stats[level]["speed"]}
    def start(self):
        # プレイヤーのステータス
        self.player_status = self.get_player_level_status(1)  # HPや攻撃力をリセット
```

ゲーム開始時の呼び出し部分は以下です。

```main.py
class PyRPG:
    ...
    def title_handler(self, event):
        """タイトル画面のイベントハンドラ"""
        ...
        if event.type == KEYDOWN and event.key == K_SPACE:
            ...
            if self.title.menu == Title.START:
                self.player.start()  # Playerを初期化
```

 # おわりに

次回は、戦闘処理に対して、イベントスクリプトを使用して実装を変更します。