こんにちは。
「Pythonでつくる ゲーム開発 入門講座」の
Chapter11,12(本格RPGを作ろう!全編・後編)を基にRPGを作っていきます。
前回は戦闘に関する修正で、攻撃など相手に使う呪文を使えるようにしました。
今回も戦闘に関する修正で以下に対応します。
・同一ターン内に対象モンスターを仲間が倒した場合の対象変更
・戦闘勝利後、仲間もレベルアップする
・その他、バグ修正
目次
攻撃対象(単体)の変更
同一ターン内に対象モンスターを
仲間が倒した場合、
同じ種類のモンスターの中から
ランダムで選択するように変更します。
※同じ種類のモンスターがいないなら
生存モンスターの中からランダムで選択する。
コマンド選択が終わった後、
素早さの高い順に
各オブジェクトの行動が
始まります。
この時、攻撃対象のモンスターを
仲間が先に倒した場合、
モンスターの種類に関係なく
生存モンスターからランダムに
選択していました。
例:
モンスターA, モンスターB, モンスターC
キャラ1の攻撃対象:モンスターA
キャラ2の攻撃対象:モンスターA
キャラ3の攻撃対象:モンスターA
キャラ4の攻撃対象:モンスターA
上記の場合、キャラ1がモンスターAを倒すと
残りのキャラ2〜4はモンスターBとCのどちらかを
ランダムで選択して攻撃します。
変更前
def main(screen, clock, font, fontS, player, area):
(省略)
elif idx == 12: # プレイヤーの攻撃
draw_battle(screen, fontS, turn_obj, player)
if tmr == 1:
set_message(turn_obj.name + " のこうげき!")
alive_flag = False # 攻撃対象が生存しているか
for i, mon in enumerate(monster):
if mon is turn_obj.target:
btl_enemy = i # エフェクトの位置に使う
alive_flag = True # 攻撃対象が生存している
break
if not alive_flag:
btl_enemy = random.randint(0, len(monster)-1)
turn_obj.target = monster[btl_enemy]
dmg, get_message = turn_obj.attack(turn_obj.target)
btl_enemy = random.randint(0, len(monster)-1)
特に条件を設けずランダムで
攻撃対象を選択しています。
これを
同じ種類のモンスターが生存している場合
同じ種類のモンスターの中から
ランダムで選択するように変更します。
また、この機能を関数にして
攻撃呪文の場合も呼び出すようにします。
def dead_alive(obj): # (攻撃)対象のモンスターが生存していればそのまま返す、そうでなければ別のモンスターを選んで返す
global btl_enemy
mon_no = []
for i, mon in enumerate(monster):
if mon is obj:
btl_enemy = i # エフェクトの位置に使う
return obj # 攻撃対象が生存している
elif mon.num == obj.num:
mon_no.append(i)
if mon_no: # 同じ種類のモンスターがいる場合は同じ種類からランダム
btl_enemy = random.randint(mon_no[0], mon_no[-1])
else: # 同じ種類のモンスターがいない場合は生存モンスターからランダム
btl_enemy = random.randint(0, len(monster)-1)
obj = monster[btl_enemy]
return obj
(省略)
def main(screen, clock, font, fontS, player, area):
(省略)
elif idx == 12: # プレイヤーの攻撃
draw_battle(screen, fontS, turn_obj, player)
if tmr == 1:
set_message(turn_obj.name + " のこうげき!")
turn_obj.target = dead_alive(turn_obj.target)
dmg, get_message = turn_obj.attack(turn_obj.target)
for i, mon in enumerate(monster):
(省略)
elif mon.num == obj.num:
mon_no.append(i)
対象オブジェクトが既にいない場合
同じ種類のモンスターがいるなら
リストmon_noに
モンスターの位置(左から何番目か)を
格納しておきます。
※enumerateについてはこちら。
if mon_no: # 同じ種類のモンスターがいる場合は同じ種類からランダム
btl_enemy = random.randint(mon_no[0], mon_no[-1])
else: # 同じ種類のモンスターがいない場合は生存モンスターからランダム
btl_enemy = random.randint(0, len(monster)-1)
obj = monster[btl_enemy]
同じ種類のモンスターがいれば
その中からランダムで選択、
同じ種類のモンスターがいないなら
生存モンスターからランダムで選択します。
※mon_no[-1]はリストの最後の値です。
以下のような流れになります。

キャラ1の攻撃対象:Green slime A
キャラ2の攻撃対象:Green slime A
キャラ3の攻撃対象:Green slime A
キャラ4の攻撃対象:Green slime A
最初のキャラがGreen slime Aを攻撃して倒す

mon_noの値は[0,1]となり、
0もしくは1を選択します。
※random.randint(mon_no[0], mon_no[-1])
→random.randint(0, 1) となります。
※mon_noは左からの位置なので
同じ種類のモンスター(Green slime)の
位置である0と1となります。
次のキャラがGreen slime Cを攻撃して倒す
※Green slime Cの位置である「1」を選択された

mon_noの値は[0]となります。
※random.randint(mon_no[0], mon_no[-1])
→random.randint(0, 0) となります。
次のキャラがGreen slime Bを攻撃して倒す

mon_noの値は[]となり
生存モンスターから選択します。
※random.randint(0, len(monster)-1)
→random.randint(0, 0)となります。
最後のキャラがRed slimeを倒して戦闘終了

単体の攻撃対象の変更は以上です。
攻撃対象(グループ)の変更
続いて、
呪文攻撃で対象がグループの時、
行動の際には、そのグループが
既にいなくなっている場合、
生存している他のグループを
選択して攻撃するようにします。
今は以下のようになっています。
def mon_overlap():
# 呪文の対象のモンスターNoを取得(list_typに格納する)
mon_no = [] # モンスターNoを格納
for mon in monster: # 既にfor文の中であり、monを使っているので、ここでmonは使えない
mon_no.append(mon.num)
dic_typ = collections.Counter(mon_no) # 重複を抽出(倒すとmonsterが変わるので毎回取得) 例{9:1、1:1}
list_typ = list(dic_typ.keys()) # キーをリストで取得(キーがモンスターNo)list_typ[0]=9,list_typ[1]=1
return list_typ
(省略)
def main(screen, clock, font, fontS, player, area):
(省略)
elif idx == 26: # 呪文発動
(省略)
elif spell_target_no == 3: # 呪文の対象がモンスターグループ
# 呪文の対象のモンスターNoを取得(list_typに格納する)して、対象モンスターNoの数を取得する→ dic_typ[list_typ[spell_target_i]]
list_typ = mon_overlap() # 例{9:1、1:1}、→ [9, 1]を取得
for i, mon in enumerate(monster):
if mon.num == list_typ[turn_obj.target]: # グループの時だけturn_obj.targetにはインデックスが入っている
spell_point, spell_message = turn_obj.use_spell(turn_obj.spell, mon, spell_target_no)
(省略)
list_typ = mon_overlap() # 例{9:1、1:1}、→ [9, 1]を取得
上記の例の場合、
攻撃対象は
turn_obj.targetが0の場合
list_typ[0] → モンスター種別No9
turn_obj.targetが1の場合
list_typ[1] → モンスター種別No1
のグループとなります。
モンスター種別Noは
「モンスター」シートのA列です。

ちなみに以下のような場合、
攻撃対象がGreen slimeだと
問題はありません。
※list_typ = [1, 2]
1:Green slimeのNo
2:Red slimeのNo

キャラ1の攻撃呪文対象:Green slime
キャラ2の攻撃呪文対象:Green slime
キャラ3の攻撃呪文対象:Green slime
キャラ4の攻撃呪文対象:Green slime
※いずれもturn_obj.targetは「0」
最初のキャラが呪文の
グループ攻撃でGreen slimeを倒す

Green slimeがいなくなると
list_typ = [2]
となるので
list_typ[0] = 2となり
次のキャラは
Red slimeを攻撃します。

ただ、同じ例で
先にRed slimeを倒すと
エラーとなります。

キャラ1の攻撃呪文対象:Red slime
キャラ2の攻撃呪文対象:Red slime
キャラ3の攻撃呪文対象:Red slime
キャラ4の攻撃呪文対象:Red slime
※いずれもturn_obj.targetは「1」
最初のキャラが呪文の
グループ攻撃でRed slimeを倒す

Re slimeがいなくなると
list_typ = [1]
となりますが
turn_obj.targetは「1」の為
list_typ[1]は
「list index out of range」の
エラーとなってしまいます。
これを解消する為に
グループの攻撃対象を
変える関数を作成します。
def dead_alive_g(no): # (攻撃)対象のモンスターグループが生存していればそのまま返す、そうでなければ別のモンスターグループを選んで返す
global btl_enemy
mon_no = mon_overlap()
if len(mon_no)-1 >= no: # noはグループのインデックス
for i in mon_no:
if i == mon_no[no]: # 対象のグループが生存
return no # 攻撃対象が生存している
return random.randint(0, len(mon_no)-1)
引数「no」はturn_obj.targetの値です。
if len(mon_no)-1 >= no:
この条件は先程の例で
エラーの原因となった
「list index out of range」
を回避する為です。
※この条件がないと
mon_no[no]でエラーとなります。
for i in mon_no:
if i == mon_no[no]: # 対象のグループが生存
return no # 攻撃対象が生存している
対象のグループが生存していれば
そのまま攻撃します。
return random.randint(0, len(mon_no)-1)
対象のグループが生存していないと
残りのグループからランダムで選択します。
グループの対象変更は以上です。
呪文の対象変更
呪文発動のときに
攻撃対象を変更できるように
関数を呼び出します。
def main(screen, clock, font, fontS, player, area):
(省略)
elif idx == 26: # 呪文発動
(省略)
elif spell_target_no == 2: # 呪文の対象がモンスター単体
turn_obj.target = dead_alive(turn_obj.target) # 対象が生存しているか、していないなら別の対象を取得
spell_point, spell_message = turn_obj.use_spell(turn_obj.spell, turn_obj.target, spell_target_no)
(省略)
elif spell_target_no == 3: # 呪文の対象がモンスターグループ
# 呪文の対象のモンスターNoを取得(list_typに格納する)して、対象モンスターNoの数を取得する→ dic_typ[list_typ[spell_target_i]]
list_typ = mon_overlap() # 例{9:1、1:1}、→ [9, 1]を取得
turn_obj.target = dead_alive_g(turn_obj.target) # 対象グループが生存しているか、していないなら別の対象グループを取得、turn_obj.targetはグループ(list_typ)のインデックス
(省略)
turn_obj.target = dead_alive(turn_obj.target)
turn_obj.target = dead_alive_g(turn_obj.target)
を追加しただけです。
攻撃対象の変更については以上です。
レベルアップ
戦闘勝利後に
獲得する経験値が
一定数を超えると
レベルアップしますが
対象が全員ではないので
修正します。
def main(screen, clock, font, fontS, player, area):
(省略)
p_lvup = "" # レベルアップしたキャラ
(省略)
elif idx == 16: # 勝利
(省略)
if tmr == 5:
if len(player) == 1:
set_message(str(btl_exp) + "ポイントの けいけんちを かくとく!")
else:
set_message("それぞれ" + str(btl_exp) + "ポイントの けいけんちを かくとく!")
for p in player:
if p.exp >= p.lv_exp:
p_lvup = p
idx = 17
tmr = 0
break
if tmr == 10:
idx = 22 # 戦闘終了
elif idx == 17: # レベルアップ
draw_battle(screen, fontS, turn_obj, player)
if tmr == 5:
set_message(p_lvup.name + " はレベルが上がった!") # player[0]からp_lvupに変更
p_lvup.lv_up() # player[0]からp_lvupに変更
if tmr == 21:
init_message()
set_message("最大HP:"+str(p_lvup.maxhp)) # player[0]からp_lvupに変更
if tmr == 26:
set_message("素早さ:"+str(p_lvup.quick)) # player[0]からp_lvupに変更
if tmr == 30:
set_message("攻撃力:"+str(p_lvup.atk)) # player[0]からp_lvupに変更
if tmr == 34:
set_message("防御力:"+str(p_lvup.dfs)) # player[0]からp_lvupに変更
if tmr == 38:
tmp_spell = p_lvup.master_spell() # player[0]からp_lvupに変更
if tmp_spell != "":
set_message(tmp_spell + " を覚えた")
if tmr == 40:
for p in player:
if p.exp >= p.lv_exp:
p_lvup = p
idx = 17
tmr = 0
break
if tmr == 45:
idx = 22 # 戦闘終了
for p in player:
if p.exp >= p.lv_exp:
p_lvup = p
idx = 17
tmr = 0
break
変更前は
if player[0].exp >= player[0].lv_exp:
と1人しか確認していませんでした。
全員確認して
ひとまず1人でも
レベルアップしたら
対象オブジェクトを
p_lvupに格納して
idx17に移ります。
if tmr == 40:
for p in player:
if p.exp >= p.lv_exp:
p_lvup = p
idx = 17
tmr = 0
break
idx17でも同様に確認して
他にレベルアップした
キャラがいれば再度idx17に
戻ります。
あと、player[0]を修正した行に
コメントに記載しました。
※ # player[0]からp_lvupに変更
レベルアップについては以上です。
不意打ちのバグ修正
モンスターと遭遇した時に
不意打ちの場合、
おかしくなっていたので
修正します。
変更前
def set_battle_turn(num, player): # 0:通常、1:先制攻撃、2:不意打ち
global battle_order
tmp_order = {}
# 素早さの順に並べる(戦闘順)
if num == 0 or num == 1: # 不意打ち(2)の場合は行動できない
for p in player:
r = random.randint(66, 100)
tmp_order[p] = int(p.quick * r/100)
if num == 0 or num == 2:
for mon in monster:
r = random.randint(66, 100)
tmp_order[mon] = int(mon.quick * r/100)
# [(obj, quick), (ojb, quick), ...]のリストになる
battle_order = sorted(tmp_order.items(), key=lambda x:x[1], reverse=True)
(省略)
def main(screen, clock, font, fontS, player, area):
(省略)
elif idx == 10: # 戦闘開始
(省略)
if btl_start == 2: # 不意打ち
idx = 23
else:
idx = 11
tmr = 0
(省略)
elif idx == 23: # ターンセット
if btl_player == len(player)-1: # 全員のコマンド選択完了
set_battle_turn(btl_start, player)
for mon in monster:
mon.set_monster_cmd()
btl_player = 0 # コマンドを選択するプレイヤーを最初に戻す
btl_start = 0 # 戦闘は通常に戻す
idx = 24
else: # 選択していなプレイヤーがいる(眠っているときはactが28がセットされている)
idx = 11
btl_player += 1
if btl_start == 2: # 不意打ち
idx = 23
不意打ちの場合、
コマンドは選択せずに
idx23となります。
else: # 選択していなプレイヤーがいる(眠っているときはactが28がセットされている)
idx = 11
btl_player += 1
ただ、idx23では
不意打ちの条件が無い為
idx11のコマンド選択に
戻ってしまいます。
if num == 0 or num == 1: # 不意打ち(2)の場合は行動できない
for p in player:
r = random.randint(66, 100)
tmp_order[p] = int(p.quick * r/100)
なお、関数set_battle_turnでは
不意打ちの条件がある為
キャラは行動しません。
※コマンド選択はできますが
行動はできない状態です。
この為、idx23の条件を変えます。
def main(screen, clock, font, fontS, player, area):
(省略)
elif idx == 23: # ターンセット
if (btl_start == 0 or btl_start == 1) and btl_player != len(player)-1:
idx = 11
btl_player += 1
else:
set_battle_turn(btl_start, player)
for mon in monster:
mon.set_monster_cmd()
btl_player = 0 # コマンドを選択するプレイヤーを最初に戻す
btl_start = 0 # 戦闘は通常に戻す
idx = 24
if (btl_start == 0 or btl_start == 1) and btl_player != len(player)-1:
不意打ちではない
かつ
コマンド選択していないキャラがいる
場合はコマンド選択する
に修正しました。
あちこちで同じことを確認して
あまり良くないと思いますけど、、
今回は以上です!(`・ω・´)
最後に
今回も仲間を追加したことに伴う
変更・修正を行いました。
次回ももう少し変更・修正をしたいと思います。
【ご注意】
プログラムやデータなどは著作権法により保護されています。
著作者の許諾を得ずに、プログラムおよびデータそのものまたは改変したものを
配布したり販売したりすることはできません。
また、これらを利用して発生した損害などに関して、著作者は一切責任を負いません。