RPGツクールプラグイン入門

現在RPGツクール入門中で、下記の投稿をしました。
[RPGツクール入門（ちょっと変化球）](https://qiita.com/JQinglong/items/b0f4975bb5f70fac9518)

なかなか面白うそうで、次は、オンライン保存系をやろうと思ったのですが、いずれにせよ、プラグイン開発の力が必要になりそうということで、まず一つプラグインを作ってみることにしました。

テーマは「クイズの出題」！
ツクールの中で条件分岐をひたすら書くのではなく、プラグインコマンドで出題、正解判定を行うというものです。

## プラグインの仕様

1. 問題・回答は、スプレッドシートで準備して、それを読み込めるようにする。
1. ランダムに出題できるようにする。
1. 出題する問題の絞り込みをできるようにする。
1. 正解・不正解により処理の切り分けをできるようにする。

こんな感じでしょうか。

できたものの使い方としては、
1. dataフォルダ配下にCSVファイルを設置する。
2. プラグイン管理画面で、CSVファイル名、利用する変数・スイッチの範囲を設定する。
3. イベント処理で、出題コマンドを呼び出す。
4. 出題に続けて、結果表示コマンドを呼び出す。
5. 必要に応じて、結果による処理の切り分けを行う。

という流れになります。
イベントの書き方はこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77307/a27bcd7a-ec56-0ef5-dcaa-7d5f14086cec.png)

このイベントを呼び出すとこんなかんじで出題されます。

![出題.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77307/a8d011e2-b4cd-b2fb-5aa0-19782e5facbd.png)


## プラグインの作り方

初歩の初歩として、そもそも何から始めればよいのかは、こちらを参照させていただきました。
[RPGツクールMV プラグイン作成入門](https://qiita.com/yamachan360/items/4160e2a1eaf25bbf0050)

スケルトンまで用意されていてありがたいです。

最初は、コモンイベントを作って呼び出すような形になるのかなとも思ったのですが、プラグインコマンドを呼び出せば何でもできるなというのが見えてきて、プラグインの中ですべてを収めることを目指しました。
書くのは、 ツクールMVで用意されているスクリプトを呼び出すわけで、そのリファレンスとしては、
[RPGツクールVXAce & MVスクリプトwikiとプラグイン](http://rpgmaker-script-wiki.xyz/index.php)
を参照しています。
特に、今回の最初のとっかかりで、4択問題の選択肢を表示するわけですが、こちらを見て、かなりイメージがつくようになりました。
[ツクールMVのスクリプトで選択肢の表示を行う方法](http://rpgmaker-script-wiki.xyz/sentakusi_mv.php#)

## スプレッドシートデータの読み込み
野望としては、Firebaseなりの外部APIから問題を取得することを考えましたが、用途としてはむしろCSV読み込みの方が多いのではないか、これを用意しておけば、データ取得のところを差し替えたAPI版もすぐに作れるのでは、ということで、CSVを読み込むことにしました。

CSV読み込みについては、こちらのプラグインを大いに参照させていただきました。
[プラグイン「文章を外部CSVファイルで記述する(4行以上でも可)」](https://forum.tkool.jp/index.php?threads/%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%80%8C%E6%96%87%E7%AB%A0%E3%82%92%E5%A4%96%E9%83%A8csv%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%A7%E8%A8%98%E8%BF%B0%E3%81%99%E3%82%8B-4%E8%A1%8C%E4%BB%A5%E4%B8%8A%E3%81%A7%E3%82%82%E5%8F%AF-%E3%80%8D.4006/)

長い問題を出題する際に、改行しないとはみ出してしまうわけですが、こちらのプラグインでその部分は実装されていたので助かりました。（と言いつつ、それに気づくまで結構試行錯誤してしまいましたが）

改造した点としては、
+ 中国語も扱いたかったので、読み込み時のcharsetをshift_jisからutf-8に変更
+ CSV内に記述できるコマンドの対応は削除（最初はシンプルにしたかったので）
+ 改行コード（\r\n）の扱いを変更し、文章内での\nによる改行を有効化（元の意図を読み切れていませんが・・・）

といったところなどです。

また、それに伴い、自動折り返しの処理を追加しました。
顔グラなしで、全角27文字が表示可能だと思いますので、25文字（というか全角は2文字換算で半角50文字）ごとに\nを挿入するようにしました。
これを$gameMessage.addに渡してあげれば、勝手に表示してページ送りもしてくれるというのは、ツクールMV様、イケてます。

![出題2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77307/cd99d278-ddd9-aba1-1ce2-6326614be4b1.png)
![出題3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/77307/e10661e4-13d3-2d8d-7ee4-91d5d97aa54f.png)

（さすがにワードラップまでは無理ですが・・・）

## ランダム出題・範囲絞り込み

このステージではレベル1の問題、次のステージではレベル2の問題、そして、そのなかでランダムに出題される、というようなことをやりたくて、データにはLV情報を持たせました。

まあ、この辺は、純粋にJavaScriptのお話ということで、あちこちから勉強させていただきました。
一気にソースのレベルが低くなっているので、あまり参考にはなさらずに。

## プラグインコマンド

上記のWikiに解説がありましたが、選択肢を表示させる処理と、選択した結果を取得する処理は、切る必要があります。
ですので、プラグインコマンドは二つ用意しました。
コマンドの作成方法についても、最初に挙げたプラグイン作成入門サイトで説明されている通りに簡単にできたので、大感謝ですし、その辺の作りも、ツクールMV、イケてると思います。

プラグインの中では、変数7個とスイッチ一つを使用します。既存のものとのバッティングを回避できるように、その番号をパラメタとして指定できるようにしました。
このスイッチを正解フラグとして使用し、プラグインコマンド実行後に、そのフラグで処理を切り分けできるようにしたというのが面白いところかなと思っています。

## 開発・テスト環境
プラグインを作り出すと、ツクール本体は単なる実行環境で、開発環境はVSCodeになっています。
[Visual Studio CodeでツクールMVをデバッグ～NW.js(ローカル環境)編～](https://qiita.com/asakayu/items/a68440e597373aabba02)

これなんかできるとよいなと思ったのですが、オンライン連携の処理が入っている関係で？うまくいきませんでした。
その辺も処理を差し替えできるようにすると、開発が楽になりそうな気がします。

```
Exception has occurred: Error
Error: Error: This operation is not supported in the environment this application is running on. "location.protocol" must be http or https.
    at c:\Users・・・
```

ところで、RPGツクール製品は、現在、RPGツクールMV Trinity はゴールデンウイークセール中ですが、通常のMVはやってないな、ということで英語版を見たら、英語版は「STAY AT HOME SALE」とか言って、20ドルでセールしている。
[RPGMAKER](https://www.rpgmakerweb.com/products/programs/rpg-maker-mv)
いいな。英語版買おうかなとか思っていたのですが、今ふと見たら、日本版もセールしています！
[RPGツクールMVダウンロード版](https://store.tkool.jp/a/rpgmaker/mv)
2020/5/7まで！
Steam版は微妙に安いけど、雰囲気で公式サイトのダウンロード版を購入。
ただし、現在はWindows機で体験版使っていますが、買ってしまったら、結局はMacでSteam入れて使うかもしれませんが・・・

というわけで、興奮状態は続きます～

ソース（とりあえず貼り付けておきますが、置き場も考えよう・・・）

```js
//=============================================================================
// 4Quiz.js	2020.4.29
// The MIT License (MIT)
//=============================================================================

/*:
 * @plugindesc 4択クイズ用プラグイン
 * @author JayQ
 *
 * @param Start Val ID
 * @desc 変数範囲開始番号：変数n番（ここで指定した番号）からn+6番まで本プラグインで使用します。
 * @default 11
 * @type number
 *
 * @param Answer Switche ID
 * @desc 正解フラグ用スイッチ番号：本プラグインでは指定されたスイッチを出題時にOFF、正解時にONにします。
 * @default 21
 * @type number
 *
 * @param Csv File Path
 * @desc メッセージが記述されてるcsvファイルパス。ルートパスはdataフォルダになっています。
 * @default ExternMessage.csv
 * @type string
 * 
 * @help
 *      CSVファイルからクイズを出題するプラグインです。
 * 
 *   	CSVファイルは以下のようなフォーマットにしてください。
 *   		LV,問題,選択肢1,選択肢2,選択肢3,選択肢4,正解番号,解説文
 *          （例）
 *   		1,JR香川駅は香川県にありません。何県にある？,愛媛県 ,高知県,徳島県,神奈川県,4,Wikipediaより
 *   		1,"A word or phrase is missing in each of the sentences below. Four answer choices are given below each sentence. Select the best answer to complete the sentence.
 *   		Customer reviews indicate that many modern mobile devices are often unnecessarily ------- .",complication,complicates,complicate,complicated,4,"The best answer to the question is choice 4.
 *   		Therefore, you should choose answer 4."
 * 
 *      2行目のように、"で囲って改行（\n）を含めることもできます。（スプレッドシートの機能でCSV形式で保存時にそうしてくれると思います）
 * 
 * Plugin Command:
 *   4Quiz question 2             # LV2の問題を出題
 *   4Quiz answer                 # 結果表示
 * 
 */

 // CSVファイルのグローバルアクセス
var $externMessageCSV = null;

//---------------------------------------------------------------------------------------------
// ExternMessage データーベース
//---------------------------------------------------------------------------------------------
var $externMessage =
{
    // セットアップでデータを
	setup: function() {
		// // 改行コードを置き換え
		// $externMessageCSV = $externMessageCSV.replace("\r\n", "\n");

		// 2次元配列に変換
		this.map = CsvImportor.parseFromCSV($externMessageCSV);

	}
};


(function(_global) {
    // ここにプラグイン処理を記載
    var N = '4Quiz';
    var param = PluginManager.parameters(N);
    var startVal = Number(param['Start Val ID'])||11;

    // CSV関連
    var parameters = PluginManager.parameters('ExternMessage');
	$externMessage.CsvFilePath = String(param['Csv File Path']);

    // DataManagerへ読み込みの予約とセットアップの予約
    // DataManager._databaseFiles.push により、変数への取り込みを行う。（通常はjsonファイル）
	DataManager._databaseFiles.push({ name:'$externMessageCSV',	src:$externMessage.CsvFilePath	});

	// ロードが完了したら呼ばれるように
	var _DataManager_setupNewGame = DataManager.setupNewGame;
	DataManager.setupNewGame = function() {
		_DataManager_setupNewGame.apply(this, arguments);
		$externMessage.setup();
	};
    // CSV関連 END

    // プラグインコマンド
    var _Game_Interpreter_pluginCommand = Game_Interpreter.prototype.pluginCommand;
    Game_Interpreter.prototype.pluginCommand = function(command, args) {
        _Game_Interpreter_pluginCommand.call(this, command, args);
        if (command == N) {
            if (args[0] == "start_val") {

                startVal = args[1] || 11;

            } else if (args[0] == "question") {

                // データ読み込み
                var filteredMap = $externMessage.map
                if (args[1]) {
                    filteredMap = $externMessage.map.filter(item => item[0] == args[1])
                }
                var maxID = filteredMap.length
                var randNum = Math.floor(Math.random()*(maxID - 0) + 0);
                var data =  filteredMap[randNum]

                // 問題
                $gameVariables.setValue(startVal, data[1])
                //選択肢
                $gameVariables.setValue(startVal + 1, data[2])
                $gameVariables.setValue(startVal + 2, data[3])
                $gameVariables.setValue(startVal + 3, data[4])
                $gameVariables.setValue(startVal + 4, data[5])
                // 正解番号
                $gameVariables.setValue(startVal + 5, data[6])
                // 解説文
                $gameVariables.setValue(startVal + 6, data[7])
                
                // 正解フラグ初期化
                $gameSwitches.setValue(param['Answer Switche ID'], false)

                // 出題の文章表示
                $gameMessage.add($gameVariables.value(startVal))

                // 選択肢の表示
                this.setupChoices([
                    [$gameVariables.value(startVal + 1),
                    $gameVariables.value(startVal + 2),
                    $gameVariables.value(startVal + 3),
                    $gameVariables.value(startVal + 4) ],
                    4,0,2,0]);
                this.setWaitMode('message');

            } else if (args[0] == "answer") {

                if ($gameVariables.value(startVal + 5) == (this._branch[this._indent] + 1)) {
                    $gameSwitches.setValue(param['Answer Switche ID'], true)
                    $gameMessage.add('正解')
                } else {
                    $gameMessage.add('不正解')
                }

                // 解説文表示
                $gameMessage.add($gameVariables.value(startVal + 6))
            }
        }
    };

})(this);


//---------------------------------------------------------------------------------------------
// CSV Helper
//---------------------------------------------------------------------------------------------
// CSVファイルから2次元配列にパースする
var CsvImportor = 
{
	parseFromCSV: function(text)
	{
		var result = new Array();
		var currentLine = new Array();
		var begin = 0;
		for(var index = 0; index < text.length; ++index)
		{
			var c = text[index];
			switch(c)
			{
				case ',':
					token = this.getToken(text, begin, index - begin);
                    currentLine.push(token);
					begin = index + 1;
					break;
				case '"':
					index = this.nextDoubleQuat(text, index + 1);
					break;
				case '\r':
					token = this.getToken(text, begin, index - begin);
					currentLine.push(token);
					begin = index + 2;
					result.push(currentLine);
					currentLine = new Array();
					break;
			}
		}
		return result;
	},

    // 文字トークンを抜き出す
    // \nで区切られる文字列が全角換算24文字を越える場合はさらに\n挿入
	getToken: function(text, begin, count) {
        var token
		if(text[begin] == '"') {
			token = text.substr(begin + 1, count - 2);
		} else {
			token = text.substr(begin, count);
        }

        token = this.cutString(token, 50)

        return token
	},

	// 次のダブルクォーテーションのインデックスを取得する
	nextDoubleQuat: function(text, index)
	{
		do {
			index = text.indexOf('"', index);
		} while(text[index - 1] == '\\');
		return index;
	},

    // 50バイトカット
	cutString: function(text, len)
	{
        String.prototype.bytes = function () {
            var length = 0;
            for (var i = 0; i < this.length; i++) {
              var c = this.charCodeAt(i);
              if ((c >= 0x0 && c < 0x81) || (c === 0xf8f0) || (c >= 0xff61 && c < 0xffa0) || (c >= 0xf8f1 && c < 0xf8f4)) {
                length += 1;
              } else {
                length += 2;
              }
            }
            return length;
        };

        var arr  = text.split("\n");
        var ret = ''
        for(let v of arr) {
            if (this.strLength(v) > len) {
                v = this.multByteInsert(v, len, '\n');
                v = this.cutString(v, len)
            }
            ret = ret.concat(v + '\n')
        }
        return ret.slice( 0, -1 ) ;

    },
    strLength: function( strSrc ){
        len = 0;
        strSrc = escape(strSrc);
        for(i = 0; i < strSrc.length; i++, len++){
            if(strSrc.charAt(i) == "%"){
                if(strSrc.charAt(++i) == "u"){
                    i += 3;
                    len++;
                }
                i++;
            }
        }
        return len;
    },    
    multByteInsert: function( str , strLimit, insStr ){
        
        var isSlice = false;
        var cutStr = str
    
        while( this.strLength(cutStr) > strLimit ){
            cutStr = cutStr.slice(0, cutStr.length-1);
            isSlice = true;
        }
    
        if( isSlice ){
            str = cutStr + insStr + str.slice(cutStr.length)
        }
    
        return str;
    }
};

//---------------------------------------------------------------------------------------------
// DataManager拡張
//---------------------------------------------------------------------------------------------
(function() {
	// 拡張子によって読み込むファイルを変更する
	const _DataManager_loadDataFile = DataManager.loadDataFile;
	DataManager.loadDataFile = function(name , src) {
		var extensionBegin = src.lastIndexOf('.');
		var extension = src.substr(extensionBegin, src.length - extensionBegin);
		if (extension == ".json") {
			_DataManager_loadDataFile.apply(this, arguments);
		} else {
			DataManager.loadCSVFile(name, src);
		}
	}

	// csv読み込み用拡張
	DataManager.loadCSVFile = function(name, src) {
		var xhr = new XMLHttpRequest();
		var url = 'data/' + src;
		xhr.open('GET', url);
		xhr.overrideMimeType('text/plain; charset=utf-8');
		xhr.onload = function() {
			if (xhr.status < 400) {
				window[name] = xhr.responseText;
			}
		};
		xhr.onerror = this._mapLoader || function() {
			DataManager._errorUrl = DataManager._errorUrl || url;
		};
		window[name] = null;
		xhr.send();
	};
})();


```