/**
* @file
* namespace stdtaskを定義し, よく使うタスクの雛形を事前に用意しておく.
*
* @author lenuser
*/
/**
* 頻繁に必要になり, かつGUIに直接依存しないタスクをまとめたnamespace.
* 以下の要素が外部に公開される.
* - stdtask.Coroutine
* - stdtask.wait
* - stdtask.Select
* - stdtask.CyclicSelect
* - stdtask.Scroll
* - stdtask.Meter
*
* @namespace
*/
var stdtask = stdtask || {};
(function(stdtask){
// #1. コルーチン
/**
* execute()を普通に実装する代わりにジェネレータを利用できるオブジェクト.
* コンストラクタの中などでuseCoroutine()メソッドを呼び出すと,
* 指定されたジェネレータ関数によって作られるジェネレータに
* このオブジェクトの更新処理を委任することができる.
*
* たとえば, ある決まった処理を1フレームに1回ずつ, 番号を増やしながら
* 順番に実行するタスクを作る場合は次のようなコードになる.
*
* ```
* // 使用例1
* class MyTask extends stdtask.Coroutine{
* constructor(){
* super();
* this.active = true;
* this.useCoroutine(this.chart);
* }
*
* *chart(opt){
* // execute()を1回呼び出すごとにループが1周ずつ進む
* for(let i = 0; i < 10; i++){
* my_periodic_task(i);
* yield true;
* }
*
* this.active = false; // 最後にタスクリストから除外してもらう
* }
* }
* ```
*
* また, 自分でオーバーライドしたexecute()の中から必要に応じて
* useCoroutine()を呼び出すこともできる. この場合, ジェネレータが完了した後は
* 元のexecute()の内容に戻る.
*
* ```
* // 使用例2
* class MyTask extends stdtask.Coroutine{
* constructor(){
* super();
* this.active = true;
* }
*
* execute(GE){
* if(GE.input.isJustPressed("Enter")){
* this.useCoroutine(this.chart, { GE: GE });
* }
* }
*
* *chart(opt){
* // execute()を1回呼び出すごとにループが1周ずつ進む
* for(let i = 0; i < 10; i++){
* my_periodic_task(opt.GE, i);
* yield true;
* }
*
* this.active = false; // 最後にタスクリストから除外してもらう
* }
* }
* ```
*
* @class
*/
stdtask.Coroutine = class{
/**
* 指定されたジェネレータ関数を使ってジェネレータを作り, このオブジェクトの
* 更新処理をこのジェネレータに委任する.
* 具体的には, このジェネレータを実行するだけの関数をthis.executeに代入する.
* ジェネレータの返り値がthis.executeの返り値として使われる.
* ジェネレータが完了したときは, このメソッドを実行する直前のexecuteの値に戻す.
* @param {GeneratorFunction} gen - 処理を委任するジェネレータ関数
* @param {Object.<*,*>} [opt={}] - ジェネレータ関数の初期化時に渡すオプション
*/
useCoroutine(gen, opt = {}){
const iter = gen.call(this, opt);
const backup = this.execute;
this.execute = (GE) => {
const result = iter.next();
if(result.done) this.execute = backup;
return result.value;
};
}
/**
* useCoroutine()を実行する前, および指定したジェネレータが完了した後に
* 使われるダミーのexecute()メソッド. 何もせずにtrueを返す.
* @param {stdgam.GameEngine} GE - タスク処理に用いるGameEngine
* @returns {boolean} 常にtrueを返す
*/
execute(GE){
return true;
}
}
/**
* 指定されたフレーム数だけ yield true または yield false を繰り返す
* ジェネレータを生成する. framesが0以下の場合は何もしない.
* @param {number} frames - 待機するフレーム数
* @param {boolean} [value=true] - ジェネレータが返す値
*/
stdtask.wait = function*(frames, value = true){
while(frames-- > 0) yield value;
}
// #2. コンポーネント
/**
* indexプロパティを持ち, キー入力に応じて
* - indexの増減
* - this.action(GE, index)の実行
* - this.cancel(GE, index)の実行
*
* を実行するタスクオブジェクトを実装する.
*
* ここで登場したindexプロパティは「複数の選択肢から1つを選ばせるUI」における
* 現在選択されている要素のインデックスを抽象化したものである.
*
* たとえば, 「矢印キーでindexを変化させてEnterで決定, ESCでキャンセル」という
* UIを作る場合,
*
* ```
* class MyUI extends stdtask.Select{
* constructor(){
* super(選択肢の個数, ["ArrowUp", "ArrowDown"], "Enter", "Escape");
* }
*
* action(GE, index){ 決定時の処理 }
* cancel(GE, index){ キャンセル時の処理 }
* }
* ```
*
* のようにすればよい. または, Selectを継承するのではなく,
*
* ```
* class MyUI{
* constructor(){
* this.select = new stdtask.Select(
* 選択肢の個数, ["ArrowUp", "ArrowDown"], "Enter", "Escape"
* );
* this.select.bind(this);
* }
*
* execute(GE){ return this.select.execute(GE); }
* action(GE, index){ 決定時の処理 }
* cancel(GE, index){ キャンセル時の処理 }
* }
* ```
*
* のように委譲してもよい.
*
* @class
* @prop {number} index - このオブジェクトが管理するパラメータ
* @prop {boolean} active - (stdgam.Sceneの意味で) このオブジェクトが有効か
*/
stdtask.Select = class{
#itemCount;
#busy;
#wait;
#codes1;
#codes2;
#modal;
/**
* 指定された設定に基づきインスタンスを生成する.
* @param {number} itemCount - 選択肢の個数. this.indexは 0 ~ (itemCount-1) の範囲を動く
* @param {string[]} dirKeys - this.indexの増減に使うキーのキーコード. dirKeys[0]が減少,
* dirKeys[1]が増加のキーとして扱われる
* @param {string} actionKey - このキーが押されたとき, this.action を実行する
* @param {string} cancelKey - このキーが押されたとき, this.cancel を実行する
* @param {number} [firstValue=0] - this.indexの初期値
* @param {number} [wait=10] - dirKeysに属するキーを押しっぱなしにしたとき,
* どの程度の間隔が空いていればキーイベントを受理するか指定する
* @param {boolean} [modal=true] - 自分より後ろのタスク処理をブロックするか
*/
constructor(itemCount, dirKeys, actionKey, cancelKey, firstValue = 0, wait = 10, modal = true){
this.#itemCount = itemCount;
this.#busy = 0;
this.#wait = wait;
this.#codes1 = dirKeys.concat([actionKey, cancelKey]);
this.#codes2 = [...dirKeys];
this.#modal = modal;
this.index = firstValue;
this.active = true;
}
#checkInput(GE){
if(this.#busy > 0) this.#busy--;
return GE.input.checkInput(this.#codes1, this.#codes2, this.#busy);
}
/**
* 項目数を変更する. これによりthis.indexの値が範囲外になる場合,
* this.indexを max(n-1, 0) に変更する.
* @param {number} n - 新しい項目数
*/
resize(n){
this.#itemCount = n;
if(this.index >= n) this.index = Math.max(n-1, 0);
}
/**
* 1フレーム分のタスク処理を実行する.
* 具体的には, キー入力に応じてthis.indexを増減させたり
* this.action/this.cancel を実行したりする.
*
* 【注意】this.action/this.canelを実行したフレームでは, modalの設定と
* 無関係に必ず false を返す. なぜならば, もしこれら処理の中で
* タスクリストの変更が発生した場合「forループの途中で中身を変更する」のと
* 同じ状況が生じるためである.
* 後続のタスク処理をブロックすることで安全にループを抜けることができる.
*
* @param {stdgam.GameEngine} GE - タスク処理に用いるGameEngine
* @returns 次のいずれかの条件を満たすとき false を返す
* 1. 自分より後ろのタスク処理をブロックする設定の場合
* 2. このフレームにおいてthis.action や this.cancel を実行した場合
*
* そうでないとき true を返す
*/
execute(GE){
const [code, k] = this.#checkInput(GE);
if(k >= 0) this.#busy = this.#wait;
if(k < 0) return !this.#modal;
if(k == 0 || k == 1){
this.move(k, this.#itemCount);
}
if(k == 2){
this.action(GE, this.index);
return false;
}
if(k == 3){
this.cancel(GE, this.index);
return false;
}
return !this.#modal;
}
/**
* k == 0 のとき, this.indexを1減らす (ただし0未満にはならない).
* k == 1 のとき, this.indexを1増やす (ただし「選択肢の個数-1」を超えない).
*/
move(k, itemCount){
if(k == 0 && this.index > 0) this.index--;
if(k == 1 && this.index < itemCount - 1) this.index++;
}
/**
* actionやcancelの実行を指定したオブジェクトに委任する.
* すなわち, 以下の処理を行う.
* - other.action(GE, index) を実行するだけの関数を this.action に代入
* - other.cancel(GE, index) を実行するだけの関数を this.cancel に代入
* @param {Object} other - action/cancelの処理を委任されるオブジェクト
*/
bind(other){
this.action = (GE, index) => { other.action(GE, index) };
this.cancel = (GE, index) => { other.cancel(GE, index) };
}
/**
* actionKeyとして指定したキーが押されたときに呼び出される.
* デフォルトでは何もしない.
* @param {stdgam.GameEngine} GE - タスク処理を実行するために使うGameEngine
* @param {number} index - this.indexの値
*/
action(GE, index){ }
/**
* cancelKeyとして指定したキーが押されたときに呼び出される.
* デフォルトでは何もしない.
* @param {stdgam.GameEngine} GE - タスク処理を実行するために使うGameEngine
* @param {number} index - this.indexの値
*/
cancel(GE, index){ }
}
/**
* indexをこれ以上減らせないときは取りうる最大値に,
* 逆にindexをこれ以上増やせないときは 0 にワープするようSelectを改変したもの.
* @class
* @prop {number} index - このオブジェクトが管理するパラメータ
* @prop {boolean} active - (stdgam.Sceneの意味で) このオブジェクトが有効か
* @extends stdtask.Select
*/
stdtask.CyclicSelect = class extends stdtask.Select{
/**
* (a + b) の値を m で割った余りを返す. ゼロ除算のチェックなどはしない.
* @param {number} a
* @param {number} b
* @param {number} m
* @returns {number} (a + b) % m の値
*/
addMod(a, b, m){
return (a + b + m) % m;
}
/**
* k == 0 のときはthis.indexを1減らし, k == 1 のときはthis.indexを1増やす.
* ただし, 結果が0未満になるときは「選択肢の個数-1」に変更し,
* 逆に結果が「選択肢の個数以上」になるときは0にする.
*/
move(k, itemCount){
this.index = this.addMod(this.index, 2 * k -1, itemCount);
};
}
/**
* 指定されたSelectオブジェクトをラップし, スクロール機能を追加するクラス.
* すなわち, indexの値をscroll + offset (0 <= offset < viewCount) に分解し,
* かつ, 直前の状態からの遷移が自然になるようにこれらの値を更新する.
*
* たとえば「optionsの要素のうち一度に表示できるものの個数が最大5個」であるような
* UIを作りたい場合, 次のようなコードを書く.
*
* ```
* class MyUI extends stdtask.Scroll{
* constructor(options){
* const select = new stdtask.Select(
* options.length, ["ArrowUp", "ArrowDown"], "Enter", "Escape"
* );
* super(select, 5);
* this.options = options;
* }
*
* draw(GE, ctx){
* for(let i = 0; i < 5; i++){
* const targetIndex = this.scroll + i;
* my_paint_operation( this.options[targetIndex] ); // 普通はiも使うはずだが
* }
* }
*
* action(GE, index){ 決定時の処理 }
* cancel(GE, index){ キャンセル時の処理 }
* }
* ```
*
* 選択肢の個数が表示できる最大数に満たない場合, 普通にSelectを使うのと変わらない.
* また, SelectではなくCyclicSelectを渡してもよい.
* @class
* @prop {number} scroll - 現在のスクロール量
* @prop {number} offset - 現在の表示区間において選択されている要素が何番目にあるか.
* より正確には selectObj.index == this.scroll + this.offset を満たす整数である
* (selectObjはコンストラクタで与えたSelectオブジェクト)
* @prop {boolean} active - (stdgam.Sceneの意味で) このオブジェクトが有効か
*/
stdtask.Scroll = class{
#contents;
#viewCount;
/**
* 一度に表示できる選択肢の個数がviewCountであるようなインスタンスを作る.
* @param {stdtask.Select} selectObj - ラップするSelectオブジェクト (CyclicSelectなどでも可)
* @param {number} viewCount - 一度に表示できる選択肢の個数
*/
constructor(selectObj, viewCount){
this.#contents = selectObj;
this.#viewCount = viewCount;
this.initScroll();
selectObj.bind(this);
this.active = true;
}
/**
* @returns {number} 一度に表示できる選択肢の個数
*/
viewCount(){
return this.#viewCount;
}
/**
* 1フレーム分のタスク処理を行う.
* より正確には, ラップされているSelectオブジェクトのexecuteを実行した後,
* その結果に基づき this.scroll と this.offset を更新する.
* @param {stdgam.GameEngine} GE - タスク処理に用いるGameEngine
*/
execute(GE){
const f = this.#contents.execute(GE);
this.#updateScroll(this.#contents.index - this.scroll - this.offset);
return f;
}
/**
* ラップされているSelectオブジェクトのindexの値を元に,
* this.scroll と this.offset の値を初期化する.
*
* 具体的には次のように決める.
* 1. indexの値がviewCount未満なら, scroll = 0, offset = index でよい.
* 2. そうでないとき, 先に offset = viewCount - 1 を決定してしまい,
*
* それから scroll = index - offset とすればよい.
*/
initScroll(){
this.scroll = this.#contents.index - this.#viewCount + 1;
if(this.scroll <= 0){
this.scroll = 0;
this.offset = this.#contents.index;
}
else{
this.offset = this.#viewCount - 1;
}
}
/**
* 項目数を変更する. これによりindexの値が範囲外になる場合,
* indexを max(n-1, 0) に変更し, それに合うように this.scroll, this.offset を調整する.
* @param {number} n - 新しい項目数
*/
resize(n){
this.#contents.resize(n);
const d = this.scroll + this.offset - this.#contents.index;
if(d > 0){
if(d <= this.offset) this.offset -= d;
else{
this.offset = 0;
this.scroll = this.#contents.index;
}
}
}
// contents.indexの値の変化量から自身の次の状態を決める.
#updateScroll(d){
if(d > 1 || d < -1){
this.initScroll(); // 先頭と末尾の間の移動が発生した
}
if(d == -1){
if(this.offset > 0) this.offset--;
else this.scroll--;
}
if(d == 1){
if(this.offset < this.#viewCount - 1) this.offset++;
else this.scroll++;
}
}
/**
* actionやcancelの実行を指定したオブジェクトに委任する.
* すなわち, 以下の処理を行う.
* - other.action(GE, index) を実行するだけの関数を this.action に代入
* - other.cancel(GE, index) を実行するだけの関数を this.cancel に代入
* @param {Object} other - action/cancelの処理を委任されるオブジェクト
*/
bind(other){
this.action = (GE, index) => { other.action(GE, index) };
this.cancel = (GE, index) => { other.cancel(GE, index) };
}
/**
* actionKeyとして指定したキーが押されたときに呼び出される.
* デフォルトでは何もしない.
* @param {stdgam.GameEngine} GE - タスク処理を実行するために使うGameEngine
* @param {number} index - this.indexの値
*/
action(GE, index){ }
/**
* cancelKeyとして指定したキーが押されたときに呼び出される.
* デフォルトでは何もしない.
* @param {stdgam.GameEngine} GE - タスク処理を実行するために使うGameEngine
* @param {number} index - this.indexの値
*/
cancel(GE, index){ }
}
/**
* 数値の等速変化を実現するMeterオブジェクトを作る.
* あるパラメータがAからBへ変化するとき, モデル内部では一瞬で値がBになるが,
* GUIではAからBまで一定の時間を掛けて変化する様子を描画したい.
* この時間変化を表現するために使う.
* @class
* @prop {number} value - そのパラメータの現在値
* @prop {number} max - パラメータの最大値
* @prop {number} frames - 等速変化にかけるフレーム数 (既に実行中のアクションには影響しない)
* @prop {boolean} active - (stdgam.Sceneの意味で) このオブジェクトが有効か
*/
stdtask.Meter = class{
#sv;
#tv;
#duration;
#elapsed;
/**
* 指定された値を初期値・最大値とするインスタンスを生成する.
* @param {number} v - パラメータの初期値
* @param {numner} max - パラメータの最大値
* @param {number} frames - 等速変化にかけるフレーム数 (既に実行中のアクションには影響しない)
*/
constructor(v, max, frames){
this.init(v, max, frames);
}
/**
* 指定された値を使って初期化する.
* もしnewFramesが偽の場合, 現在のframesの値をそのまま保持する.
* @param {number} newValue - パラメータの初期値
* @param {numner} newMax - パラメータの最大値
* @param {number} [frames=null] - 等速変化にかけるフレーム数 (既に実行中のアクションには影響しない).
* 偽として判定される値を渡した場合は現在の値を保持する
*/
init(newValue, newMax, newFrames = null){
this.value = this.#sv = this.#tv = newValue;
this.max = newMax;
this.frames = newFrames || this.frames;
this.#duration = this.#elapsed = 0;
this.active = true;
}
/**
* 等速変化の実行中かどうか調べる.
* @returns {boolean} 変化中ならtrue
*/
isChainging(){
return (this.#duration != this.#elapsed);
}
/**
* 等速変化の実行中の場合, それを開始する直前の値を返す.
* そうでないとき, 単に現在値を返す.
* @returns 等速変化中はそれを開始する直前の値, そうでないとき現在の値
*/
startingPoint(){
if(this.#duration != this.#elapsed) return this.#sv;
else return this.value;
}
/**
* 目標値への変化を開始する.
* @param {number} target - 等速変化の目標値
* @param {number} [dur=this.frames] - 等速変化にかけるフレーム数 (既に実行中のアクションには影響しない).
* 省略した場合はthis.framesの値をそのまま使う
*/
changeTo(target, dur = this.frames){
this.#sv = this.value;
this.#tv = target;
this.#duration = dur;
this.#elapsed = 0;
this.active = true;
}
/**
* 1フレーム分のタスク処理を実行する.
* @param {stdgam.GameEngine} GE - このタスク処理に用いるGameEngine
*/
execute(GE){
if(this.#elapsed < this.#duration){
this.#elapsed++;
// 残りのフレーム数を全体フレーム数で割った比を求める.
// ただし, 残りがないときは厳密に 0 になるようにする.
let t = 0;
if(this.#elapsed < this.#duration){
t = (this.#duration - this.#elapsed) / this.#duration;
}
// ゴールから逆算して現在値を決める.
const v = Math.floor(this.#tv - (this.#tv - this.#sv) * t);
this.value = Math.max(0, Math.min(this.max, v));
}
return true;
}
}
})(stdtask);
Source