連続攻撃技とは
追加効果だけでは処理できない技が、初代に限定していても数多くあります。その一つが「連続攻撃技」です。
連続攻撃技はさらに4パターンに分かれる。
- 攻撃回数が2回固定であるもの
- 攻撃回数が3回固定であるもの
- 攻撃回数が2回から5回までの間で決まるもの
- 攻撃回数が最大3回で外れるまで攻撃するもの
急所判定も攻撃のたびなので、少なくとも1回は急所に当たる確率は通常のわざより高い
初代の連続攻撃技は「2回固定」と「攻撃回数が2〜5回」のどちらかに分類されます。
それでは、連続攻撃技を実装していきましょう。
ヒット回数の算出
それでは、連続攻撃技のサンプルとして「れんぞくパンチ」を実装します。最新世代では無くなってしまった技の1つですが、初代ではしっかりと現存していたで用意しましょう。
れんぞくパンチ(/Classes/Move/CometPunch.php)
<?php
$root_path = __DIR__.'/../..';
require_once($root_path.'/Classes/Move.php');
// れんぞくパンチ
class CometPunch extends Move
{
/**
* 正式名称
* @var string
*/
protected $name = 'れんぞくパンチ';
/**
* 説明文
* @var string
*/
protected $description = '2〜5回連続で攻撃する';
/**
* タイプ
* @var string
*/
protected $type = 'Normal';
/**
* 分類
* @var string(physical:物理|special:特殊|status:変化)
*/
protected $species = 'physical';
/**
* 威力
* @var integer
*/
protected $power = 18;
/**
* 命中率
* @var integer
*/
protected $accuracy = 85;
/**
* 使用回数
* @var integer
*/
protected $pp = 15;
/**
* 優先度
* @var integer
*/
protected $priority = 0;
/**
* 連続攻撃回数
*
* @return integer
*/
public function times()
{
return random_int(2, 5);
}
}
他の技との変更点は「times」という技回数取得用のメソッドがあることです。連続攻撃技以外は1回を返せるように、親クラスのmoveにもtimesメソッドを作成しておきましょう。
技クラス(/Classes/Move.php)
<?php
$root_path = __DIR__.'/..';
require_once($root_path.'/App/Traits/InstanceTrait.php');
require_once($root_path.'/App/Traits/ResponseTrait.php');
// 技
abstract class Move
{
use InstanceTrait;
use ResponseTrait;
--省略
/**
* 技回数
*
* @return integer
*/
public function times()
{
// デフォルトは1回
return 1;
}
これでtimesメソッドを使って技回数を取得した際、通常技なら1回、連続攻撃技なら各技クラスに割り当てられた回数を取得することができます。
繰り返し処理
次に連続攻撃の繰り返し処理についてです。連続技の場合、算出した回数分攻撃を繰り返すことになりますが、そのままattackメソッドを回数分実行するわけにはいきません。なぜなら、命中判定は毎回実行されるわけではないからです。
※一部の技を除きます
なので、命中判定移行の処理を回数分繰り返しましょう。
攻撃用トレイト(/App/Traits/Service/Battle/ServiceBattleTrait.php)
/**
* 攻撃
* (攻撃→ダメージ計算→ひんし判定)
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param object $move
* @return void
*/
protected function attack($atk_pokemon, $def_pokemon, $move)
{
// (省略)ここで命中判定(checkHit)終了
// 技を回数分実行
$times = $move->times();
for($i = 0; $i < $times; $i++){
// 攻撃判定成功時の処理
$this->attackSuccess($atk_pokemon, $def_pokemon, $move);
}
// 連続技はヒット回数のメッセージを返却
if($times > 1){
return $this->setMessage($times.'回当たった');
}
}
ダメージ計算処理を、attackSuccessというメソッドにまとめ、forを使ってループさせました。通常技はtimesメソッドで1回が返ってくるので、ループは1度だけ実行され、連続攻撃技は、取得できた回数分実行されるという仕組みです。
連続攻撃技の場合は、最後にヒット回数をメッセージとして表示するので、取得した回数が1回以上であればメッセージを返却しておきましょう。
ダメージ計算補正値の変更
ダメージ計算処理をまとめたattackSuccessメソッドの処理を確認する前に、1点修正をしなければならない箇所があります。それが「補正値」についてです。
今まで、補正値はすべてプロパティ(m)に格納していましたが、これだと連続攻撃の回数分補正値が上書きされてしまいます。
例えば、ダメージ計算前に行なっているタイプ一致の補正値や、急所判定などが上書きされてしまえば回数を重ねる度にダメージは大きくなりますし、乱数補正に関しては回数を重ねる毎にダメージ量が減っていくことになります。
そうならないように、attackSuccess内で生成する補正値は$mというローカル変数を用意してそこに格納していきます。
攻撃用トレイト(/App/Traits/Service/Battle/ServiceBattleTrait.php)
/**
* 攻撃判定成功時の処理
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param object $move
* @return void
*/
private function attackSuccess($atk_pokemon, $def_pokemon, $move)
{
// ローカル変数として補正値を用意
$m = 1;
// 必要ステータスの取得
$stats = $this->getStats($move->getSpecies(), $atk_pokemon, $def_pokemon);
// ダメージ計算
if($move->getSpecies() !== 'status'){
/**
* 物理,特殊技
*/
if(!is_null($move->getPower())){
// 急所判定(固定ダメージ技は判定不要)
$critical = $this->checkCritical($move->getCritical());
if($critical){
// 補正値を乗算
$m *= $critical;
$this->setMessage('急所に当たった!');
}
}
// 乱数補正値の計算
$rand = $this->calRandNum();
if($rand){
// 補正値を乗算
$m *= $rand;
}
// タイプ一致補正の計算
$this->calMatchType($move->getType(), $atk_pokemon->getTypes());
// ダメージ計算
$damage = $this->calDamage(
$atk_pokemon->getLevel(), # 攻撃ポケモンのレベル
$stats['a'], # 攻撃ポケモンの攻撃値
$stats['d'], # 防御ポケモンの防御値
$move->getPower(), # 技の威力
$this->m * $m, # 補正値(プロパティ*ローカル)
);
// やけど補正
if(($move->getSpecies() === 'physical') && ($atk_pokemon->getSa() === 'SaBurn')){
// 物理且つやけど状態ならダメージを半減
$damage *= 0.5;
}
// タイプ相性のメッセージを返却
$this->setMessage($this->type_comp_msg);
}else{
/**
* 変化技
*/
$damage = 0;
}
// ダメージ計算
$def_pokemon->calRemainingHp('sub', $damage);
// 追加効果(相手にHPが残っていれば)
if($def_pokemon->getRemainingHp()){
// 追加効果
$move->effects($atk_pokemon, $def_pokemon);
// 追加効果のメッセージをセット
$this->setMessage($move->getMessages());
$move->resetMessage();
return;
}
}
メソッドの冒頭で$m(初期値1)を用意しておき、もし補正が発生すればそこに乗算、最終的なダメージ計算時にはプロパティのmとローカル変数の$mを乗算した値を引数に指定しています。
これに合わせて、補正値計算のメソッド(急所判定、乱数補正、タイプ一致)の返り値もそれぞれ変更しましょう。
攻撃用トレイト(/App/Traits/Service/Battle/ServiceBattleTrait.php)
/**
* 急所判定
*
* @param object $move
* @return mixed (numeric|boolean)
*/
private function checkCritical(...$rank)
{
switch (array_sum($rank)) {
// 急所ランク+0
case 0:
$chance = 4.17; #(%)
break;
// 急所ランク+1
case 1:
$chance = 12.5; #(%)
break;
// 急所ランク+2
case 2:
$chance = 50; #(%)
break;
// 急所ランク+3以上
default:
$chance = 100; #(%)
break;
}
/**
* 0〜10000からランダムで数値を取得して、それより小さければ急所
* 確率($chance)は*100して整数で比較する
*/
if(($chance * 100) >= (mt_rand(0, 10000))){
// 急所に当たった
return 1.5;
}
// 急所に当たらなかった
return false;
}
/**
* 乱数補正値の計算
*
* @return numeric
*/
private function calRandNum()
{
// 85〜100の乱数をかけ、その後100で割る
return (mt_rand(85, 100) / 100);
}
/**
* タイプ一致補正値の計算(一致→1.5倍)
*
* @param string $move_type 技タイプ
* @param array $pokemon_types 攻撃ポケモンのタイプ
* @return mixed (numeric|boolean)
*/
private function calMatchType($move_type, $pokemon_types)
{
if(in_array($move_type, $pokemon_types, true)){
// 攻撃ポケモンのタイプと技タイプが一致
return 1.5;
}
// タイプ一致ではない
return false;
}
これで補正値が引き継がれてしまうことはありません。それでは実際に技を使って確認してみましょう。
現状、多弾ヒットしているかどうかはダメージ量でしか計算できませんが、しっかりと回数が表示されていることは確認できました。
一撃必殺技とは
ポケモン初代から現在までも引き継がれている歴史的な技の一つに「一撃必殺技」というものがあります。こちらは名前の通り、一撃で相手を倒すことができる技なのですが、一部現状作成した技判定とは異なる部分があります。なので、こちらも別処理を作成しましょう。
一撃必殺技(ポケモンwiki)
命中率は低いが、当たればどんなにステータスに差があったとしてもひんしにさせることができるわざが該当する。単に威力の高いわざで相手を一撃で倒したとしても、そのわざは一撃必殺技とはよばれない。命中すると「いちげき ひっさつ!」(漢字:一撃必殺!)という特殊なメッセージが表示される。
今回の検証では「じわれ」を使います。
じわれ(/Classes/Move/Fissure.php)
<?php
$root_path = __DIR__.'/../..';
require_once($root_path.'/Classes/Move.php');
// じわれ
class Fissure extends Move
{
/**
* 正式名称
* @var string
*/
protected $name = 'じわれ';
/**
* 説明文
* @var string
*/
protected $description = '一撃必殺技';
/**
* タイプ
* @var string
*/
protected $type = 'Ground';
/**
* 分類
* @var string(physical:物理|special:特殊|status:変化)
*/
protected $species = 'physical';
/**
* 威力
* @var integer
*/
protected $power = null;
/**
* 命中率
* @var integer
*/
protected $accuracy = 30;
/**
* 使用回数
* @var integer
*/
protected $pp = 5;
/**
* 優先度
* @var integer
*/
protected $priority = 0;
/**
* 一撃必殺技確認用フラグ
* @var boolean
*/
protected $one_hit_knockout_flg = true;
}
一撃必殺技ではダメージ計算を行わないため、威力にはnullをセットしておきます。更に、一撃必殺技だということがわかるように、one_hit_knockout_flgというプロパティを持たせてtrueをセットしておきましょう。合わせて、親クラスにも初期プロパティをセットしておきましょう。
技クラス(/Classes/Move.php)
<?php
$root_path = __DIR__.'/..';
require_once($root_path.'/App/Traits/InstanceTrait.php');
require_once($root_path.'/App/Traits/ResponseTrait.php');
// 技
abstract class Move
{
--省略
/**
* 一撃必殺確認用フラグ
* @var boolean
*/
protected $one_hit_knockout_flg = false;
命中率の計算
一撃必殺には30%という命中率が設定されていますが、これは一律の数値ではありません。なぜなら、レベル差によってこの確率は変動するからです。
命中率 = 30 + (攻撃側のレベル – 相手側のレベル)
ランク補正、命中率を上下させる効果の影響を受けない。
相手側が攻撃側よりレベルが高い場合は無効。
レベルが高ければ、そのレベルの差分が命中率に上乗せされるという仕様です。なので、通常の命中率取得処理とは別に、一撃必殺用の命中率取得処理を用意しましょう。
技クラス(/Classes/Move.php)
/**
* 一撃必殺の命中率を計算
*
* @param string $pokemon
* @return integer
*/
public function getOneHitKnockoutAccuracy($atk, $def)
{
if($atk->getLevel() > $def->getLevel()){
return $this->accuracy + ($atk->getLevel() - $def->getLevel());
}
return $this->accuracy;
}
もしレベルが相手よりも上回っていれば、差分を加えた値を返却しています。
無効化処理
一撃必殺技は、相手のレベルが上回っていれば無効化されます。その際には「ぜんぜん効いていない」というメッセージが表示されるため、こちらも分岐を分けて作成しなければなりません。攻撃が外れた際のメッセージと合わせて、技クラスに追加しておきます。
技クラス(/Classes/Move.php)
<?php
$root_path = __DIR__.'/..';
require_once($root_path.'/App/Traits/InstanceTrait.php');
require_once($root_path.'/App/Traits/ResponseTrait.php');
// 技
abstract class Move
{
use InstanceTrait;
use ResponseTrait;
/**
* チャージ技確認用フラグ
* @var boolean
*/
protected $charge_flg = false;
/**
* 一撃必殺確認用フラグ
* @var boolean
*/
protected $one_hit_knockout_flg = false;
/**
* 攻撃失敗時のメッセージ
* @var string
*/
protected $failed_msg = 'しかし::pokemonの攻撃は外れた!';
/**
* 一撃必殺失敗時のメッセージ
* @var string
*/
protected $one_hit_knockout_failed_msg = '::pokemonには全然効いていない!';
/**
* インスタンス作成時に実行される処理
*
* @return void
*/
public function __construct()
{
//
}
/**
* チャージ効果
*
* @return void
*/
public function charge($atk)
{
// チャージ不要
return false;
}
/**
* 技回数
*
* @return integer
*/
public function times()
{
// デフォルトは1
return 1;
}
/**
* 追加効果(ダメージ計算後に実行)
*
* @return void
*/
public function effects(...$args)
{
//
}
/**
* 名称の取得
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* 説明文の取得
*
* @return string
*/
public function getDescription()
{
return $this->description;
}
/**
* タイプの取得
*
* @return object
*/
public function getType()
{
return $this->getInstance($this->type);
}
/**
* 分類の取得
*
* @return string
*/
public function getSpecies()
{
return $this->species;
}
/**
* 威力の取得
*
* @return string
*/
public function getPower()
{
return $this->power;
}
/**
* 命中率の取得
*
* @return integer
*/
public function getAccuracy()
{
return $this->accuracy;
}
/**
* 一撃必殺の命中率を計算
*
* @param string $pokemon
* @return integer
*/
public function getOneHitKnockoutAccuracy($atk, $def)
{
if($atk->getLevel() > $def->getLevel()){
return $this->accuracy + ($atk->getLevel() - $def->getLevel());
}
return $this->accuracy;
}
/**
* 使用回数の取得
*
* @param integer $correction 補正値
* @return integer
*/
public function getPp(int $correction=0)
{
return $this->pp + (int)(floor($this->pp / 5) * $correction);
}
/**
* 優先度の取得
*
* @return integer
*/
public function getPriority()
{
return $this->priority;
}
/**
* 急所ランクの取得
*
* @return integer
*/
public function getCritical()
{
return $this->critical ?? 0;
}
/**
* チャージフラグの取得
*
* @return boolean
*/
public function getChargeFlg()
{
return $this->charge_flg;
}
/**
* 一撃必殺フラグの取得
*
* @return boolean
*/
public function getOneHitKnockoutFlg()
{
return $this->one_hit_knockout_flg;
}
/**
* 攻撃失敗時のメッセージを取得
*
* @param string $pokemon
* @return string
*/
public function getFailedMessage($pokemon)
{
return str_replace('::pokemon', $pokemon, $this->failed_msg);
}
/**
* 一撃必殺失敗時(命中率が0)のメッセージを取得
*
* @param string $pokemon
* @return string
*/
public function getOneHitKnockoutFailedMessage($pokemon)
{
return str_replace('::pokemon', $pokemon, $this->one_hit_knockout_failed_msg);
}
}
こちらが技クラスの最終コードです。状態異常などと同様に、メッセージを取得するためのメソッドを追加しました。
次に、命中判定処理を修正しましょう。
攻撃用トレイト(/Traits/Service/Battle/AttackTrait.php)
/**
* 命中判定
*
* @param object $atk
* @param object $def
* @param object $move
* @return boolean
*/
private function checkHit($atk, $def, $move)
{
// 一撃必殺技のチェック
if($move->getOneHitKnockoutFlg()){
if($atk->getLevel() < $def->getLevel()){
// 相手の方がレベルが高ければ無効
$this->setMessage($move->getOneHitKnockoutFailedMessage($def->getPrefixName()));
return false;
}
// レベル差計算を含めた命中率を取得
$accuracy = $move->getOneHitKnockoutAccuracy($atk, $def);
}else{
// 命中率取得
$accuracy = $move->getAccuracy();
}
// nullの場合は命中率関係無し
if(is_null($accuracy)){
return true;
}
/**
* 0〜100からランダムで数値を取得して、それより小さければ命中
* 例:命中80%→mt_randで60が生成されたら成功、90なら失敗
*/
if($accuracy >= mt_rand(0, 100)){
// 攻撃成功
return true;
}
// 攻撃失敗
$this->setMessage($move->getFailedMessage($atk->getPrefixName()));
return false;
}
最初に一撃必殺フラグを使って分岐を行い、もし一撃必殺であればレベル差を確認、一撃必殺用の命中率を取得するという処理を追加しました。その後、通常の命中判定に入っています。
checkHitメソッドの引数が変更になっているので、呼び出し箇所(attackメソッド内)も合わせて修正しておきましょう。
// 命中判定
if(!$this->checkHit($atk_pokemon, $def_pokemon, $move)){
// 攻撃失敗
return;
}
ダメージ計算
最後に一撃必殺技のダメージ判定です。命中判定が終わればダメージ計算に入りますが、一撃必殺ではそれは不要です。なぜなら、どんな相手であっても技が当たれば一撃で倒すことができるからです。
※持ち物や特性などによる判定は除く
なので、ダメージ計算前に再度一撃必殺技かどうかを判定する分岐を追加しましょう。
攻撃用トレイト:attackメソッド内(/Traits/Service/Battle/AttackTrait.php)
// 一撃必殺
if($move->getOneHitKnockoutFlg()){
$def_pokemon->calRemainingHp('death');
$this->setMessage('一撃必殺');
return;
}
// 技を回数分実行
$times = $move->times();
for($i = 0; $i < $times; $i++){
// 攻撃判定成功時の処理
$this->attackSuccess($atk_pokemon, $def_pokemon, $move);
}
// 連続技はヒット回数のメッセージを返却
if($times > 1){
return $this->setMessage($times.'回当たった');
}
calRemainingHpメソッドに用意しているdeathを使って相手のHPを0にしています。それでは一撃必殺技を使って確認してみましょう。
自分のレベル以下の相手
自分のレベル超過の相手
しっかり判定通りの結果が返ってきましたね。ちなみにライチュウ戦のデバッガーのレベルがリセットされている理由は、無策でライチュウ相手に挑んだところ、でんきショックで葬られたからです。
まとめ
いかがだったでしょうか。
今回のPHPポケモンでは「連続攻撃技」と「一撃必殺技」を実装しました。
初代だけでもかなりのバリエーションを備えた技があり、そのすべての仕様を作るとなればかなりの作業量になりそうです。
ゲームづくりに興味がある人や、ポケモンが好きな人は、ぜひ参考にしてみてくださいね。