Source

config.js

/**
 * @file
 * namsspace configを定義し, その中にconfigScene, configScene2を実装する.
 *
 * @author lenuser
 */

/*
 * configScene, configScene2の構成要素をまとめたnamespace.
 * 以下の要素が外部に公開される.
 * - config.configScene
 * - config.configScene2
 * - config.SideboardDialog
 *
 * @namespace
 */
var config = config || {};
(function(Public){

// #1. 各項目の設定ダイアログの実装

/**
 * cardlist.jsの内容をキャラ別に分割するクラス.
 * RAW_CARD_DATAの各要素をそのまま使うので注意.
 * @class
 */
let Prism = class{
    /**
     * それぞれの分類に付ける名前
     * @type {string[]}
     */
    static labels = PrimitiveSuits.map((e) => CharacterNames[e]).concat(["多人数"]);

    /**
     * RAW_CARD_DATAの要素をキャラクター別に分類して, 結果をthis.subsetsに代入する.
     * 具体的には, Prism.labels[k] に属する要素の集合がthis.subsets[k] であるような
     * 配列をthis.subsetsに保持する.
     * ただし, 多人数カードはすべて「多人数」という1つの分類にまとめる.
     */
    constructor(){
        this.subsets = [];
        for(let i = 0; i < Prism.labels.length; i++){
            this.subsets.push([]);
        }
        for(const data of RAW_CARD_DATA){
            const m = PrimitiveSuits.indexOf(data.suit_string);
            if(m >= 0) this.subsets[m].push(data);
            else this.subsets.at(-1).push(data);
        }
    }
}

/**
 * 各項目の設定変更ダイアログを実装するための土台を提供する.
 * @class
 */
let DialogBase = class{
    static font = "23px Sans-Serif";
    static color = "white";
    static bgColor = "rgb(0,0,0,0.8)";
    static padLeft = 50;
    static padBottom = 20;

    constructor(owner, defaultValue, x, y, w, h, footnote = null){
        this.owner = owner;
        this.result = defaultValue;
        this.footnote = footnote || "矢印キーで項目を選んで A で決定";
        this.x = x;
        this.y = y;
        this.width = w;
        this.height = h;
        this.active = true;
    }

    draw(GE, ctx){
        ctx.save();
        ctx.fillStyle = DialogBase.bgColor;
        ctx.fillRect(this.x, this.y, this.width, this.height);

        ctx.font = DialogBase.font;
        ctx.fillStyle = DialogBase.color;
        ctx.fillText(this.footnote, this.x + DialogBase.padLeft,
                                    this.y + this.height - DialogBase.padBottom);
        ctx.restore();
    }
}

/**
 * メインカード選択ダイアログを実装する.
 * @class
 */
let MainCardDialog = class extends DialogBase{
    constructor(owner, defaultValue, x, y, w, h){
        super(owner, defaultValue, x, y, w, h, "← or → で項目を選んで A で決定");
        this.prism = new Prism();
        this.categorySelect = new stdtask.CyclicSelect(
            Prism.labels.length, ["ArrowLeft", "ArrowRight"], "KeyA", "KeyS", 0, 10
        );
        this.categorySelect.bind(this);
    }

    draw(GE, ctx){
        super.draw(GE, ctx);
        ctx.save();
        ctx.font = DialogBase.font;
        ctx.fillStyle = DialogBase.color;

        const m = this.categorySelect.index;
        if(!this.cardSelect){
            ctx.fillText(`分類: 《 ${Prism.labels[m]} 》`, this.x + 50, this.y+70);
        }
        else{
            const d = this.prism.subsets[m][this.cardSelect.index];
            ctx.fillText(`分類:   ${Prism.labels[m]}`, this.x + 50, this.y+70);
            ctx.fillText("カード:", this.x + 50, this.y+110);
            ctx.fillText(`《 ${d.id} ${d.character} 》`, this.x + 140, this.y+110);
            ctx.fillText(`HP ${d.HP}, MP ${d.MP}`, this.x + 143, this.y+150);
            if(d.main){
                ctx.fillText(`メインスキル『${d.main.name}』`, this.x + 143, this.y+190);
                ctx.fillText(" " + d.main.desc, this.x + 143, this.y + 230);
            }
        }
        ctx.restore();
    }

    execute(GE){
        return (this.cardSelect || this.categorySelect).execute(GE);
    }

    action(GE, n){
        const s = this.prism.subsets[n];
        if(s.length > 0){
            this.cardSelect = new stdtask.CyclicSelect(
                s.length, ["ArrowLeft", "ArrowRight"], "KeyA", "KeyS", 0, 10
            );
            this.cardSelect.bind(this);
            const backup = [ this.action, this.cancel ];
            this.action = (GE, n) => {
                this.result = this.prism.subsets[this.categorySelect.index][this.cardSelect.index];
                this.active = false;
            };
            this.cancel = (GE, n) => {
                this.cardSelect = null;
                this.action = backup[0];
                this.cancel = backup[1];
            };
        }
        else{
            const obj = T.finite(T.text(
                "この分類は空です", {x: this.x + 140, y: this.y + 190, font: DialogBase.font}
            ), 30);
            this.owner.addSprite(obj);
            this.owner.addTask(obj, true);
        }
    }

    cancel(GE, n){
        this.active = false;
    }
}

/**
 * 選択肢を縦に並べて表示し, その中から1つを選択させるダイアログを実装する.
 * 選択肢群の前に1行だけラベルを表示できる. また, ダイアログの下部に
 * footnoteを表示できる.
 * @class
 */
let SimpleChoice = class extends DialogBase{
    static step = 50;

    constructor(owner, label, options, defaultValue, x, y, w, h){
        super(owner, defaultValue, x, y, w, h, "↑ or ↓ で項目を選んで A で決定");
        this.label = label;
        this.options = options;
        this.active = true;

        this.select = new stdtask.Select(
            options.length, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS", defaultValue, 10
        );
        this.select.bind(this);
    }

    draw(GE, ctx){
        super.draw(GE, ctx);
        ctx.save();
        ctx.font = DialogBase.font;
        ctx.fillStyle = DialogBase.color;
        ctx.fillText(this.label, this.x + 50, this.y + 70);

        const p = ctx.measureText("→ ").width;
        for(let i = 0; i < this.options.length; i++){
            const y = this.y + 120 + SimpleChoice.step * i;
            if(i == this.select.index){
                ctx.fillText("→ " + this.options[i], this.x + 100, y);
            }
            else{
                ctx.fillText(this.options[i], this.x + 100 + p, y);
            }
        }
        ctx.restore();
    }

    execute(GE){
        return this.select.execute(GE);
    }

    action(){
        this.result = this.select.index;
        this.active = false; // 選択決定
    }

    cancel(){
        this.active = false;  // キャンセルして戻る
    }
}

/**
 * localStorage設定のダイアログを実装する.
 * SimpleChoiceに似ているが, 独自の確認事項があるので別途用意する.
 * @class
 */
let LocalStorageDialog = class extends DialogBase{
    constructor(owner, defaultValue, x, y, w, h){
        super(owner, defaultValue, x, y, w, h, "↑ or ↓ で項目を選んで A で決定");
        this.select = new stdtask.Select(
            2, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS", defaultValue, 10
        );
        this.select.bind(this);
        this.active = true;
    }

    draw(GE, ctx){
        super.draw(GE, ctx);
        ctx.save();
        ctx.font = DialogBase.font;
        ctx.fillStyle = DialogBase.color;
        ctx.fillText("設定を保存するためにlocalStorageを使用しますか?", this.x + 50, this.y + 70);

        ctx.fillStyle = "orange";
        ctx.fillText("【注意】「使用しない」を選択した場合、", this.x + 45, this.y + 110);
        ctx.fillText("使用中のlocalStorageは即座に消去されます!", this.x + 50, this.y + 150);

        ctx.fillStyle = DialogBase.color;
        const p = ctx.measureText("→ ").width;
        if(this.select.index == 0){
            ctx.fillText("→ 使用しない", this.x + 100, this.y + 220);
            ctx.fillText("使用する", this.x + 100 + p, this.y + 270);
         }
         else{
            ctx.fillText("使用しない", this.x + 100 + p, this.y + 220);
            ctx.fillText("→ 使用する", this.x + 100, this.y + 270);
         }
        ctx.restore();
    }

    execute(GE){
        return this.select.execute(GE);
    }

    action(){
        this.result = this.select.index;
        this.active = false; // 選択決定
    }

    cancel(){
        this.active = false;  // キャンセルして戻る
    }
}


// #2. 追加スキャン用のダイアログの実装

/*
 * 追加スキャンを実装するときにサイドボードのカードを選択する必要があるので,
 * このnamespaceで作っておく.
 */

/**
 * mainSceneで追加スキャンのために使うダイアログの実装.
 * 使用するのは別のシーンだが, 「カードライブラリから1枚を選ぶ」という操作が
 * メインカードの選択と酷似しているので, 実装の効率化や見た目の統一ために
 * ここで定義して外部に提供する.
 * @class
 */
Public.SideboardDialog = class extends DialogBase{
    static labels = PrimitiveSuits.map((e) => CharacterNames[e]).concat(["多人数"]);

    constructor(owner, sideboardObj, defaultValue, x, y, w, h){
        super(owner, defaultValue, x, y, w, h, "← or → で項目を選んで A で決定");
        this.initPartition(sideboardObj);
        this.prevDesc = null;
        this.lines = null;

        this.categorySelect = new stdtask.CyclicSelect(
            this.subsets.length, ["ArrowLeft", "ArrowRight"], "KeyA", "KeyS", 0, 10
        );
        this.categorySelect.bind(this);
    }

    // sideboardObjはDeckなので微妙に処理が違う
    initPartition(sideboardObj){
        this.subsets = Public.SideboardDialog.labels.map((e) => []);
        for(const card of sideboardObj.cards()){
            const m = card.mark;
            if(m < Public.SideboardDialog.labels.length - 1) this.subsets[m].push(card);
            else this.subsets.at(-1).push(card);
        }
    }

    draw(GE, ctx){
        super.draw(GE, ctx);
        ctx.save();
        ctx.font = DialogBase.font;
        ctx.fillStyle = DialogBase.color;

        const m = this.categorySelect.index;
        const label = Public.SideboardDialog.labels[m];
        if(!this.cardSelect){
            ctx.fillText(`分類: 《 ${label} 》`, this.x + 50, this.y+70);
        }
        else{
            const card = this.subsets[m][this.cardSelect.index];
            ctx.fillText(`分類:   ${label}`, this.x + 50, this.y+70);
            ctx.fillText("カード:", this.x + 50, this.y+110);
            ctx.fillText(`《 ${getCharacterName(Suits[card.mark])} ${card.value} /  MP ${card.MP} 》`, this.x + 140, this.y+110);
            if(card.skill){
                ctx.fillText(`サブスキル 『${card.skill.caption}』`, this.x + 50, this.y + 150);
                if(!this.lines || this.prevDesc != card.skill.desc){
                    this.lines = card.skill.desc.split("\n");
                    this.prevDesc = card.skill.desc;
                }
                for(let i = 0; i < this.lines.length; i++){
                    ctx.fillText(this.lines[i], this.x + 50, this.y + 190 + 40*i);
                }
            }
        }
        ctx.restore();
    }

    execute(GE){
        return (this.cardSelect || this.categorySelect).execute(GE);
    }

    action(GE, n){
        const s = this.subsets[n];
        if(s.length > 0){
            this.cardSelect = new stdtask.CyclicSelect(
                s.length, ["ArrowLeft", "ArrowRight"], "KeyA", "KeyS", 0, 10
            );
            this.cardSelect.bind(this);
            const backup = [ this.action, this.cancel ];
            this.action = (GE, n) => {
                this.result = this.subsets[this.categorySelect.index][n];
                this.active = false;
            };
            this.cancel = (GE, n) => {
                this.cardSelect = null;
                this.action = backup[0];
                this.cancel = backup[1];
            };
        }
        else{
            const obj = T.finite(T.text(
                "この分類は空です", {x: this.x + 140, y: this.y + 190, font: DialogBase.font}
            ), 30);
            this.owner.addSprite(obj);
            this.owner.addTask(obj, true);
        }
    }

    cancel(GE, n){
        this.active = false;
    }
}


// #3. configScene, configScene2の実装

/**
 * コンフィグ画面を実装するSceneオブジェクト.
 * @type {stdgam.Scene}
 */
Public.configScene = new stdgam.Scene({
saveConfig(){
    LocalStorageInfo.saveConfig(this.setting.mainCardData.id, this.setting.chainRule, Card.MarkerFlag, this.setting.QBChance);
},

displayObject: {
    x: 340,
    y: 150,
    paint(ctx, mainCard){
        ctx.save();
        ctx.strokeStyle = "rgb(255,255,255,0.5)";
        ctx.lineWidth = 5;
        ctx.strokeRect(this.x, this.y-40, 600, 30*4+40);
        ctx.fillStyle = "rgb(0,0,0,0.5)";
        ctx.fillRect(this.x, this.y-40, 600, 30*4+40);

        ctx.fillStyle = "white";
        ctx.font = "22px Sans-Serif";
        ctx.fillText(`${mainCard.id} ${mainCard.character}`,
                     this.x + 20, this.y);
        ctx.fillText(`HP ${mainCard.HP},  MP ${mainCard.MP}`,
                     this.x + 20, this.y+30);
        if(mainCard.main){
            ctx.fillText(`メインスキル 『${mainCard.main.name}』`, this.x + 20, this.y + 60);
            ctx.fillText(" " + mainCard.main.desc, this.x + 20, this.y + 90);
        }
        ctx.restore();
    }
},

operations: [
    function*(GE, opt){
        const dialog = new MainCardDialog(
            opt.scene, opt.scene.setting.mainCardData, 200, 140, 600, 420
        );
        opt.scene.addSprite(dialog);
        opt.scene.addTask(dialog, true);
        while(dialog.active) yield true;

        opt.scene.setting.mainCardData = dialog.result;
        if(LocalStorageInfo.isUsed()){
            opt.scene.saveConfig();
        }
    },
    function*(GE, opt){
        const dialog = new LocalStorageDialog(
            opt.scene, LocalStorageInfo.isUsed() ? 1 : 0, 200, 140, 600, 420
        );
        opt.scene.addSprite(dialog);
        opt.scene.addTask(dialog, true);
        while(dialog.active) yield true;

        if(dialog.result == 1){
            opt.scene.saveConfig();
            LocalStorageInfo.saveDeck(opt.scene.deck);
        }
        else if(LocalStorageInfo.isUsed()){
            LocalStorageInfo.removeStorage();
        }
    },
    function*(GE, opt){
        const dialog = new SimpleChoice(
            opt.scene, "コンボ成立条件", ["第1弾ルール", "第2弾ルール"],
            opt.scene.setting.chainRule-1, 200, 140, 600, 420
        );
        opt.scene.addSprite(dialog);
        opt.scene.addTask(dialog, true);
        while(dialog.active) yield true;

        opt.scene.setting.chainRule = dialog.result + 1;
        if(LocalStorageInfo.isUsed()){
            opt.scene.saveConfig();
        }
    }
],

onLoad(GE, setting){
    this.setting = setting;
    this.deck = setting.deckSet.cards();  // localStorageを使うとき、すぐに保存するため必要

    this.add(T.image(GE.caches.get("BACKGROUND"), {x: 0, y: 0}));
    this.add(T.text("メインカードを変更する", {x: 90, y:90, font: "32px Sans-Serif"}));
    this.add(T.text("現在の設定:", {x: 160, y:133, font: "24px Sans-Serif"}));

    this.add(T.text("localStorageの設定を変更する", {x: 90, y:330, font: "32px Sans-Serif"}));

    let tmp = T.text("現在の設定:  ", {x: 160, y:373, font: "24px Sans-Serif"});
    tmp.execute = (GE) => {
        tmp.text = LocalStorageInfo.isUsed() ? "現在の設定: 使用する" : "現在の設定: 使用しない";
        return true;
    };
    this.add(tmp);

    this.add(T.text("コンボ成立条件の設定を変更する", {x: 90, y:510, font: "32px Sans-Serif"}));
    this.add(T.ftext("現在の設定:  第${}弾ルール", this.setting, "chainRule", {x: 160, y:553, font: "24px Sans-Serif"}));

    this.itemY = [ 90, 330, 510 ];
    this.select = new stdtask.CyclicSelect(
        this.itemY.length, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS", 0, 10
    );
    this.select.bind(this);

    const mainCardView = {
        owner: this,
        draw(GE, ctx){ this.owner.displayObject.paint(ctx, this.owner.setting.mainCardData); }
    };
    this.add(mainCardView);
},

draw(GE, ctx){
    ctx.save();
    ctx.font = "32px Sans-Serif";
    ctx.fillStyle = "white";
    ctx.fillText("→ ", 50, this.itemY[this.select.index]);
    ctx.restore();
},

execute(GE){
    this.select.execute(GE);
},

action(GE, n){
    this.useCoroutine(GE, this.operations[this.select.index], {scene: this});
},

cancel(GE, n){
    GE.changeScene("select");
}

});

/**
 * 特殊設定のコンフィグ画面を実装するSceneオブジェクト.
 * @type {stdgam.Scene}
 */
Public.configScene2 = new stdgam.Scene({
saveConfig(){
    LocalStorageInfo.saveConfig(this.setting.mainCardData.id, this.setting.chainRule, Card.MarkerFlag, this.setting.QBChance);
},

operations: [
    function*(GE, opt){
        const dialog = new SimpleChoice(
            opt.scene, "スキル持ちカードのマーカー表示をしますか?",
            ["表示する", "表示しない"],
            Card.MarkerFlag ? 0 : 1, 200, 140, 600, 420
        );
        opt.scene.addSprite(dialog);
        opt.scene.addTask(dialog, true);
        while(dialog.active) yield true;

        Card.MarkerFlag = (dialog.result == 0);
        if(LocalStorageInfo.isUsed()){
            opt.scene.saveConfig();
        }
    },

    function*(GE, opt){
        const dialog = new SimpleChoice(
            opt.scene, "キュゥべえチャンスのON/OFFを選択してください",
            ["ON", "OFF"],
            this.setting.QBChance ? 0 : 1, 200, 140, 600, 420
        );
        opt.scene.addSprite(dialog);
        opt.scene.addTask(dialog, true);
        while(dialog.active) yield true;

        this.setting.QBChance = (dialog.result == 0);
        if(LocalStorageInfo.isUsed()){
            opt.scene.saveConfig();
        }
    }
],

onLoad(GE, setting){
    this.setting = setting;
    this.deck = setting.deckSet.cards();  // localStorageを使うとき, すぐに保存するため必要

    this.add(T.image(GE.caches.get("BACKGROUND"), {x: 0, y: 0}));

    this.add(T.text("スキル持ちカードのマーカー表示", {x: 90, y:90, font: "32px Sans-Serif"}));
    let tmp = T.text("現在の設定:  ", {x: 160, y:133, font: "24px Sans-Serif"});
    tmp.execute = (GE) => {
        tmp.text = Card.MarkerFlag ? "現在の設定: 表示する" : "現在の設定: 表示しない";
        return true;
    };
    this.add(tmp);

    this.add(T.text("キュゥべえチャンスのON/OFF", {x: 90, y:270, font: "32px Sans-Serif"}));
    let tmp2 = T.text("現在の設定:  ", {x: 160, y:313, font: "24px Sans-Serif"});
    tmp2.execute = (GE) => {
        tmp2.text = this.setting.QBChance ? "現在の設定: ON" : "現在の設定: OFF";
        return true;
    };
    this.add(tmp2);

    this.itemY = [ 90, 270 ];
    this.select = new stdtask.CyclicSelect(
        this.itemY.length, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS", 0, 10
    );
    this.select.bind(this);
},

draw(GE, ctx){
    ctx.save();
    ctx.font = "32px Sans-Serif";
    ctx.fillStyle = "white";
    ctx.fillText("→ ", 50, this.itemY[this.select.index]);
    ctx.restore();
},

execute(GE){
    this.select.execute(GE);
},

action(GE, n){
    this.useCoroutine(GE, this.operations[this.select.index], {scene: this});
},

cancel(GE, n){
    GE.changeScene("select");
}

});

})(config);