CODE Python RPG

独学でPythonでRPGを作成する(第33回)攻撃呪文をモンスターグループや全体に

投稿日:

こんにちは。
「Pythonでつくる ゲーム開発 入門講座」の
Chapter11,12(本格RPGを作ろう!全編・後編)を基にRPGを作っていきます。

前回は主にマップに関する変更を加えました。

今回はまた戦闘関連に戻って、呪文の攻撃対象を変更します。
今まではモンスター単体への攻撃のみでしたが
モンスターグループや全体に攻撃できるようにします。

目次

  1. 呪文シート
  2. charaの変更
  3. battleの変更
  4. 動画
  5. 最後に

呪文シート

呪文をモンスターグループや全体に
攻撃できるようにします。

呪文シートを以下のようにしています。

呪文の説明やダメージ量は
以下のサイトを参考にさせて頂いています。

呪文の説明
ダメージ量

ダメージ量が2000とか
おかしい値になっているのは
テスト用です。
すぐにモンスターを倒せるように。。

G列(呪文演出)は
呪文を使った時の画面の色です。
これは適当です(^_^;)

 

で、今回のメインになるのが
F列のターゲットです。

今まで使えるのは
0の単体回復と
2の単体攻撃
のみでした。

今回の変更で
3の呪文はモンスターグループに
4の呪文はモンスター全体に
攻撃できるようにします。

 

なお、1の味方全員について
今は味方がいない(プレイヤー1人のみ)ので
単体と同じ動作をします。

味方を加えた時に変更しようと
思っています。

 

 

charaの変更

最初にchara.pyのCharaクラスを変更します。

変更前

(省略)
class Chara():
(省略)

    def spell(self, str_spell):
        if self.mp < spell_usemp_dict[str_spell]:
            return -1 # mpが足りない
        else:
            self.mp -= spell_usemp_dict[str_spell] # 消費mp
            return random.randint(spell_lo_dict[str_spell], spell_up_dict[str_spell])

変更後

(省略)
class Chara():
(省略)

    def confirm_spell(self, str_spell):
        if self.mp < spell_usemp_dict[str_spell]:
            return False
        else:
            self.mp -= spell_usemp_dict[str_spell] # 消費mp
            return True
    def use_spell(self, str_spell, num):
        dmg = []
        for _ in range(num):
            dmg.append(random.randint(spell_lo_dict[str_spell], spell_up_dict[str_spell]))
        return dmg

 

変更前はspellメソッドで
MPの有無を確認して、
足りなければ-1、
足りていればダメージ量
を返していました。

このメソッドを2つに分けました。

MPの有無の確認:confirm_spell
MPが足りていればTrue
MPが足りなければFalse
を返します。

ダメージ量の確認:use_spell
引数のnumは攻撃するモンスターの数です。
複数のモンスターに攻撃するので
各モンスターへのダメージ量を
リストに入れて返します。
単体に攻撃する(numが1)場合もあります。

 

使う呪文によってnumの値が
変わる(場合分けが必要)ので
メソッドを別々にしました。

 

chara.pyの変更は以上です。

 

 

battleの変更

次にbattle.pyを変更します。

グループへの攻撃の流れです。

  • (生き残っている)モンスターのグループ(重複)の確認。
  • グループを選択
  • 選択したグループへ攻撃

グループ(重複)の確認

まず、モンスターのグループ(重複)を
確認するメソッドを作成します。

戦闘が進んでモンスターを倒すごとに変わるので、
1回の戦闘で何度か呼び出すことになります。

def mon_overlap():
    # 呪文の対象のモンスターNoを取得(list_typに格納する)
    mon_no = [] # モンスターNoを格納
    for mon in monster:
        mon_no.append(mon.num)
    dic_typ = collections.Counter(mon_no) # 重複(倒すとmonsterが変わるので毎回取得) 例{9:1, 1:2}
    list_typ = list(dic_typ.keys()) # キーをリストで取得(キーがモンスターNo)list_typ[0]=9,list_typ[1]=1
    return dic_typ, list_typ

モンスターはchara.pyのMonsterクラスの
num(モンスターNo)で識別します。

生き残っているモンスターNoを
リストに入れて重複を取得します。

重複の取得はcollections.Counterを使います。
collections.Counterの詳細はこちら

結果の例としては
{9:1, 1:2}
の辞書型になります。
dic_typ[9] = 1
dic_typ[1] = 2

どちらも数字で分かりにくいですが
モンスターNo 9のモンスターが1匹
モンスターNo 1のモンスターが2匹
の2つのグループが存在するということになります。

 

また、存在するモンスターNoを
リストlist_typに格納します。
list_typ[0]=9
list_typ[1]=1

mon_overlap関数で
dic_typとlist_typを返します。

 

 

グループを選択

次に、攻撃対象のモンスターの
グループを選択します。

あわせてモンスター全体の場合も
追加しています。
※モンスター全体を攻撃する呪文

 

変更前

def spell_target_select(key, player): # 呪文の対象を選択
    global idx, spell_target_i
    ent = False
    if player.get_spell_target(player.mas_spell[sel_spell_i]) == 0: # 呪文の対象が味方
        if key[K_SPACE] or key[K_RETURN]: # 決定
            ent = True
    else: # 呪文の対象がモンスター
        if key[K_LEFT] and spell_target_i > 0: # ←キー
            spell_target_i -= 1
        elif key[K_RIGHT] and spell_target_i < len(monster)-1: # →キー
            spell_target_i += 1
        elif key[K_SPACE] or key[K_RETURN]: # 決定
            ent = True
    if key[K_b]: # キャンセル
        idx = 21 # idx=21 で spell_target_i=0にしている
    return ent

変更後

def spell_target_select(key, player): # 呪文の対象を選択
    global idx, spell_target_i
    ent = False
    if player.get_spell_target(player.mas_spell[sel_spell_i]) <= 1: # 呪文の対象が味方
        if key[K_SPACE] or key[K_RETURN]: # 決定
            ent = True
    elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 2: # 呪文の対象がモンスター単体
        if key[K_LEFT] and spell_target_i > 0: # ←キー
            spell_target_i -= 1
        elif key[K_RIGHT] and spell_target_i < len(monster)-1: # →キー
            spell_target_i += 1
        elif key[K_SPACE] or key[K_RETURN]: # 決定
            ent = True
    elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 3: # 呪文の対象がモンスターグループ
        dic_typ, list_typ = mon_overlap()
        if key[K_LEFT] and spell_target_i > 0: # ←キー
            spell_target_i -= 1
        elif key[K_RIGHT] and spell_target_i < len(dic_typ)-1: # →キー
            spell_target_i += 1
        elif key[K_SPACE] or key[K_RETURN]: # 決定
            ent = True
    elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 4: # 呪文の対象がモンスター全体
        if key[K_SPACE] or key[K_RETURN]: # 決定
            ent = True
    if key[K_b]: # キャンセル
        idx = 21 # idx=21 で spell_target_i=0にしている
    return ent

 

攻撃呪文の対象が3、4の場合を
追加しています。

グループの数はlen(dic_typ)で
取得できます。
※{9:1, 1:2}の場合、
 len(dic_typ)は2グループ。

これでグループ数以上に
右に行かないように
「→」キーに制限をかけています。

elif key[K_RIGHT] and spell_target_i < len(dic_typ)-1: # →キー

 

また、3の場合(グループを選択)も
変数spell_target_iを使います。

 

全体攻撃の4の場合は
モンスターを選択する必要がないので
決定するかどうかのみです。

 

なお、先に述べたように
1の場合(味方全員)について
今は味方がいないので
味方単体と同じ動作をします。
※0と1の場合は同じ動作

if player.get_spell_target(player.mas_spell[sel_spell_i]) <= 1: # 呪文の対象が味方

 

 

もう一つ、モンスターグループを
選択できるようにしたので
モンスターの表示も変更します。
※グループを選択している時は
 グループ全体を点滅させます。

変更前

def draw_battle(bg, fnt, obj, player): # 戦闘画面の描画 obj:戦闘で行動中のオブジェクト
(省略)

    for i, mon in enumerate(monster):
(省略)

        if display_flg:
            if monster[i] is obj: # 行動中のモンスターなら行動エフェクト
                bg.blit(mon.img, [monster[i].x, monster[i].y+emy_step])
            else:
                # 呪文対象を決める(idx=25)、呪文対象がモンスターの時、呪文対象を表示させない(点滅させる)
                if idx == 25 and i == spell_target_i and player.get_spell_target(player.mas_spell[sel_spell_i]) == 2 and tmr%5 == 0:
                    pass
                else:
                    bg.blit(mon.img, [monster[i].x, monster[i].y])

(省略)

変更後

def draw_battle(bg, fnt, obj, player): # 戦闘画面の描画 obj:戦闘で行動中のオブジェクト
(省略)

    for i, mon in enumerate(monster):
(省略)

        if display_flg:
            if mon is obj: # 行動中のモンスターなら行動エフェクト
                bg.blit(mon.img, [mon.x, mon.y+emy_step])
            else:
                # 呪文対象を決める(idx=25)、呪文対象がモンスター単体の時、呪文対象を表示させない(点滅させる)
                if idx == 25:
                    if player.get_spell_target(player.mas_spell[sel_spell_i]) == 2: # 呪文の対象がモンスター単体
                        if i == spell_target_i and tmr%5 == 0:
                            pass
                        else:
                            bg.blit(mon.img, [mon.x, mon.y])
                    elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 3: # 呪文の対象がモンスターグループ
                        dic_typ, list_typ = mon_overlap() # 例{9:1、1:1}、[9, 1]
                        if mon.num == list_typ[spell_target_i] and tmr%5 == 0: # モンスターNoとキーが一致したら呪文の対象モンスター
                            pass
                        else:
                            bg.blit(mon.img, [mon.x, mon.y])
                    elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 4: # 呪文の対象がモンスター全体
                        if tmr%5 == 0:
                            pass
                        else:
                            bg.blit(mon.img, [mon.x, mon.y])
                    else: # 回復系
                        bg.blit(mon.img, [mon.x, mon.y])
                else:
                    bg.blit(mon.img, [mon.x, mon.y])

 

elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 3:

3の場合(グループを選択)、
各モンスターが選択中のグループで
あるかを確認します。

if mon.num == list_typ[spell_target_i] and tmr%5 == 0:

選択中のモンスターNoは
list_typに格納していますので
これと各モンスターのモンスターNoが
一致するかを確認します。

一致する場合はそのモンスターを
点滅させます。
※tmrが5の倍数の時は表示しない(pass)

 

elif player.get_spell_target(player.mas_spell[sel_spell_i]) == 4:

全体攻撃の4の場合は全てのモンスターを点滅し、
0や1の回復の場合(else:)は常にモンスターを表示させます。

 

あと、なぜか変更前は「monster[i]」を
使っていましたが、
これを「mon」に変更しています。

グループ選択は以上です。

 

 

選択したグループへ攻撃

最後に選択したグループへ攻撃します。

まず、do_attack関数を2つに分けます。

変更前

def do_attack(target, dmg, obj):
    global idx, tmr
    tmp_exp = 0
    monster[target].hp -= dmg
    if monster[target].hp <= 0:
        monster[target].hp = 0
        set_message(obj.name + " は " + monster[target].name + " をやっつけた!")
        tmp_exp += monster[target].exp
        del_battle_turn(monster[target]) # ターンオブジェクトから消す(倒したモンスターは攻撃しない)
        dead_monster.append(monster[target]) # 先に加える
        monster.pop(target)
        if not monster: # monsterが空だとFalseを返すので not monsterがTrueだと空
            idx = 16 # 勝利
            tmr = 0
    return tmp_exp

変更後

def do_attack(target, dmg, obj):
    tmp_exp = 0
    monster[target].hp -= dmg
    if monster[target].hp <= 0:
        monster[target].hp = 0
        set_message(obj.name + " は " + monster[target].name + " をやっつけた!")
        tmp_exp += monster[target].exp
    return tmp_exp

def check_monster():
    global idx, tmr
    for mon in reversed(monster): # 複数の時、逆から消さないと倒せないモンスターが出る
        if mon.hp == 0:
            del_battle_turn(mon) # ターンオブジェクトから消す(倒したモンスターは攻撃しない)
            dead_monster.append(mon) # 先に加える
            monster.remove(mon)
    if not monster: # monsterが空だとFalseを返すので not monsterがTrueだと空
        idx = 16 # 勝利
        tmr = 0

 

変更前のdo_attack:
ダメージ計算の後に
モンスターを倒した場合は
リストmonsterからも削除して
戦闘勝利も判定していました。

変更後のdo_attack:
ダメージ計算をして
倒した場合は経験値を返します。

check_monster:
倒したモンスターがいれば
リストmonsterから削除して
空になれば戦闘勝利を判定します。

 

check_monster関数の中で
for文はmonsterを逆から
確認しています。
※reversedの詳細はこちら

理由はこのfor文の中で
リストmonsterから要素を削除する場合があり
for文の途中でリストから削除してしまうと
リストが崩れてしまうからです。

  1. monster = [A, B, C, D, E]をfor文で確認する
  2. for文でインデックスが3の時にD.hp==0でmonster[3]を削除
  3. monster = [A, B, C, E]になる
  4. monsterの要素が全部で4になるので
    インデックス3まで確認したfor文は
    ここで終わってしまう。
    ※Eを確認できていない。

 

do_attackを2つに分けたのも同じ理由です。

後述しますが、グループに攻撃する時は
do_attackをfor文の中で利用しますので
for文の途中でmonsterの要素を削除してしまうと
不都合が生じてしまいます。

この為、do_attackではmonsterから削除せずに
別の関数(check_monster)で削除するようにしました。

 

そして、モンスターへの攻撃です。 

変更前

def main(screen, clock, font, fontS, player, area):
(省略)

    dmg = 0 # プレイヤーが与えるダメージ、受けるダメージ
(省略)

        elif idx == 26: # 呪文発動
            draw_battle(screen, fontS, turn_obj, player)
            if tmr == 1:
                spell_blink = player.get_spell_blink(player.mas_spell[tmp_sel_spell_i]) # 呪文演出(いろ)
                spell_blink = spell_blink.split(",")
                set_message(turn_obj.name + " は " + turn_obj.mas_spell[sel_spell_i] + " となえた!")
                dmg = turn_obj.spell(turn_obj.mas_spell[sel_spell_i])
            if 2 <= tmr and tmr <= 4:
                pygame.draw.rect(screen, spell_blink[tmr%3], [0, 0, screen.get_width(), screen.get_height()])
            if tmr == 5:
                if turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 2: # 呪文の対象がモンスター1匹
                    set_message(monster[spell_target_i].name + "に " + str(dmg)+"ポイントのダメージを与えた!")
            if tmr == 11:
                if dmg != -1:
                    if turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 0: # 呪文の対象が味方一人
                        player.hp += dmg
                        if player.hp > player.maxhp:
                            player.hp = player.maxhp
                        set_message(player.name + " の キズが かいふくした!")
                    if turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 2: # 呪文の対象がモンスター1匹
                        btl_exp += do_attack(spell_target_i, dmg, turn_obj)
                else:
                    set_message("しかし MPがたりない!")

            if tmr == 16:
                init_message()
                idx = 24 # ターンの確認
                tmr = 0

変更後

def main(screen, clock, font, fontS, player, area):
(省略)

    dmg = 0 # プレイヤーが与えるダメージ、受けるダメージ
    spell_dmg = [] # プレイヤーが与えるダメージ、受けるダメージ 呪文用 対象がグループもしくは全体の場合があるのでリスト
(省略)

        elif idx == 26: # 呪文発動
            draw_battle(screen, fontS, turn_obj, player)
            if tmr == 1:
                spell_blink = player.get_spell_blink(player.mas_spell[tmp_sel_spell_i]) # 呪文演出(いろ)
                spell_blink = spell_blink.split(",")
                set_message(turn_obj.name + " は " + turn_obj.mas_spell[sel_spell_i] + " となえた!")
            if 2 <= tmr and tmr <= 4:
                pygame.draw.rect(screen, spell_blink[tmr%3], [0, 0, screen.get_width(), screen.get_height()])
            if tmr == 8:
                if turn_obj.confirm_spell(turn_obj.mas_spell[sel_spell_i]): # mpが足りていたら
                    if turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 0: # 呪文の対象が味方単体
                        spell_dmg = turn_obj.use_spell(turn_obj.mas_spell[sel_spell_i], 1)
                        player.hp += spell_dmg[0] # 単体なので添字は0
                        if player.hp > player.maxhp:
                            player.hp = player.maxhp
                        set_message(player.name + " の キズが かいふくした!")
                    elif turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 1: # 呪文の対象が味方全体
                        pass
                    elif turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 2: # 呪文の対象がモンスター単体
                        spell_dmg = turn_obj.use_spell(turn_obj.mas_spell[sel_spell_i], 1)
                        set_message(monster[spell_target_i].name + "に " + str(spell_dmg[0])+"ポイントのダメージを与えた!")
                        btl_exp += do_attack(spell_target_i, spell_dmg[0], turn_obj)
                        check_monster()
                    elif turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 3: # 呪文の対象がモンスターグループ
                        # 呪文の対象のモンスターNoを取得(list_typに格納する)して、対象モンスターNoの数を取得する→ dic_typ[list_typ[spell_target_i]]
                        dic_typ, list_typ = mon_overlap() # 例{9:1、1:1}、[9, 1]
                        spell_dmg = turn_obj.use_spell(turn_obj.mas_spell[sel_spell_i], dic_typ[list_typ[spell_target_i]]) # dic_typのvalueを使う、list_typ[spell_target_i]はキー(モンスターNo)
                        j = 0
                        for i, mon in enumerate(monster):
                            if mon.num == list_typ[spell_target_i]:
                                set_message(mon.name + "に " + str(spell_dmg[j])+"ポイントのダメージを与えた!")
                                btl_exp += do_attack(i, spell_dmg[j], turn_obj)
                                draw_battle(screen, fontS, turn_obj, player)
                                pygame.display.update()
                                time.sleep(1)
                                j += 1
                        check_monster()
                    elif turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 4: # 呪文の対象がモンスター全体
                        spell_dmg = turn_obj.use_spell(turn_obj.mas_spell[sel_spell_i], len(monster)) # dic_typのvalueを使う、list_typ[spell_target_i]はキー(モンスターNo)
                        j = 0
                        for i, mon in enumerate(monster):
                            set_message(mon.name + "に " + str(spell_dmg[j])+"ポイントのダメージを与えた!")
                            btl_exp += do_attack(i, spell_dmg[j], turn_obj)
                            draw_battle(screen, fontS, turn_obj, player)
                            pygame.display.update()
                            time.sleep(1)
                            j += 1
                        check_monster()
                else:
                    set_message("しかし MPがたりない!")
           
            if tmr == 16:
                init_message()
                idx = 24 # ターンの確認
                tmr = 0

 

dmg = turn_obj.spell(turn_obj.mas_spell[sel_spell_i]) 

変更前は最初にダメージを取得していましたが
対象が単体かグループかによって変わりますので
場合分け(if文)の中で取得します。

chara.pyに追加したuse_spellメソッドで
ダメージを取得します。

この時、取得するダメージはリストspell_dmgに
入れていきます。

 

if turn_obj.confirm_spell(turn_obj.mas_spell[sel_spell_i]): # mpが足りていたら

MPの有無は同じくchara.pyに追加した
confirm_spellメソッドで確認します。

 

elif turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 2: # 呪文の対象がモンスター単体
    spell_dmg = turn_obj.use_spell(turn_obj.mas_spell[sel_spell_i], 1)

対象がモンスター単体の時はuse_spellの引数numは
1となります。(モンスターの数)

 

elif turn_obj.get_spell_target(turn_obj.mas_spell[sel_spell_i]) == 3: # 呪文の対象がモンスターグループ
    dic_typ, list_typ = mon_overlap()
    spell_dmg = turn_obj.use_spell(turn_obj.mas_spell[sel_spell_i], dic_typ[list_typ[spell_target_i]])

対象がモンスターグループの場合
use_spellの引数numは
dic_typ[list_typ[spell_target_i]]となります。
※グループのモンスターの数

dic_typ = {9:1, 1:2}
dic_typ[9] = 1
dic_typ[1] = 2

list_typ[0]=9
list_typ[1]=1

の時に、spell_target_iが1であれば
グループのモンスターの数は
dic_typ[list_typ[spell_target_i]] = 2匹
となります。

 

また、先ほど述べたように
do_attackはfor文の中で利用します。

for i, mon in enumerate(monster):
    if mon.num == list_typ[spell_target_i]:
        set_message(mon.name + "に " + str(spell_dmg[j])+"ポイントのダメージを与えた!")
        btl_exp += do_attack(i, spell_dmg[j], turn_obj)
        draw_battle(screen, fontS, turn_obj, player)
        pygame.display.update()
        time.sleep(1)
        j += 1
check_monster()

この為、do_attack関数の中で
monsterから削除してしまうと
判定がおかしくなってしまいます。

なので、monsterの削除はfor文の外で
check_monster関数を使います。

for i, mon in enumerate(reversed(monster)):
    if mon.num == list_typ[spell_target_i]:

ちなみに、ここでreversedを使うと
グループ内の右に位置しているモンスターから
攻撃してしまいます。

 

また、for文の中で「pygame.display.update()」を
使っています。
これがないと、倒したモンスターなどが
複数いる場合も同時に消えます。
※倒したモンスターから順番に消えてくれません。

ここで「pygame.display.update()」を使うのは
あまり良くないかもしれません。。
が、他にやりようがあるのか分かりませんでした。

 

残りの4(全体攻撃)も3(グループ攻撃)と
ほぼ同じとなります。

use_spellのモンスターの数の引数numは
生存モンスター全体になる為、
len(monster)となります。
※回復は割愛します。

 

長くなりましたが、モンスターグループや全体に
攻撃する変更は以上となります。

 

 

動画

モンスターのグループや全体に攻撃する様子を
こちらに動画をアップしました。

ご参考までに。

 

という感じで、今回は以上です!(`・ω・´)

 

 

最後に

今回はモンスターグループや全体に攻撃できるように
変更しました。

次回は補助系の呪文を追加していきたいと思います。

まとめサイトへ

 

 

【ご注意】
プログラムやデータなどは著作権法により保護されています。
著作者の許諾を得ずに、プログラムおよびデータそのものまたは改変したものを
配布したり販売したりすることはできません。
また、これらを利用して発生した損害などに関して、著作者は一切責任を負いません。

 

 

-CODE, Python, RPG
-, , , , , , , ,

Copyright© kerublog , 2021 All Rights Reserved Powered by STINGER.