ランク補正とは
ポケモンでは通常ステータスとは違い、バトル中にのみ変化するステータスが存在します。初代仕様であれば、「なきごえ」や「かたくなる」といった変化技により「こうげき」や「ぼうぎょ」の強化や弱体化をさせるものです。これはバトル中であれば引き継がれるものですが、交代やバトルが終了すればリセットされることになるため、今までに実装した努力値や個体値などとは少し異なった処理が必要になります。
ポケモンwiki ランク補正
バトルステータスの導入
それでは、ランク補正(バトルステータス)を実装していきます。まず、ステータスとして用意するのは以下の8項目です。
- 攻撃(Attack)
- 防御(Defense)
- 特攻(SpAtk)
- 特防(SpDef)
- 素早さ(Speed)
- 急所(Critical)
- 命中(Accuracy)
- 回避(Evasion)
このすべてが同じようなランク補正がされるわけでは有りませんが、一旦は共通項目として実装していきます。
ランクには段階があります。バトル開始時(初期状態)はすべてが0で始まり、最大+6、最低―6の13段階に分かれています。
ではまず、ポケモンクラスにランクのプロパティを設定しましょう。
ポケモンクラス(/Classes/Pokemon.php)
/**
* ランク(バトルステータス)
* @var array(min:-6, max:6)
*/
protected $rank = [
'Attack' => 0,
'Defense' => 0,
'SpAtk' => 0,
'SpDef' => 0,
'Speed' => 0,
'Critical' => 0,
'Accuracy' => 0,
'Evasion' => 0,
];
プロパティに合わせて、ランクの取得メソッドをGet用トレイトに作成しましょう。
Get格納トレイト(Traits/Pokemon/GetTrait.php)
/**
* ランク(バトルステータス)の取得
*
* @param string|null
* @return array|integer
*/
public function getRank($param=null)
{
if(is_null($param)){
return $this->rank;
}else{
return $this->rank[$param];
}
}
もし引数が設定されていなければすべてのランク、設定されていれば該当するランクを取得するというメソッドです。
加算処理
まずはランクの加算処理から作成していきましょう。他のプロパティでは数値をセットしていましたが、今回は加算または減算で操作をしていきます。
それでは、ポケモンクラスに加算メソッドを追加しましょう。
ポケモンクラス(/Classes/Pokemon.php)
/**
* ランクの加算
*
* @param string $param
* @param integer $val (min:1, max:12)
* @return string
*/
public function addRank($param, $val)
{
// 変化ランクに合わせたメッセージ
$msg = [
1 => '上がった',
2 => 'ぐーんと上がった',
3 => 'ぐぐーんと上がった',
12 => '最大まで上がった',
];
// 既にランクが最大であればfalseを返却
if($this->rank[$param] === 6){
return $this->name.'の'.transJp($param).'はもう上がらない';
}
// 加算処理
$this->rank[$param] += $val;
// 最大値は6
if($this->rank[$param] > 6){
$this->rank[$param] = 6;
}
return $this->name.'の'.transJp($param).'が'.$msg[$val] ?? $msg[3];
}
まずは引数からです。ランクは複数に対して影響を与えるものもありますが、基本的には1項目ずつの処理になります。なので、第1引数($param)でキーを指定します。第2引数では加算する値を受け取っています。上昇の最小値は1で、最大値は12です。
ここで上昇値について細かく見ていきましょう。
// 変化ランクに合わせたメッセージ
$msg = [
1 => '上がった',
2 => 'ぐーんと上がった',
3 => 'ぐぐーんと上がった',
12 => '最大まで上がった',
];
1〜12の幅がありますが、基本的には上記の4段階が使用されます。そして、上昇値に合わせて出力するメッセージも異なるため、配列として用意します。
例えば「こうげき」が1段階上がれば、「こうげきが上がった」というメッセージが表示されます。上がる段階に合わせて、メッセージが異なり、そのメッセージの違いでどのぐらい能力に変化があるのかを知ることができるのです。
最大ランクは+6ですが、12という数字が用意されている理由は「最大まで上げる」という技があるからです。第1世代では存在しませんが、―6で合ったとしても+6にするという効果を持っているため、その補正用に12という数字が用意されています。
次は、上限に達した際の処理についてです。
// 既にランクが最大であればfalseを返却
if($this->rank[$param] === 6){
return $this->name.'の'.transJp($param).'はもう上がらない';
}
こうげき+7という状態は発生しません。そうならないためにも、現在の数値が6であれば、もう上がらないというメッセージを返却しています。
これを「6以上であれば」としていないのには理由があります。次の処理を見てみましょう。
// 加算処理
$this->rank[$param] += $val;
// 最大値は6
if($this->rank[$param] > 6){
$this->rank[$param] = 6;
}
加算処理を行った後に、最大値のチェックをしています。これにより6超過の数値が入らないようにしています。もし前処理の「現在のランクが6であれば」が「6以上であれば」になっていると、もし何かの手違いで6以上の数値が入っていた場合、6に戻すといった処理にたどり着けず不正な値が残り続けてしまうため、一致による判定を採用しました。
加算処理がされれば、最後にメッセージを返却します。
return $this->name.'の'.transJp($param).'が'.$msg[$val] ?? $msg[3];
ポケモン名+パラメーター名+上昇メッセージという構成です。パラメーター名は英語なので、以前Resources/Langに作成した翻訳関数を使用しています。
減算処理
次に減算処理を作成します。こちらは加算処理と構成はほとんど同じです。
ポケモンクラス(/Classes/Pokemon.php)
/**
* ランクの減算
*
* @param string $param
* @param integer $val (min:1, max:3)
* @return string
*/
public function subRank($param, $val)
{
// 変化ランクに合わせたメッセージ
$msg = [
1 => '下がった',
2 => 'がくっと下がった',
3 => 'がくーんと下がった',
];
// 既にランクが最低であればfalseを返却
if($this->rank[$param] === -6){
return $this->name.'の'.transJp($param).'はもう下がらない';
}
// 減算処理
$this->rank[$param] -= $val;
// 最低値は-6
if($this->rank[$param] < -6){
$this->rank[$param] = -6;
}
return $this->name.'の'.transJp($param).'が'.$msg[$val] ?? $msg[3];
}
メッセージの違いと、減算処理、数値の違いだけです。攻撃と違って12段階の変化技は用意されていないため、減算では1〜3の3段階だけが用意されています。
技の追加効果
ランク補正に必要なメソッドとパラメーターが最低限揃ったので、技の追加効果を実装していきます。こちらは、技に対してそれぞれ用意します。
まずは、技クラスに空のメソッドとレスポンストレイトを追加しましょう。
技クラス(/Classes/Move.php)
<?php
require_once(__DIR__.'/../Traits/InstanceTrait.php');
require_once(__DIR__.'/../Traits/ResponseTrait.php');
// 技
abstract class Move
{
use InstanceTrait;
use ResponseTrait;
/**
* インスタンス作成時に実行される処理
*
* @return void
*/
public function __construct()
{
//
}
/**
* 追加効果
*
* @return void
*/
public function effects(...$args)
{
//
}
レスポンストレイトは、減算・加算処理で返ってきたメッセージを格納するために使用します。
追加効果として用意したメソッド(effects)は、追加効果を実行する際に呼び出すメソッドです。親クラスに空を用意したのは、すべての技に追加効果は存在しないためです。追加効果があるかどうかを判定するよりも、空メソッドを呼び出すほうが実装が簡単だからです。
コンストラクタとおナジック、もし子に親と同じメソッドが存在していた場合は、子のメソッドが優先されます。こうしておくことで、追加効果が存在する技にだけeffectsというメソッドを用意するだけで済みます。
引数に使用している可変長引数は、子メソッドと引数を統一するために使用しています。もし親と子に同名メソッドが存在する場合は、その引数が一致していなければならないからです。可変長引数を使用した場合は、引数が配列となって格納されるため、引数の数は自由に設定できます。現状は攻撃ポケモンと防御ポケモンの2つまでしか想定していませんが、今後増えた時に対応がしやすいという観点から可変長引数を採用しました。
それでは、各技の追加効果メソッドを作成していきましょう。まずはサンプルとして「なきごえ」にeffectsを追加します。
なきごえ(/Classes/Move/Growl.php)
/**
* 追加効果
*
* @param array $args
* @return void
*/
public function effects(...$args)
{
/**
* @param Pokemon $atk 攻撃ポケモン
* @param Pokemon $def 防護ポケモン
*/
list($atk, $def) = $args;
// 攻撃ランクを1段階下げる
$result = $def->subRank('Attack', 1);
// メッセージをセット
$this->setMessage($result);
}
「なきごえ」の追加効果は、相手の「こうげき」を1段階下げるという変化技です。引数として攻撃ポケモンと防御ポケモンを可変長引数で受け取り、listでそれぞれ$atkと$defに分けます。
あとは、先程作成したsubRankのメソッドを使用してステータスを対象のステータスを変更して、返り値として受け取ったメッセージをセットするだけです。
PHPではオブジェクトを引数で渡しても、基本的には参照渡しとなります。今回はそれを利用して、引数で受け取ったポケモンのオブジェクトに対してメソッドを実行しているので、本来のオブジェクトも変更されていることになります。
オブジェクトと参照(PHP.net)
それでは、攻撃用トレイトに追加効果の実行処理を追加しましょう。
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
<?php
trait AttackTrait
{
/**
* 省略記号
*
* @var L レベル
* @var A 攻撃値
* @var D 防御値
* @var P 威力
* @var M 補正値
*/
/**
* 補正値
* @var float 小数点第2位までの数値
*/
private $m = 1;
/**
* 攻撃する
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param string $move_class
* @return void
*/
protected function attack($atk_pokemon, $def_pokemon, $move_class)
{
// 技のインスタンスを取得
$move = $this->getInstance($move_class);
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getName().'は'.$move->getName().'を使った!');
// タイプ相性チェック
$type_comp_msg = $this->checkTypeCompatibility($move->getType(), $def_pokemon->getTypes());
// 「こうかがない」の判定(命中率と威力がnullではなく、タイプ相性補正が0の場合)
if(!is_null($move->getAccuracy()) && !is_null($move->getPower()) && ($this->m === 0)){
// こうかがない
$this->setMessage($def_pokemon->getName().'には効果が無いみたいだ');
return;
}
// 命中判定
$hit = $this->checkHit($move->getAccuracy());
if(!$hit){
// 攻撃失敗
$this->setMessage('しかし'.$atk_pokemon->getName().'の攻撃は外れた!');
return;
}
// 必要ステータスの取得
$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){
$this->setMessage('急所に当たった!');
}
}
// 乱数補正値の計算
$this->calRandNum();
// タイプ一致補正の計算
$this->calMatchType($move->getType(), $atk_pokemon->getTypes());
// ダメージ計算
$damage = $this->calDamage(
$atk_pokemon->getLevel(), # 攻撃ポケモンのレベル
$stats['a'], # 攻撃ポケモンの攻撃値
$stats['d'], # 防御ポケモンの防御値
$move->getPower(), # 技の威力
$this->m, # 補正値
);
// タイプ相性のメッセージを返却
$this->setMessage($type_comp_msg);
}else{
/**
* 変化技
*/
$damage = 0;
}
// 追加効果
$move->effects($atk_pokemon, $def_pokemon);
// 追加効果のメッセージをセット
$this->setMessage($move->getMessages());
// ダメージの格納
$this->setResponse($damage, 'damage');
}
追加効果は基本的に最後に発生します。なので、技種類の判別が終わった後に実行して、メッセージを格納しています。
変更に合わせてコントローラーを修正しましょう。
バトル用コントローラー(/Classes/Controller.php)
<?php
require_once(__DIR__.'/../Controller.php');
require_once(__DIR__.'/../../Traits/Battle/AttackTrait.php');
// バトル用コントローラー
class BattleController extends Controller
{
use AttackTrait;
/**
* 敵ポケモン格納用
* @var object
*/
protected $enemy;
/**
* @return void
*/
public function __construct()
{
// 親コンストラクタの呼び出し
parent::__construct();
// 自ポケモンの格納
$this->myPokemon($_SESSION['pokemon']);
// 敵ポケモンの格納
if(isset($_SESSION['enemy'])){
$this->enemyPokemon($_SESSION['enemy']);
}else{
$this->enemyPokemon();
}
// ランク(バトルステータス)の引き継ぎ
if(isset($_SESSION['rank'])){
$this->pokemon
->setRank($_SESSION['rank']['pokemon']);
$this->enemy
->setRank($_SESSION['rank']['enemy']);
}
// アクションが選択された
if(isset($_POST['action'])){
// アクションメソッドの実行
$this->action(htmlspecialchars($_POST['action']), htmlspecialchars($_POST['param'] ?? null));
return;
}
}
/**
* 自ポケモンの格納
*
* @param array $pokemon
* @return void
*/
private function myPokemon($pokemon)
{
$this->pokemon = new $pokemon['class_name']($pokemon);
}
/**
* 敵ポケモンの格納
*
* @param array|null $pokemon
* @return void
*/
private function enemyPokemon($pokemon=null)
{
if(is_null($pokemon)){
$this->enemy = new Fushigidane();
$this->setMessage('野生の'.$this->enemy->getName().'が現れた!');
}else{
$this->enemy = new $pokemon['class_name']($pokemon);
}
}
/**
* アクション
*
* @param string $action
* @param mixed $param
* @return void
*/
private function action($action, $param)
{
switch ($action) {
// にげる
case 'run':
unset($_SESSION['enemy']);
unset($_SESSION['rank']);
header("Location: ./home.php", true, 307) ;
exit;
// たたかう
case 'fight':
$this->attack($this->pokemon, $this->enemy, $param);
$this->setResponse($this->pokemon->getRank(), $this->pokemon->getName());
$this->setResponse($this->enemy->getRank(), $this->enemy->getName());
break;
}
}
/**
* 敵ポケモン情報の取得
*
* @return object
*/
public function getEnemy()
{
return $this->enemy;
}
}
まずはコンストラクタ内から見ていきましょう。
// ランク(バトルステータス)の引き継ぎ
if(isset($_SESSION['rank'])){
$this->pokemon
->setRank($_SESSION['rank']['pokemon']);
$this->enemy
->setRank($_SESSION['rank']['enemy']);
}
ランクも引き継がなければなりませんが、個体値やレベルなどと違ってバトル以外では不要になるため、通常の引き継ぎ処理とは分けて作成しました。セッションにrankというキーで自ポケモンと相手ポケモンの2体分を格納しています。こちらの引き継ぎ処理は後ほど説明します。
次にアクションメソッド内を見てみましょう。
switch ($action) {
// にげる
case 'run':
unset($_SESSION['enemy']);
unset($_SESSION['rank']);
header("Location: ./home.php", true, 307) ;
exit;
// たたかう
case 'fight':
$this->attack($this->pokemon, $this->enemy, $param);
$this->setResponse($this->pokemon->getRank(), $this->pokemon->getName());
$this->setResponse($this->enemy->getRank(), $this->enemy->getName());
break;
}
「にげる」が選択された場合はランクを引き継ぐ必要がなくなるため、セッションに格納されたランクを敵ポケモンと同様にunsetで削除しています。
確認用として、レスポンスに自ポケモンと相手ポケモンのランク一覧を格納しました。
ランクの引き継ぎ
ランクを変化させる処理は整いましたが、バトル中にそのステータスを引き継がなければなりません。なので、他と同様にrank用のセッションを使用します。
バトル画面用ファイル(/battle.php)
<section>
<div class="row">
<div class="col-12 col-sm-6 mb-5">
<?php $_SESSION['pokemon'] = $pokemon->export(); # 自ポケモンの情報をセッションに格納 ?>
<?php $_SESSION['enemy'] = $enemy->export(); # 敵ポケモンの情報をセッションに格納 ?>
<?php
$_SESSION['rank'] = [
'pokemon' => $pokemon->getRank(),
'enemy' => $enemy->getRank(),
];
?>
<?php include('Resources/Partials/Battle/Forms/move.php'); # たたかう ?>
<form action="" method="post">
<div class="input-group mb-3">
<input type="hidden" name="action" value="run">
<input class="btn btn-danger" type="submit" value="逃げる">
</div>
</form>
</div>
<div class="col-12 col-sm-6 mb-5">
<div class="result-box border p-3 mb-3">
<?php foreach($controller->getMessages() as list($msg, $status)): ?>
<?php if($status == 'error') $class = 'text-danger'; ?>
<?php if($status == 'success') $class = 'text-success'; ?>
<p class="<?=$class ?? ''?>"><?=$msg?></p>
<?php endforeach; ?>
</div>
</div>
</div>
</section>
<section>
<div class="row">
<div class="col-12">
<pre><?php var_export($controller->getResponses()); ?></pre>
</div>
</div>
</section>
pokemonとenemy同様に、rankの下にそれぞれキーをもたせて配列としてランクを格納しています。これで、セッション内に現在のランクが入りました。
先程レスポンスに格納したデータは、一番下に出力されるようにpreタグでvar_exportを表示させています。
それでは、先程追記したバトル用コントローラー内の処理に戻ります。
バトル用コントローラー(/Classes/Controller/BattleController.php)
// ランク(バトルステータス)の引き継ぎ
if(isset($_SESSION['rank'])){
$this->pokemon
->setRank($_SESSION['rank']['pokemon']);
$this->enemy
->setRank($_SESSION['rank']['enemy']);
}
$_SESSION[‘rank’]が存在していれば、それぞれにsetRankでランクを格納しています。setRankのメソッドはポケモンのセット格納用トレイトに用意しておきます。
Set格納用トレイト(/Traits/Pokemon/SetTrait.php)
/**
* ランク(バトルステータス)をセットする
* @param array|string $rank
* @return void
*/
public function setRank($rank)
{
// 初期化
if($rank === 'reset'){
$this->rank = [
'Attack' => 0,
'Defense' => 0,
'SpAtk' => 0,
'SpDef' => 0,
'Speed' => 0,
'Critical' => 0,
'Accuracy' => 0,
'Evasion' => 0,
];
return;
}
// ランクをセット
if(is_array($rank)){
$this->rank = $rank;
}
}
他のセット用メソッドとほとんど変わりませんが、引数に合わせてresetという処理を追加しています。技によってはresetが行われることが合ったり、入れ替えをした際にresetをする必要が出てくるためです。配列で初期値をセットする単純な方法をとっていますが、array_mapなどを使用して配列に対して0をセットしても構いません。
それでは出力結果を見てみましょう。
ランクの変更が確認できました。最低値に合わせたメッセージの出力も問題ありません。
現段階ではランク補正が補正値の計算に含まれていないので、ダメージ量などに変更はありませんが、値が格納できたことでその実装は今までの補正値同様に計算式を当てはめるだけになりました。
まとめ
いかがだったでしょうか。
今回のPHPポケモンは「ランク補正」についてご紹介しました。これにより、変化技や攻撃技の補助効果などが一部実装できるようになり、バトルの楽しみもより増えてきましたのではないでしょうか。
現在プログラミング学習に取り組んでいる方は、ぜひ参考にしてくださいね。