努力値の実装
今回はポケモンのやりこみ要素の一つ、努力値システムを導入します。既に努力値の項目は「ピカチュウで学ぶオブジェクト指向」の段階で実装し、ステータス計算にも判定済みですが、肝心な「努力値を獲得する仕組み」自体は出来ていませんでした。なので、バトルシステムも終盤となったこのタイミングで実装していきましょう。
努力値とは
そもそも努力値とはどういったものなのか見ていきましょう。
努力値(どりょくち、英:Effort values)とは、ポケモンの強さにかかわる数値のひとつ。ステータス画面では確認できない隠しパラメータの一つである。
なお、努力値という言葉は俗称であり、ゲーム内では基礎ポイントと呼ばれている。
ポケモンは戦闘の後に、経験値を得ると同時に、これとは別の「隠し経験値」のようなものも獲得するようになっており、この数値もステータス上昇に影響する。これが通称「努力値」、もしくは「基礎ポイント」である。
努力値(ポケモンwiki)
ということです。簡単に説明すると、バトルに勝利することで経験値の他にもらえる隠しポイントといったところです。これはステータスの数値に影響するため、効率良い獲得をすることで他のポケモンとは大きく差別化できる強いポケモンに育て上げることが可能になります。
PHPポケモンで努力値システムを導入するに当たっておさえて置きたいのは以下の3項目です。
- ポケモンを倒すと、そのポケモンが持つ獲得努力値を得られる
- 努力値は全ステータス合計最大510
- 努力値は1ステータス最大252
それでは、上記3点に注意しながら実装していきましょう。
獲得努力値
まずは獲得努力値の実装です。本来アイテムを使って努力値を得ることも可能ですが、現在はアイテムの概念が無いため戦闘による獲得のみを実装します。
ポケモンを倒すことで、そのポケモンが保有している「獲得努力値」を得ることができます。例えばPHPでサンドバック野生ポケモンとして出てくれているフシギダネであれば、「とくこう+1」という獲得努力値を持っています。こちらも最新世代を参考にしながらポケモンへ割り当てていきます。
獲得努力値一覧(ポケモンwiki )
それではフシギダネに獲得努力値のプロパティ($reward_ev)をセットしましょう。
フシギダネ(/Classes/Pokemon/Fushigidane.php)
/**
* 獲得努力値
* @var array
*/
protected $reward_ev = [
'SpAtk' => 1,
];
参考ページを見て気づいた方もいるかも知れませんが、獲得努力値は1項目だけではありません。中には「こうげき」と「とくこう」など複数の項目に対して努力値を与えてくれるポケモンもいます。なので、あくまで複数を想定しておきましょう。
次に、獲得努力値の取得処理を作成しましょう。
Get格納トレイト(Traits/Pokemon/GetTrait.php)
/**
* 獲得努力値を取得する
* @return array
*/
public function getEv()
{
return $this->ev;
}
/**
* 獲得努力値を取得する
* @return array
*/
public function getRewardEv()
{
return $this->reward_ev;
}
後ほど努力値が正常に取得できているかを確認するために、getEvというメソッドも合わせて作成しています。
これで獲得努力値の準備は完了です。
努力値の付与
次に努力値の付与機能を作成しましょう。まずは努力値を割り当てるためのメソッド(setEv)を作成します。
Set格納トレイト(/Traits/Pokemon/SetTrait.php)
/**
* 努力値をセット(取得)する
* @param array $reward_ev
* @return void
*/
public function setEv($reward_ev)
{
// 最大努力値合計は510
if(array_sum($this->ev) >= 510){
return;
}
// 努力値を加算
foreach($reward_ev as $key => $val){
$this->ev[$key] += $val;
// 各ステータスの最大は252
if($this->ev[$key] > 252){
$this->ev[$key] = 252;
}
// 最大努力値を超過させないための処理
if(array_sum($this->ev) > 510){
// 510超過分をセットした努力値から減算
$this->ev[$key] -= array_sum($this->ev) - 510;
break;
}
}
}
引数には獲得努力値(配列)を受け取っています。
まず最初に、現在の努力値合計がどれぐらいあるかをチェックします。前項で説明した通り、合計最大努力値は510と決まっています。上限を超えてしまわないよう、まずは現在ポケモンが保有している努力値の合計をarray_sumを使って算出します。
array_sum(PHP.net)
もし510以上だった場合はそこで処理終了です。
合計最大値をオーバーしていなければ、獲得経験値をforeachにかけて順番に処理をします。
次に各ステータスの上限値を制限します。
// 各ステータスの最大は252
if($this->ev[$key] > 252){
$this->ev[$key] = 252;
}
1ステータスの最大値は252です。なので、加算結果が252を超過しているようであれば、252を再セットすることで対応します。
これで処理自体は問題ないように思えますが、努力値を複数もつポケモンがいるということを忘れてはいけません。例えば「バタフリー」を倒したケースで考えてみましょう。
バタフリーの獲得努力値は「とくこう+2」「とくぼう+1」です。もし合計最大値のチェック、個別最大値のチェックだけであれば、とくこう努力値を2取得した段階で合計努力値が510になっても、とくぼう努力値が252未満であればforeachの処理で+1されてしまい、合計努力値が511になるのです。ステータス上では端数となる1という努力値ですが、もし獲得努力値が2で、その結果が「252、252、6+2(超過分)」のような割り振りになれば、本来割り振れないステータスが1存在してしまうことになります。
このような例外による端数を規制するためにも、個別ステータスの算出後に再度合計値のチェックを行い、510を超過していた場合は超過分を加算したステータスから減算します。
// 最大努力値を超過させないための処理
if(array_sum($this->ev) > 510){
// 510超過分をセットした努力値から減算
$this->ev[$key] -= array_sum($this->ev) - 510;
break;
}
もし最大努力値数のチェックを2度実行したくない人は、最初の合計最大値のチェックを削除してもらって構いません。個人的に努力値がオーバーしている状態でforeachの処理に入らせたくなかったため、前処理で弾くようにしています。
これで努力値の加算処理は実装完了です。
バトル結果判定の修正
作成した努力値の取得メソッドは経験値取得のタイミングで行います。ですが、前回までのバトル終了判定だと「状態異常または状態変化による戦闘不能」が起こった際にバトル終了判定が行われないという不具合が発覚したため、合わせて修正します。
バトルコントローラー(/Classes/Controller/BattleController.php)
/**
* ひんし状態の格納
* @var array
*/
private $fainting = [
'friend' => false,
'enemy' => false,
];
--省略
/**
* アクション
*
* @param string $action
* @param mixed $param
* @return void
*/
private function action($action, $param)
{
// 敵ポケモンの技をインスタンス化
$e_move = $this->getInstance($this->aiSelectMove());
switch ($action) {
/**
* にげる
*/
case 'run':
$this->run++;
if($this->checkRun()){
$this->endBattle();
}
$this->setMessage('逃げられない!');
// 敵ポケモンの攻撃
$p_damage = $this->attack($this->enemy, $this->pokemon, $e_move);
break;
/**
* たたかう
*/
case 'fight':
// 自ポケモンの技をインスタンス化
$p_move = $this->getInstance($param);
// 行動順の判定
$order_array = $this->orderMove(
[$this->pokemon, $this->enemy, $p_move],
[$this->enemy, $this->pokemon, $e_move],
);
// 行動順にforeachでattackメソッドを実行
foreach($order_array as list($atk, $def, $move)){
$this->attack($atk, $def, $move);
// ひんしチェック
if($this->setToCheckFainting($def, $atk)){
// ひんしポケモン有り
break 2;
}
}
// 行動順にforeachでcheckAfterSaとcheckAfterScを実行
foreach($order_array as list($atk, $def, $move)){
$this->checkAfterSa($atk);
$this->checkAfterSc($atk, $def);
// ひんしチェック
if($this->setToCheckFainting($def, $atk)){
// ひんしポケモン有り
break 2;
}
}
$this->setMessage('行動を選択してください');
break;
}
// ひんしポケモンがでた場合の処理
if($this->fainting['enemy'] || $this->fainting['friend']){
$this->judgment();
}
}
--省略
/**
* ひんし状態の格納
*
* @return boolean (true:ひんしポケモン有り, false:ひんしポケモン無し)
*/
private function setToCheckFainting($def, $atk)
{
// 防御側のひんし状態を格納
$this->fainting[$def->getPosition()] = $this->checkFainting($def);
// 攻撃側のひんし状態を格納
$this->fainting[$atk->getPosition()] = $this->checkFainting($atk);
// 返り値判定
if($this->fainting['enemy'] || $this->fainting['friend']){
return true;
}
return false;
}
/**
* バトル結果判定
*
* @return void
*/
private function judgment()
{
if($this->fainting['friend']){
// 味方がひんし状態になった
$this->setMessage('目の前が真っ暗になった');
}else{
// 相手がひんし状態になった(味方はひんし状態ではない)
// 経験値の計算
$exp = $this->calExp($this->pokemon, $this->enemy);
// 経験値をポケモンにセット(返り値をpokemonに格納)
$this->pokemon = $this->pokemon
->setExp($exp);
// 努力値を獲得
$this->pokemon
->setEv($this->enemy->getRewardEv());
// ポケモンに溜まったメッセージを取得
$this->setMessage($this->pokemon->getMessages());
}
// バトル終了判定用メッセージの格納
$this->setMessage(' ', 'battle-end');
}
アクションメソッドを修正、ひんし状態をプロパティに格納してチェックまでを行うメソッド(setToCheckFainting)とバトル結果判定(judgment)の2つのメソッドを作成しました。
今までは攻撃メソッド(attack)内でひんし判定を行い、返り値としてその結果をactionメソッドに伝えていましたが、ひんし判定を新しく作成したsetToCheckFaintingで行なっています。行動後の状態異常・変化判定でのひんし判定も無くし、返り値は無しに変更しました。
※attackメソッド内のひんし判定を削除した関係上、追加効果の判定は残HPで行なっています
攻撃用トレイト(/Traits/Battle/AttackTrait.php)
/**
* 攻撃
* (攻撃→ダメージ計算→ひんし判定)
*
* @param object $atk_pokemon
* @param object $def_pokemon
* @param object $move
* @return void
*/
protected function attack($atk_pokemon, $def_pokemon, $move)
{
// 行動チェック(状態異常・状態変化)
if(!$this->checkBeforeSa($atk_pokemon) || !$this->checkBeforeSc($atk_pokemon)){
// 行動失敗
return;
}
// 攻撃メッセージを格納
$this->setMessage($atk_pokemon->getPrefixName().'は'.$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->getPrefixName().'には効果が無いみたいだ');
return;
}
// 命中判定
$hit = $this->checkHit($move->getAccuracy());
if(!$hit){
// 攻撃失敗
$this->setMessage('しかし攻撃は外れた!');
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, # 補正値
);
// やけど補正
if(($move->getSpecies() === 'physical') && ($atk_pokemon->getSa() === 'SaBurn')){
// 物理且つやけど状態ならダメージを半減
$damage *= 0.5;
}
// タイプ相性のメッセージを返却
$this->setMessage($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());
return;
}
}
チェック格納トレイト(/Traits/Battle/CheckTrait.php)
/**
* アタック後の状態異常チェック
*
* @param object Pokemon
* @return void
*/
protected function checkAfterSa($pokemon)
{
if(empty($pokemon->getSa())){
// 状態異常にかかっていない
return;
}
switch ($pokemon->getSa()) {
/**
* どく
*/
case 'SaPoison':
// 最大HPの1/8ダメージを受ける
$poison = new SaPoison;
// 小数点以下切り捨て
$damage = (int)($pokemon->getStats('HP') / 8);
if($damage){
// 最小ダメージ数は1
$damage = 1;
}
// メッセージ
$this->setMessage($poison->getTurnMessage($pokemon->getPrefixName()));
break;
/**
* もうどく
*/
case 'SaBadPoison':
// 最大HPの(ターン数/16)ダメージを受ける(最大15/16)
$bad_poison = new SaBadPoison;
// ターンカウントを進める
$pokemon->goSaTurn();
// 小数点以下切り捨て
$damage = (int)($pokemon->getStats('HP') / 16) * $pokemon->getSa('turn');
if($damage){
// 最小ダメージ数は1
$damage = 1;
}
// メッセージ
$this->setMessage($bad_poison->getTurnMessage($pokemon->getPrefixName()));
break;
/**
* やけど
*/
case 'SaBurn':
// 最大HPの1/16ダメージを受ける
$burn = new SaBurn;
// 小数点以下切り捨て
$damage = (int)($pokemon->getStats('HP') / 16);
if($damage){
// 最小ダメージ数は1
$damage = 1;
}
// メッセージ
$this->setMessage($burn->getTurnMessage($pokemon->getPrefixName()));
break;
}
// ダメージ計算
$pokemon->calRemainingHp('sub', $damage ?? 0);
}
/**
* アタック後の状態変化チェック
*
* @param object Pokemon
* @return void
*/
protected function checkAfterSc($sicked_pokemon, $enemy_pokemon)
{
// ひるみ解除
$sicked_pokemon->releaseSc('ScFlinch');
// 状態変化を取得
$sc = $sicked_pokemon->getSc();
if(empty($sc)){
// 状態異常にかかっていない
return;
}
/**
* やどりぎのタネ
*/
if(isset($sc['ScLeechSeed'])){
// 最大HPの1/8HPを吸収する
$leech_seed = new ScLeechSeed;
// 小数点以下切り捨て
$damage = (int)($sicked_pokemon->getStats('HP') / 8);
if($damage){
// 最小ダメージ数は1
$damage = 1;
}
// ダメージ計算
$sicked_pokemon->calRemainingHp('sub', $damage);
// 回復
$enemy_pokemon->calRemainingHp('add', $damage);
// メッセージ
$this->setMessage($leech_seed->getTurnMessage($sicked_pokemon->getPrefixName()));
// HPが0になっていればチェック終了
if(!$sicked_pokemon->getRemainingHp()){
return;
}
}
/**
* バインド
*/
if(isset($sc['ScBind'])){
// 最大HPの1/8ダメージを受ける
$bind = new ScBind;
// バインドのターンカウントを進める
$sicked_pokemon->goScTurn('ScBind');
if($sc['ScBind']['turn'] <= 0){
// バインド解除
$sicked_pokemon->releaseSc('ScBind');
$this->setMessage($bind->getRecoveryMessage($sicked_pokemon->getPrefixName(), $sc['ScBind']['param']));
}else{
// 小数点以下切り捨て
$damage = (int)($sicked_pokemon->getStats('HP') / 8);
if($damage){
// 最小ダメージ数は1
$damage = 1;
}
// ダメージ計算
$sicked_pokemon->calRemainingHp('sub', $damage);
// メッセージ
$this->setMessage($bind->getTurnMessage($sicked_pokemon->getPrefixName(), $sc['ScBind']['param']));
// HPが0になっていればチェック終了
if(!$sicked_pokemon->getRemainingHp()){
return;
}
}
}
}
今回作成した努力値の取得処理はjudgment内で相手ポケモンのgetRewardEvメソッドを引数に実行しています。
それでは出力結果を見てみましょう。
setResponseを使って努力値の変化を確認するためにgetEvの結果を画面下へ出力しています。フシギダネとのバトル終了後、フシギダネにセットしたとくこう(SpAtk)の努力値が+1されていることが確認できました。
これで努力値の割り振り処理は完成です。
まとめ
いかがだったでしょうか。
今回のPHPポケモンは「努力値システムの実装方法」をご紹介しました。
多くのゲームでは隠しステータスが用いられており、その存在はゲームを更に楽しませてくれるような楽しみ要素の1つです。これがあることで、ゲームをやり込む楽しみや戦略性が生まれます。
ゲームづくりに興味がある方、プログラミング学習に取り組んでいる方は、ぜひ参考にしてくださいね。