【解決!】トリアコンタン様のScopeExtendを用いた重複除外スキルについて

 いつもお騒がせしております。こまどりです。
 とうスレッドをご覧いただきありがとうございます。
 このフォーラムに登録してもうすぐ2年になるに関わらずスレ立ては初なので、至らぬ点も多々あると思いますから、適宜ご指摘いただけると幸いです。

 さて、本題に入りましょう。トリアコンタン様のプラグイン,「ScopeExtend」を入れて<SE重複除外><SERemoveDuplication>を選び対象を「敵ランダムn体」にしたとき、敵の数がnより大きくても攻撃がn回行われない場合がある点についてです。
 (参考映像)
 
 映像内でテレーゼが使っている「みだれぎり」のデータベース設定は次のようなものです。
2020-11-22_12h49_00(1).png
 「みだれぎり」はScopeExtendの機能で敵5体を攻撃する仕様になっていますが、動画内では殆どが4回攻撃、時に3回攻撃になってしまっています。これは検証用に新しく作ったプロジェクトなのでScopeExtend以外のプラグインは一切入っていません。
 私は現在制作中の自作品で「重複なくランダムに、"必ず"敵3体を攻撃するスキル」を作る目的でこのプラグインを導入しました。したがって「重複なく敵3体を攻撃する」スキルを実現する他の方法を知っている方からの意見・提案も歓迎します(プラグインの不具合に対してお知恵をいただくことを想定していますが)。
 

げれげれ

ユーザー
(初めに)
制作者であるトリアコンタン様の降臨を待つのが最適解なのかもしれませんが、
私の方でもどうにかできたので口出ししてみます。

>トリアコンタン様:もし余計な手出しであった場合は何事もなかったようにスルーして進めてください。

 ー ー ー ー ー ー ー ー 

まずおそらくですが、不具合ではないかなーと。
機能名称が「重複除外」であるように、
「1つのターゲットに攻撃が重複した場合に、1回しかHITしないように重複分を除外する機能」
がこのプラグインの意図するところかと思われます。
元より
「n回攻撃を確実にn体の敵にHITさせる機能」
ではないかと。
スクリプトの該当箇所からもその意図が読み取れます。(シンプルにArray.filterメソッドで重複ターゲットを除外しています)

4回攻撃に偏るのは、確率的に4回が出易いからですね。
(5回攻撃となるにはターゲットの抽選で一度も対象が被らない場合のみなので、
 1 * 7/8 * 6/8 * 5/8 * 4/8 ≒ 0.205(約20.5%) 5回に1回程度の確率しかない)

で、御託はこれくらいにして「確実にn体に攻撃するにはどうすれば良いか」ですが、
以下の要領でイケました。

211行目のif文を以下のように改変
JavaScript:
        if (this.isScopeExtendInfo(['重複除外', 'RemoveDuplication', '重複削除'])) {
            targets = targets.filter(function(target, i) {
                return targets.indexOf(target) === i;
            }.bind(this));
            //ここから追加
            var unit = this.opponentsUnit();
            //「アイテム対象数 >= 対象ユニットの生存数」の場合は生存メンバー全てを対象とする
            if(this.numTargets() >= unit.aliveMembers().length) {
                targets = unit.aliveMembers();
            } else {
                var addedMember = null;
                //targetsが設定対象数に達するまでメンバーを追加する
                while(this.numTargets() > targets.length) {
                    //targetsに含まれない生存メンバーを抽選で当てるまでループ
                    do {
                        addedMember = unit.randomTarget();
                    } while(targets.contains(addedMember));
                    targets.push(addedMember);
                }
            }
        }
ただし、これには少しだけ問題があります。

targets(攻撃対象数)がn体に達していない場合はひたすら再抽選を繰り返すのですが、
対象ユニットの狙われ率に極端な偏りがある場合、当たり(既存対象と被らない新規対象)を引き当てるまで
延々とループを繰り返します。
いちおう無限ループにはならないはずですが、狙われ率に極端な開きがあるとループ回数が大幅に増えます。
(それはもう年末ジャンボで一等前後賞を引き当てるまで延々とくじを買い続けるかのように)
狙われ率を生かしつつ無駄にループさせないスマートな実装方法が何かあれば良いのですが。

とはいえ、一般的なRPGの狙われ率の差程度ならば概ね問題ないはずです。
内部的には数十回程度の再抽選は発生するかもしれませんが、今時のPCの計算力なら
気にする程にはならないと思われます。

以上、ご参考までに。
 
  げれげれさん、お答えいただきありがとうございます!
 私はJavascriptについては造詣が浅いので、コードの解説までして下さったことを嬉しく思います。
>元より「n回攻撃を確実にn体の敵にHITさせる機能」ではないかと。
 どちらかというと「1~n体を攻撃するスキル」という設計思想だったわけですね。それと、「ランダム攻撃」をする際敵の狙われ率を参照するという仕組みは知りませんでした。ある種の範囲攻撃スキルとして用いることを加味すると、<SEランダム:n>タグと組み合わせたほうがスキルとして使い勝手がよいかもしれませんね。
>狙われ率を生かしつつ無駄にループさせないスマートな実装方法が何かあれば良いのですが。
 私がJavascriptを書けるわけではないので難易度は度外視しますし、さほどスマートでもないですが……。
 ターゲットを選んだ時点で、そのキャラクターは戦闘不能者のような「選出不可」の集合に移動するような仕組みがあれば、ループ回数は減ると思います。私が出来るわけではないですけど。

 ともあれ、貴重な時間を私の問題のために使っていただいてありがとうございます!早速動かしてみますね。
 

DarkPlasma

ユーザー
不具合ではなく、元々のターゲット一覧から重複を取り除くだけ、という仕様に見えます。
n体をランダムに選ぶだけであれば、対象を全体にしてSEランダムで事足りないでしょうか。

狙われ率を考慮したいというのであれば、 repeatTargets でゴソゴソやるよりも targetsForOpponents をハックする形にすると良いのでは。

JavaScript:
const _Game_Action_targetsForOpponents = Game_Action.prototype.targetsForOpponents;
Game_Action.prototype.targetsForOpponents = function() {
  if(this.isScopeExtendInfo(['ランダムwith狙われ率', 'RandomWithTargetRate'])) {
    const number = this.getScopeExtendInfo(['ランダムwith狙われ率', 'RandomWithTargetRate']);
    return this.randomTargetsWithoutDuplication(this.opponentsUnit(), number);
  }
  return _Game_Action_targetsForOpponents.call(this);
};

Game_Action.prototype.randomTargetsWithoutDuplication = function(unit, number) {
  return unit.randomTargetsWithoutDuplication(number);
};

Game_Unit.prototype.randomTargetsWithoutDuplication = function(number) {
  let candidates = unit.aliveMembers();
  const result = [];
  [...Array(number)].forEach(() => {
    const target = this.randomTargetFromCandidates(candidates);
    result.push(target);
    candidates = candidates.filter(candidate => candidate.index() !== target.index());
  });
  return result;
};

Game_Unit.prototype.randomTargetFromCandidates = function(candidates) {
  let tgrRand = Math.random() * this.tgrSumInCandidates(candidates);
  return candidates.find(candidate => {
    tgrRand -= candidate.tgr;
    return tgrRand <= 0;
  });
};

Game_Unit.prototype.tgrSumInCandidates = function(candidates) {
  return candidates.reduce((r, member) => r + member.tgr, 0);
};

ご指摘の通り、一部コードが間違っていたのを修正しました。
だいたいこんな感じじゃないかな、というふんわりした感覚で書いた例示コードなので、ちゃんと動くかどうかは試してみてください。

2020/11/25追記:
このままでは動きません。githubに動くものを公開していますので、そちらをご利用ください。
https://github.com/elleonard/RPGtko...er/plugins/DarkPlasma_RandomWithTargetRate.js
 
最後に編集:

げれげれ

ユーザー
ある種の範囲攻撃スキルとして用いることを加味すると、<SEランダム:n>タグと組み合わせたほうがスキルとして使い勝手がよいかもしれませんね。
n体をランダムに選ぶだけであれば、対象を全体にしてSEランダムで事足りないでしょうか。

そうですね、狙われ率を考慮する必要がないのであれば、「全体」+「SEランダム」が手っ取り早いですね。

ターゲットを選んだ時点で、そのキャラクターは戦闘不能者のような「選出不可」の集合に移動するような仕組みがあれば
そうなんですよねー、選択済のターゲットを抽選対象から除外したリストを作成することで
対処できないか考えたんですが、抽選機能(randomTargetメソッド)がGame_Unitオブジェクトでないと
使えないので、そこで「これはちょっと大掛かりな手を入れないと難しいかなぁ」という感じです。

狙われ率を考慮したいというのであれば、 repeatTargets でゴソゴソやるよりも targetsForOpponents をハックする形にすると良いのでは
やはりそうなりますか・・・
小手先の対応でごちゃごちゃやるよりも、別個の新機能として用意した方がむしろすっきりしますよね、そうですよね・・・
(参考になります!)
 

fspace

ユーザー
不具合ではなく、元々のターゲット一覧から重複を取り除くだけ、という仕様に見えます。
n体をランダムに選ぶだけであれば、対象を全体にしてSEランダムで事足りないでしょうか。

狙われ率を考慮したいというのであれば、 repeatTargets でゴソゴソやるよりも targetsForOpponents をハックする形にすると良いのでは。

JavaScript:
const _Game_Action_targetsForOpponents = Game_Action.prototype.targetsForOpponents;
Game_Action.prototype.targetsForOpponents = function() {
  if(this.isScopeExtendInfo(['ランダムwith狙われ率', 'RandomWithTargetRate'])) {
    const number = this.getScopeExtendInfo(['ランダムwith狙われ率', 'RandomWithTargetRate']);
    return this.randomTargetsWithoutDuplication(this.opponentsUnit(), number);
  }
  return _Game_Action_targetsForOpponents.call(this);
};

Game_Action.prototype.randomTargetsWithoutDuplication = function(unit, number) {
  return unit.randomTargetsWithoutDuplication(number);
};

Game_Unit.prototype.randomTargetsWithoutDuplication = function(number) {
  let candidates = unit.aliveMembers();
  const result = [];
  [...Array(number).keys()].reduce(() => {
    const target = this.randomTargetFromCandidates(candidates);
    result.push(target);
    candidates = candidates.filter(candidate => candidate.index() !== target.index());
  });
  return result;
};

Game_Unit.prototype.randomTargetFromCandidates = function(candidates) {
  let tgrRand = Math.random() * this.tgrSumInCandidates(candidates);
  return candidates.find(candidate => {
    tgrRand -= candidate.tgr;
    return tgrRand <= 0;
  });
};

Game_Unit.prototype.tgrSumInCandidates = function(candidates) {
  return candidates.reduce((r, member) => r + member.tgr, 0);
};

趣旨とはあまり関係ない細かい話でアレなんですが、randomTargetsWithoutDuplication内のreduceforEachに書き換え忘れてるみたいです。あと手前のkeysも。
 
 DarkPlasmaさん、ご提案誠にありがとうございます。そしてそれを元に、より完全なものへと修正してくださったfspaceさんもありがとうございます!
>n体をランダムに選ぶだけであれば、対象を全体にしてSEランダムで事足りないでしょうか。
 足りました。お恥ずかしい限りですが全くの盲点でした。この方法なら、<SE重複除外> で「1~n体ランダム攻撃」を実装したくなっても両立できますね……!さらに「狙われ率」を考慮した追加コードまで作っていただいて感激でございます。
 が、実際に動かしてみるとただの全体攻撃になってしまいました。具体的今一度動画とデータベース画面、DarkPlasmaさんのお書きした部分を加筆したjsファイルをお送りしますので、私のタグの使い方や追加コードの置き場所が間違えていないかご確認いただけないでしょうか。よろしくお願いします。
動画。ターゲット抽選の処理が上手く作動していない様子が伺えます。
「みだれぎり1」のデータベース設定は次の通りです。
2020-11-23_21h13_30(1).png
動画末尾で一度だけ出した「みだれぎり2」のデータベース設定です。こちらはご提案通り上手く行きました。改めて感謝申し上げます。
2020-11-23_22h16_05(1).png
そして、これが導入したjsファイルです
 

Attachments

  • ScopeExtendEx.js
    17.5 KB · 閲覧: 6

DarkPlasma

ユーザー
すみません。ScopeExtend.jsに直接追記することは想定していませんでした。
別のプラグインとして、以下のような内容にする想定でした。
JavaScript:
(function() {
  'use strict';
  // ここに例示コード
})();

同じプラグインの中に追記してしまうと、すでにあるコードと命名が衝突しそうですし、何より元プラグインの更新に追従できなくなるので、推奨しません。
 
2020-11-25_00h07_45(1).png
 こんばんは。別プラグインとして先日の部分をScopeExtendの下に入れて「みだれぎり1」を放ったところ、上のような画面が出てゲームが止まってしまいました。「みだれぎり2」は正常に動作したので、原因はDarkPlasmaさんにお書きいただいた部分にあると思われます。今一度コードをご確認いただけないでしょうか。
 
 検証用プロジェクト及び、自作品のプロジェクト内の双方で動作を確認しました!DarkPlasmaさん、お付き合いいただきありがとうございます!この返信を以て当スレッドは解決と致します。DarkPlasmaさんの名前もクレジットに載せさせていただきますね!
 
最後に編集:
トップ