へんしんとは
今回はサムネイルに合わせて、特別技の1つ「へんしん」を実装します。
へんしんを使うことで、相手ポケモンをコピーすることができますが、その全てをコピーするわけではありません。コピーできる項目とそうでない項目は以下の通りです。
コピーできる項目
- ポケモンの種族
- HP以外のステータス
- ランク補正
- 倒した際の努力値
- 基礎経験値
- 努力値
コピーできない項目
- HP
- レベル
- ニックネーム
- 状態異常
- 状態変化
- 個体値
現在PHPポケモンで実装されている機能をまとめました。本来のポケモン仕様ではHPを除くステータス実数値を引き継ぐことになります。ですが、PHPポケモンでは実数値を保管せず、努力値・個体値・ランク補正を使って計算しているため、こちらは独自の仕様を入れながら作成していきます。
へんしん処理の流れ
まずはへんしん処理をする上での流れをまとめましょう。他の技と異なり、変更しなければいけない項目が多く、種族値などポケモンが持つ値を変更してしまうと、バトル終了判定で元に戻す項目が多くなります。そうなると、本来ではなり得ない状態が出来上がってしまうことにも繋がりますので、バトルで使用しているインスタンスを置き換えることで対応します。
× へんしん後のポケモンに書き換え
○ バトルで使用するインスタンスを置き換え
インスタンスを置き換えるために、へんしんを使った際に「へんしん状態のポケモンオブジェクト」を呼び出す必要があります。通常のインスタンス作成では、HPの種族値など変更できない項目がいくつかあるので、こちらはへんしん専用のインスタンス作成処理を追加することで対応します。
へんしん用インスタンスの作成
それでは、1つずつ処理を見ていきましょう。
コピーポケモンの作成
まずはコピーポケモンのインスタンス作成処理についてです。現在はポケモンオブジェクトを呼び出す際にレベルの指定のみを引数としていましたが、第2引数を追加して「へんしん」によるオブジェクト生成ができるようにしましょう。
ポケモンクラス(/Classes/Pokemon.php)
// ポケモン
abstract class Pokemon
{
--省略
/**
* へんしんフラグ
* @var boolean
*/
protected $transform_flg = false;
/**
* インスタンス作成時に実行される処理
* @param param:mixed
* @param transform:object|null::Pokemon
* @return void
*/
public function __construct($param=null, $transform=null)
{
if(is_object($transform)){
// へんしん用処理
$this->transform($param, $transform);
}else{
// 初期化
$this->init($param);
}
}
もし第2引数で対象のポケモンオブジェクトが指定されていた場合、へんしんの特別処理としてtransformのメソッドを呼び出しています。記述量が多いので、こちらはトレイト分けしました。
へんしん処理用トレイト(/App/Traits/Class/Pokemon/ClassPokemonTransformTrait.php)
<?php
// へんしん処理専用トレイト
trait ClassPokemonTransformTrait
{
/**
* へんしんによるインスタンスの作成処理
* @param pokemon:object::Pokemon
* @param enemy:object::Pokemon
* @return void
*/
private function transform(object $pokemon, object $enemy)
{
// へんしんでコピーするステータス
$this->rank = $enemy->getRank(); # ランク
$this->iv = $enemy->getIv(); # 個体値
$this->ev = $enemy->getEv(); # 努力値
$this->move = $enemy->getMove(null, 'array'); # 覚えている技
// へんしんでコピーされないステータス
$this->level = $pokemon->getLevel(); # レベル
$this->sa = $pokemon->getSa(); # 状態異常
$this->sc = $pokemon->getSc(); # 状態変化
$this->position = $pokemon->getPosition(); # 立場
$this->nickname = $pokemon->getNickName(); # ニックネーム
$this->remaining_hp = $pokemon->getRemainingHp(); # 残りHP
$this->exp = $pokemon->getExp(); # 現在の経験値
/**
* へんしん時に書き換えする処理
*/
// へんしんフラグ
$this->transform_flg = true;
// HPは元ポケモンのステータスを引き継ぐ
$this->base_stats['HP'] = $pokemon->getBaseStats()['HP'];
$this->iv['HP'] = $pokemon->getIv()['HP'];
$this->ev['HP'] = $pokemon->getEv()['HP'];
// 技の残りPPを5にする
$this->move = array_map(function($move){
$move['remaining'] = 5;
return $move;
}, $this->move);
// 覚える技・進化レベルを空にする(念の為)
$this->level_move = [];
if(isset($this->evolve_level)){
$this->evolve_level = null;
}
}
}
ステータスの実数値を引き継げるように、個体値・努力値は相手ポケモンのものを受け取っています。HPのステータス変更は入らないため、書き換え後に上書きで自ポケモンのものと置き換えています。
また、へんしん後にレベルアップ処理が入った際にも同様の演出ができるように、現在の経験値も元ポケモンから引き継いでいます。
技のPPはへんしん後にオール5になるので、array_mapを使って書き換えています。
プロパティの置き換え
現在バトルコントローラーで使用してるポケモンのオブジェクトは、pokemonというプロパティに格納されています。こちらを置き換えることで、元ポケモンのオブジェクトはparty内、バトル用はpokemonというように扱い、元オブジェクトを書き換えることなく処理をします。
バトルコントローラー用トレイト(/App/Traits/Controller/BattleControllerTrait.php)
/**
* 引き継ぎ処理
* @return void
*/
protected function inheritance()
{
// バトル状態の引き継ぎ
if(isset($_SESSION['__data']['battle_state'])){
$battle_state = unserializeObject($_SESSION['__data']['battle_state']);
// ターン最初の状態へ初期化
$battle_state
->turnInit();
// グローバルにセット
setBattleState($battle_state);
}else{
// バトル状態の初期化
initBattleState();
}
// ポケモン番号の引き継ぎ
$this->order = $_SESSION['__data']['order'];
// 敵ポケモンの引き継ぎ
if(isset($_SESSION['__data']['enemy'])){
$this->enemy = unserializeObject($_SESSION['__data']['enemy']);
// 前ターンの状態をプロパティに格納
$this->before['enemy'] = clone $this->enemy;
}
// 戦闘中のポケモンをプロパティにセット
$transform = getBattleState()->getTransform('friend');
if($transform){
// へんしん状態の場合はBattleStateからポケモン情報を取得
$this->pokemon = $transform;
}else{
$this->pokemon = $this->party[$this->order];
}
// 前ターンの状態をプロパティに格納
$this->before['friend'] = clone $this->pokemon;
}
もしへんしん状態であれば、格納するオブジェクトをパーティーからではなく、バトル状態へ格納したポケモンオブジェクトから参照するように分岐をさせました。
バトル状態クラス(/Classes/BattleState.php)
<?php
/**
* バトル状態クラス
*/
class BattleState
{
--省略
/**
* へんしん情報
* @var array
*/
private $transforms;
/**==================================================================
* 初期化・初期値
==================================================================**/
/**
* 初期化
* @return void
*/
public function init() :void
{
$this->run = 0;
$this->dafaultFields();
$this->dafaultTurnDamages();
$this->dafaultTransforms();
$this->dafaultLastMoves();
}
--省略
/**
* へんしん状態の初期値
* @return void
*/
public function dafaultTransforms() :void
{
$this->transforms = [
'friend' => [],
'enemy' => [],
];
}
--省略
/**==================================================================
* へんしん状態
==================================================================**/
/**
* へんしん状態の格納
* @param pokemon:object::Pokemon
* @param enemy:object::Pokemon
* @return object::Pokemon
*/
public function setTransform(object $pokemon, object $enemy): object
{
$class = get_class($enemy);
$this->transforms[$pokemon->getPosition()] = new $class($pokemon, $enemy);
// へんしん後のポケモンインスタンスを返却
return $this->transforms[$pokemon->getPosition()];
}
/**
* へんしん状態の取得
* @param position:string::friend|enemy
* @return mixed
*/
public function getTransform(string $position)
{
return $this->transforms[$position];
}
}
へんしん状態でレベルアップ処理や状態異常の処理が入った場合、両方に同じように処理を加える必要があります。なので、こちらはそれぞれ処理終了直前に整合性を取るための処理を追加します。
バトルコントローラー用トレイト(/App/Traits/Controller/BattleControllerTrait.php)
/**
* バトル結果判定
*
* @return void
*/
private function judgment()
{
if($this->fainting['friend']){
// 味方がひんし状態になった
setMessage('目の前が真っ暗になった');
}else{
// 相手がひんし状態になった(味方はひんし状態ではない)
// 経験値の計算
$exp = $this->calExp($this->pokemon, $this->enemy);
// 経験値をポケモンにセット
$this->party[$this->order]
->setExp($exp);
// 努力値を獲得
$this->party[$this->order]
->setEv($this->enemy->getRewardEv());
// もしポケモンが「へんしん状態」であれば変更後の状態を引き継ぎ
if($this->pokemon->checkSc('ScTransform')){
$this->pokemon
->judgmentTransform($this->party[$this->order]);
}
}
// 散らばったお金の取得
$money = getBattleState()->getMoney();
if($money){
setMessage($this->player->getName().'は、'.$money.'円拾った');
$this->player
->addMoney($money);
}
// バトル終了判定用メッセージの格納
setEmptyMessage('battle-end');
}
/**
* デストラクタ直前の処理
* @return void
*/
private function checkBeforeDestruct()
{
$transform = getBattleState()->getTransform('friend');
// もし「へんしん状態」であれば、残HPと状態異常を元ポケモンに反映
if($transform){
$this->party[$this->order]
->mirroringTransform($transform);
}
}
へんしん用トレイト(/App/Traits/Class/Pokemon/ClassPokemonTransformTrait.php)
/**
* へんしん状態のステータスを元ポケモンに反映させる処理
* @param trans_pokemon:object::Pokemon
* @return void
*/
public function mirroringTransform(object $trans_pokemon)
{
if($this->checkSc('ScTransform')){
$this->sa = $trans_pokemon->getSa('all'); # 状態異常
$this->remaining_hp = $trans_pokemon->getRemainingHp(); # 残HP
}
}
/**
* バトル判定後の状態をへんしん状態のポケモンに反映させる処理
* @param base_pokemon:object::Pokemon
* @return void
*/
public function judgmentTransform(object $base_pokemon)
{
$this->ev = $base_pokemon->getEv(); # 努力値
$this->level = $base_pokemon->getLevel(); # レベル
$this->exp = $base_pokemon->getExp(); # 経験値
}
レベルアップ処理については、パーティー内のオブジェクトに対して直接処理を行い、へんしんポケモンには値のみを引き継ぐようにしています。
状態異常と残りHPはへんしんポケモンを基準として、パーティー内の元ポケモンに値を反映させました。これで、両方が同じ状態で進行するように整合性を保っています。
次に、バトル処理中のオブジェクトについてです。もしへんしんを先手で発動した場合、防御ターンにそのタイプやステータスが反映されている必要があります。オブジェクトは参照されていますが、プロパティの値そのものが変更される場合は、既に配列に格納されたオブジェクトからはアクセスができなくなってしまいます。
なので、バトル処理内でも「へんしん」が行われたかを判定するための分岐を追加して、プロパティ自体を変更しましょう。
たたかう用サービス(/App/Services/Battle/FightService.php)
/**
* 行動順に攻撃処理
*
* @return boolean (false: ひんしポケモン有り)
*/
private function actionAttack($orders)
{
foreach($orders as list($atk, $def, $move)){
// 攻撃ポケモンの怒り解除
$atk->releaseSc('ScRage');
// 先手が「へんしん」を使って成功した場合のオブジェクト置き換え処理
if(!$def->getTransformFlg() && $def->checkSc('ScTransform')){
// 防御ポケモンのへんしんフラグがfalse且つ、状態変化で「へんしん」がセットされている場合
$def = $this->battle_state
->getTransform($def->getPosition());
}
// 攻撃(返り値に使用した技を受け取る)
$attack_move = $this->attack($atk, $def, $move);
// 最後に使用した技を格納
$this->battle_state
->setLastMove($atk->getPosition(), $attack_move);
// バトル終了のレスポンスチェック(交代技など)
if(getResponse('end')){
break;
}
// ひんしチェック
$this->fainting = [
$atk->getPosition() => $this->checkFainting($atk),
$def->getPosition() => $this->checkFainting($def),
];
// どちらかがひんし状態なら処理終了
if($this->fainting['friend'] || $this->fainting['enemy']){
$result = false;
break;
}
} # endforeach
// 結果返却
return $result ?? true;
}
これで、プロパティの値を変更する処理が整いました。
へんしんの特別処理
次に、へんしんを使った際の特別処理を見ていきましょう。まずは、技クラスと状態変化クラスについてです。
へんしん:技(/Classes/Move/MoveTransform.php)
<?php
$root_path = __DIR__.'/../..';
require_once($root_path.'/Classes/Move.php');
// へんしん
class MoveTransform extends Move
{
/**
* 正式名称
* @var string
*/
protected $name = 'へんしん';
/**
* 説明文
* @var string
*/
protected $description = '相手のポケモンに変身する。';
/**
* タイプ
* @var string
*/
protected $type = 'TypeNormal';
/**
* 分類
* @var string(physical:物理|special:特殊|status:変化)
*/
protected $species = 'status';
/**
* 威力
* @var integer|null
*/
protected $power = null;
/**
* 命中率
* @var integer|null
*/
protected $accuracy = null;
/**
* 使用回数
* @var integer
*/
protected $pp = 10;
/**
* 対象
* @var string
*/
protected $target = 'enemy';
/**
* へんしんの専用効果
* @param atk:object::Pokemon
* @param def:object::Pokemon
* @param battle_state:object::BattleState バトル状態
* @return void
*/
public function exTransform(object $atk, object $def, object $battle_state)
{
if(!$atk->checkSc('ScTransform')){
// 現在へんしん状態でなければ実行
$atk->setSc('ScTransform', 0, get_class($def)); # 先に状態変化(へんしん)をセット
$battle_state->setTransform($atk, $def);
return true;
}else{
// 現在へんしん状態であれば失敗
return false;
}
}
}
へんしん:状態変化(/Classes/StateChange/ScTransform.php)
<?php
$root_path = __DIR__.'/../..';
require_once($root_path.'/Classes/StateChange.php');
// へんしん
class ScTransform extends StateChange
{
/**
* 正式名称
* @var string
*/
protected $name = 'へんしん';
}
へんしんオブジェクトを作成する際に状態変化をコピーしているため、オブジェクト生成前に状態変化をセットしています。へんしんループを防ぐために、PHPポケモンではへんしん状態からのへんしんは失敗するようにしました。
次に、攻撃トレイト内での処理について見てみましょう。
攻撃トレイト(/App/Traits/Service/Battle/ServiceBattleAttackTrait.php)
// attackメソッド内
//
// 攻撃メッセージを格納
$this->atk_msg_id = issueMsgId();
setMessage(
$atk_pokemon->getPrefixName().'は'.$move->getName().'を使った!',
$this->atk_msg_id
);
// 「へんしん」の特別処理
if(get_class($move) === 'MoveTransform'){
$this->exTransform($atk_pokemon, $def_pokemon, $move);
return $move;
}
へんしん使用メッセージは必要になるため、攻撃メッセージ生成直後に返信の特別処理を呼び出しています。
特別処理用トレイト(/App/Traits/Service/Battle/ServiceBattleExTrait.php)
/**
* へんしんの特別処理
*
* @param atk:object::Pokemon
* @param def:object::Pokemon
* @param move:object::Move
* @return void
*/
protected function exTransform(object $atk, object $def, object $move): void
{
// へんしんの特別処理を呼び出し
$result = $move->exTransform($atk, $def, $this->battle_state);
if($result){
// へんしん
setResponse([
'param' => get_class($def),
'action' => 'transform',
'target' => $atk->getPosition(),
], $this->atk_msg_id);
// 成功
setMessage($atk->getPrefixName().'は'.$def->getName().'にへんしんした');
// プロパティの書き換え
if($atk->getPosition() === 'friend'){
// 味方
$this->pokemon = $this->battle_state
->getTransform('friend');
}else{
// 相手
$this->enemy = $this->battle_state
->getTransform('enemy');
}
}else{
// 失敗
setMessage('しかし上手く決まらなかった');
}
}
へんしん後はポケモンの画像を変更する必要があるので、レスポンスには対象ポケモンのクラス名、actionにtransform、targetに自身を選択しています。サービス自体のプロパティの置き換え処理は、このタイミングで行っています。
画像の置き換え
次に画像の置き換え処理についてです。こちらはjsでレスポンスの値に合わせてsrcを書き換えます。現在のポケモンの判定がしやすいように、data要素として現在のポケモンクラスを持たせました。
バトル画面(/Resources/Page/Battle.php)
<?php # 自ポケモン詳細 ?>
<div class="col-6 text-center">
<input type="image"
id="friend-pokemon-image"
class="action-img-btn"
src="/Assets/img/pokemon/dots/back/<?=get_class($before_pokemon)?>.gif"
alt="<?=$before_pokemon->getName()?>"
data-pokemon="<?=get_class($before_pokemon)?>"
data-toggle="modal"
data-target="#friend-battle-state-modal" />
</div>
バトルメッセージ用JS(/Public/Assets/js/Battle/message.js)
// ==============================================
// へんしん処理 =================================
//
/**
* @param json
* @return Promise
**/
var doAnimateTransform = function (target, param){
return new Promise ((resolve, reject) => {
// 対象のポケモン画像を指定
var img = $('#' + target + '-pokemon-image');
// へんしん前のポケモンクラスを取得
var before = img.data('pokemon');
// srcを書き換える
var src = img.attr('src').replace(before, param);
img.attr('src', src);
resolve();
});
}
メッセージのアクション分岐にtransformを追加し、へんしん用のアニメーション処理へ分岐させるようにしました。jsでの処理は画像を変更するだけなので、受け取ったポケモンクラスを、現在のポケモンクラスと置き換えるように、srcに対してreplaceをして新しい画像パスへと変更しています。
では、実際にへんしんによる技効果を見てみましょう。
しっかり対象ポケモンにへんしん、技を引き継ぐことができました。
メタモンの作成
へんしんを追加することができたので、ここで久々に新しいポケモン「メタモン」を追加しましょう。
メタモン(/Classes/Pokemon/Metamon.php)
<?php
require_once(__DIR__.'/../Pokemon.php');
// メタモン
class Metamon extends Pokemon
{
/**
* 正式名称
* @var string(min:1 max:5)
*/
protected $name = 'メタモン';
/**
* ニックネーム
* @var string(min:1 max:5)
*/
protected $nickname = '';
/**
* タイプ
* @var array
*/
protected $types = ['TypeNormal'];
/**
* 初期レベル
* @var array
*/
protected $default_level = [
5
];
/**
* 基礎経験値
* @var integer
*/
protected $base_exp = 101;
/**
* レベルアップで覚える技
* @var array
*/
protected $level_move = [
[1, 'MoveTransform'], # へんしん
];
/**
* 種族値
* @var array
*/
protected $base_stats = [
'HP' => 48,
'Attack' => 48,
'Defense' => 48,
'SpAtk' => 48,
'SpDef' => 48,
'Speed' => 48,
];
/**
* 獲得努力値
* @var array
*/
protected $reward_ev = [
'HP' => 1,
];
}
技はへんしんのみです。実際に相手がへんしんを使った場合でも、正常な処理がされるかどうかを検証するために用意しました。では、相手ポケモンとしてメタモンを登場させて戦ってみましょう。
こちらが持っている技をコピーして戦っていることが確認できました。へんしん後は獲得努力値と経験値も引き継いでいるため、それぞれが高いポケモンで挑めば効率よく稼ぐことができます。しかし技構成がコピーされてしまうため、HPの状態やレベル、急所の発動頻度によっては自分の首を締める結果にもなるので注意が必要です。
メタモンについては、近々本番環境へも反映予定ですので、ぜひ遊んでみてくださいね。
まとめ
いかがだったでしょうか。
今回のPHPポケモンでは「へんしん」の実装方法をご紹介しました。
ポケモンに興味がある方、プログラミング学習に挑戦しようと考えている方は、ぜひ参考にしてみてくださいね。