6
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Game_action徹底解剖

先日カウンター拡張プラグインを作成しました。
このプラグインではカウンター条件の定義にJavaScriptの式を利用できるのですが、Game_actionがわからないことにはどうにもなりません。
そこで、徹底的に解説します。

subject()
actionの使用者が格納されています。
ダメージ式におけるaはsubject()の戻り値と同じです。

setSubject()
引数はGame_battlerクラスです。
ActorかEnemyかを見ながらパラメータを決めています。

friendsUnit()
opponentsUnit()
それぞれ、subject()の戻り値に対して同名の関数を呼び出しています。
スキル使用者の味方全て・あるいは敵全てを取得しています。
内部では、攻撃対象選択の処理で使われています。
setItem()
setSkill()
setItemObject()
上の2つが、数値でアイテムやスキルのIDをセットしています。
setItemObject()は上記2つから呼び出される関数で、直にアイテムを設定しています。
使うのがスキルだけ・アイテムだけという時は上の2つでよく、
スキルを使うかアイテムを使うかわからない時は、setItemObject()でどちらにも対応できるようにすればよいです。

useItem()
スキルやアイテムの発動コストを支払います。
具体的にはMPを消費したり、アイテムを使ったりです。

damageType関連

関数名 該当するダメージタイプ
isHpEffect() HPダメージ,HP回復,HP吸収
isMpEffect() MPダメージ,MP回復,MP吸収
isDamage() HPダメージ,MPダメージ
isRecover() HP回復,MP回復
isDrain() HP吸収,MP吸収
isHpRecover() HP回復
isMpRecover() MP回復

isHpEffect()に始まり、スキルおよびアイテムのダメージタイプの判定関数が並んでいます。
注意点として、isRecover()は追加効果による回復効果があっても反応しないということです。
そのため、actionが回復効果であるかをチェックする場合は専用の処理を作る必要があります。

speed()
行動準の処理を行う部分です。
ツクールの戦闘は素早さにランダム部分が入って面倒くさいとよく言われます。
ここのMath.randomInt以降を削除すればランダム部分はなくなります。

rpg_object.js
Game_Action.prototype.speed = function() {
    var agi = this.subject().agi;
    var speed = agi + Math.randomInt(Math.floor(5 + agi / 4));
    if (this.item()) {
        speed += this.item().speed;
    }
    if (this.isAttack()) {
        speed += this.subject().attackSpeed();
    }
    return speed;
};

ターゲット関連

isForOpponent()
isForFriend()
isForDeadFriend()
isForUser()
isForOne()
isForRandom()
isForAll()
攻撃対象の選び方を定義しています。
これらはboolの戻り値を返すのみで、実際のターゲット設定はmakeTargets()で行われます。

needsSelection()
スキルが対象選択を行うかどうかを定義しています。
敵単体・味方単体などが該当します。

makeTargets()
実際に攻撃対象を決定する処理です。
敵全体攻撃の場合、敵全てを対象にするという処理を行っています。
makeTargets()前にsetTarget()を呼んでおくと攻撃対象を固定できます。
デフォルトでは-1が入っており、この状態ではランダムにターゲットを選びます。
Enemyの行動は、ターゲット選択処理を行わずにやっているために、初期値の-1が入ったままになっています。

rpg_object.js
Game_Action.prototype.makeTargets = function() {
    var targets = [];
    if (!this._forcing && this.subject().isConfused()) {
        targets = [this.confusionTarget()];
    } else if (this.isForOpponent()) {
        targets = this.targetsForOpponents();
    } else if (this.isForFriend()) {
        targets = this.targetsForFriends();
    }
    return this.repeatTargets(targets);
};

targetsForOpponents()
targetsForFriends()

rpg_object.js
Game_Action.prototype.targetsForOpponents = function() {
    var targets = [];
    var unit = this.opponentsUnit();
    if (this.isForRandom()) {
        for (var i = 0; i < this.numTargets(); i++) {
            targets.push(unit.randomTarget());
        }
    } else if (this.isForOne()) {
        if (this._targetIndex < 0) {
            targets.push(unit.randomTarget());
        } else {
            targets.push(unit.smoothTarget(this._targetIndex));
        }
    } else {
        targets = unit.aliveMembers();
    }
    return targets;
};

targetsForFriends()

evalDamageFormula()のコードは省略しますが、似たようなものです。

ダメージ計算およびメイン処理

rpg_object.js
Game_Action.prototype.evalDamageFormula = function(target) {
    try {
        var item = this.item();
        var a = this.subject();
        var b = target;
        var v = $gameVariables._data;
        var sign = ([3, 4].contains(item.damage.type) ? -1 : 1);
        var value = Math.max(eval(item.damage.formula), 0) * sign;
        if (isNaN(value)) value = 0;
        return value;
    } catch (e) {
        return 0;
    }
};

データベースのダメージ計算式で定義したデータを使ってダメージ計算をします。
ダメージ計算式で呼ぶ事のできる関数はグローバル名前空間で定義した関数だけのようです。
自作のシステムがダメージ計算式に関わるのであれば、グローバル名前空間で呼び出すためのプラグインを1つ作っておくといいと思います。

また、ダメージ式の中でのthisはGame_actionクラスです。
プラグインによって外部からパラメータを格納する場合、Game_actionに格納するのが良いと思います。
この関数はダメージ計算以外に、自動行動を設定したアクターでも呼ばれるらしいです。
そのため、ダメージ式内部にアイテムを増減させる処理などの副作用を入れることはお勧めしません。

makeDamageValue()

rpg_object.js
Game_Action.prototype.makeDamageValue = function(target, critical) {
    var item = this.item();
    var baseValue = this.evalDamageFormula(target);
    var value = baseValue * this.calcElementRate(target);
    if (this.isPhysical()) {
        value *= target.pdr;
    }
    if (this.isMagical()) {
        value *= target.mdr;
    }
    if (baseValue < 0) {
        value *= target.rec;
    }
    if (critical) {
        value = this.applyCritical(value);
    }
    value = this.applyVariance(value, item.damage.variance);
    value = this.applyGuard(value, target);
    value = Math.round(value);
    return value;
};

evalDamageFormula()の戻り値を利用し、実際に適用するダメージを決めます。
target.pdrおよびtarget.mdrは不明。
target.recはおそらく薬の知識などによる回復倍率と思われます。
クリティカルの場合、applyCritical()でダメージを大きくします。
この後でapplyVariance()によりダメージに乱数補正がかかります。

game_object.js
Game_Action.prototype.applyVariance = function(damage, variance) {
    var amp = Math.floor(Math.max(Math.abs(damage) * variance / 100, 0));
    var v = Math.randomInt(amp + 1) + Math.randomInt(amp + 1) - amp;
    return damage >= 0 ? damage + v : damage - v;
};

ダメージのランダム化処理です。
特に書くことなし。

apply()

game_object.js
Game_Action.prototype.apply = function(target) {
    var result = target.result();
    this.subject().clearResult();
    result.clear();
    result.used = this.testApply(target);
    result.missed = (result.used && Math.random() >= this.itemHit(target));
    result.evaded = (!result.missed && Math.random() < this.itemEva(target));
    result.physical = this.isPhysical();
    result.drain = this.isDrain();
    if (result.isHit()) {
        if (this.item().damage.type > 0) {
            result.critical = (Math.random() < this.itemCri(target));
            var value = this.makeDamageValue(target, result.critical);
            this.executeDamage(target, value);
        }
        this.item().effects.forEach(function(effect) {
            this.applyItemEffect(target, effect);
        }, this);
        this.applyItemUserEffect(target);
    }
};

ダメージ計算処理のメイン部分です。
ここで実際のダメージ計算及びダメージの適用が行われます。
一つのスキルに対して、攻撃対象の数だけ処理が行われます。

乱数の処理と判定用のパラメータの取得を分けているのが自分好み。
一方で、使用効果の適用部分が関数で分けられていなくてフックできないのがイヤです。

item関連

Game_Action.item()で行動した内容が取得できます。
具体的にはSkillクラスかItemクラスです。
データベースを見ればわかりますが、スキルの設定とアイテムの設定はほぼ同じです。
スキルにはアイテムに加えて使用時のメッセージと要求する武器タイプが追加されています。

そのアクションがitemによるものかどうかを判定します。
懐かしのバルバトスとか作れますね。
isDamage
isMpEffect
MPが関連する効果かどうかを判定します。
MP回復・MPダメージ・MP吸収ならtrueです。

クリティカルヒット関連

仕様

ダメージ計算(makeDamageValue())までで、発生したダメージの値が0であれば処理を行わない。
クリティカルヒットが発生しても、取り消される。

rpg_object.js
Game_Action.prototype.apply = function(target){
   //中略
   if (this.item().damage.type > 0) {
       result.critical = (Math.random() < this.itemCri(target));
       var value = this.makeDamageValue(target, result.critical);
       this.executeDamage(target, value);
   }
}

Game_Action.prototype.applyの内、上記の部分がクリティカルヒットの処理を行う部分です。
itemCri

rpg_object.js
Game_Action.prototype.itemCri = function(target) {
    return this.item().damage.critical ? this.subject().cri * (1 - target.cev) : 0;
};

最終的なクリティカル率を決定する関数です。
この関数が返した数値を利用して判定を行います。
0.0なら0%・0.5なら50%・1.0なら100%です。
criは対象のBattlerの特徴で定められた「会心率」の合計値です。
この関数の戻り値はランダム要素を含みません。
補足:Math.random()は0 以上 1 未満の数値を返します。
最終的にGame_action.applyで参照されます。(他の場所からの参照は無し)

applyCritical

Game_Action.prototype.applyCritical = function(damage) {
    return damage * 3;
};

クリティカルが発生していれば、ダメージ計算式を適用した後にこの関数でダメージを書き換えます。
この時点で防御力の計算やステータスの上下動の影響を受けた後です。
ポケモンの「急所に当たった」(攻撃側にとって不利なダメージ補正を無効化する)やドラクエの「会心の一撃」(敵の防御力を無視する)を作ろうとした場合、ダメージ計算前にステートを付与するのが有効だと思います。
関連リンク:クリティカル時に一瞬だけステートを付与するプラグイン

使用効果

applyItemUserEffect

rpg_object.js
Game_Action.prototype.applyItemUserEffect = function(target) {
    var value = Math.floor(this.item().tpGain * this.subject().tcr);
    this.subject().gainSilentTp(value);
};

スキルの使用後にTPを増やしています。
TPは、いわゆる必殺ゲージでの使用を想定していると思われます。
そのほかにも、スキル使用後に眠り状態になるとか、そういった仕様を作る場合に役立つと思います。
関連リンク:トリアコンタンさんのスキル副作用プラグイン
スキル仕様前後のタイミングで、使用効果をスキル使用者に適用できる。

ステート

実はここがとんでもないことになっています。
ここではポケモンの技を例に挙げて説明します。
サンプルは、でんじは・10まんボルト・でんじほうの3つとします。

技名 命中 効果 MVでの命中の設定例
10まんボルト 100 マヒ(10%) 仕様効果(マヒ10%)
でんじほう 50 マヒ(100%) 成功率(50%)
命中タイプ(必中)
使用効果(マヒ100%)
でんじは(~XY) 100 マヒ 命中タイプ(必中)
仕様効果(マヒ100%)
でんじは(SM~) 90 マヒ 仕様効果(マヒ100%)
rpg_object.js
Game_Action.prototype.itemEffectAddNormalState = function(target, effect) {
    var chance = effect.value1;
    if (!this.isCertainHit()) {
        chance *= target.stateRate(effect.dataId);
        chance *= this.lukEffectRate(target);
    }
    if (Math.random() < chance) {
        target.addState(effect.dataId);
        this.makeSuccess(target);
    }
};

判定はこの部分なんですが、命中タイプを物理にすると追加効果の判定まで命中率判定を行います。
かといって必中にすると敵の状態異常耐性の概念が消えます。
正直、わかりづらいしこれは前のバージョンであるXPやVXaceでも指摘されていたため、今後も変更される可能性は低いです。
そのため、プレイヤーには命中70%と書きながら実際は90%にするといったバッドノウハウが構築されているようです。
さらに、処理をフックすることが困難で1から書かないといけないため修正プラグインは常に競合の危機にあります。

成功率は補助魔法の成功率を意図して作られていると考えられます。
その理由ですが、成功率で判定して失敗した場合、システム>用語>メッセージ最下部にある「行動失敗」の文章が再生されます。
デフォルトでは「効かなかった」ですが、これが補助魔法や状態異常魔法向けのメッセージ臭いのです。
参考までに必中・成功率0でスキルを使うと「行動失敗」で定義した文章が出ます。
ポケモン同様の形式にしようとすれば「攻撃は外れた」にする手もありますが、そうすると自身に効果のあるスキルの成功率を100%未満にしたときに変になってしまうというジレンマ。

まとめ

大体こんな感じです。
書き洩らしはありますが、需要あれば追加します。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
6
Help us understand the problem. What are the problem?