Source

edit.js

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

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

// #1. カードを展示するコンポーネント

/**
 * CardPanelで利用するカーソルを実装するクラス.
 * @class
 * @prop {number} x
 * @prop {number} y
 * @extends stdtask.Scroll
 */
let CardCapture = class extends stdtask.Scroll{
    #owner;
    #colCount;
    #descPainter;

    /**
     * @param {CardPanel} owner
     * @param {number} colCount;
     * @param {number} x
     * @param {number} y
     * @param {function(CanvasRenderingContext2D, Card):void} dp
     */
    constructor(owner, colCount, x, y, dp){
        const select = new stdtask.Select(
            owner.cardCount(), ["ArrowLeft", "ArrowRight"], "KeyA", "KeyS"
        );
        super(select, colCount);

        this.#owner = owner;
        this.#colCount = colCount;
        this.x = x;
        this.y = y;
        this.#descPainter = dp;
    }

    selectedIndex(){
        return this.scroll + this.offset;
    }

    draw(GE, ctx){
        if(this.active && this.#owner.cardCount() > 0){
            ctx.save();
            const x = this.x - 10 + this.offset * Card.width * 1.3;
            const y = this.y - 10;
            ctx.strokeStyle = "rgba(255,130,130,0.6)";
            ctx.lineWidth = 10;
            ctx.strokeRect(x, y, Card.width + 20, Card.height + 20);
            ctx.fillStyle = "white";

            if(this.#descPainter){
                const card = this.#owner.watch(this.selectedIndex());
                this.#descPainter.paint(ctx, card);
            }
            ctx.restore();
        }
    }

    action(GE, n){
        this.#owner.action();
        this.resize(this.#owner.cardCount());
    }

    cancel(GE, n){
        this.#owner.cancel();
    }
}

/**
 * 指定されたCardSetの内容を横一列に展示するコンポーネント.
 * 表示に加えて, カードの選択機能も担当する.
 * 対象のCardSetの内容を動的に監視するので, カードの追加・削除は
 * 単にCardSet自体を編集すればよい.
 * @class
 * @prop {number} x
 * @prop {number} y
 * @prop {boolean} showAlways
 * @prop {boolean} active
 */
let CardPanel = class{
    #owner;
    #deckSet;
    #colCount;
    #capture;

    constructor(owner, deckSet, colCount, x, y, showAlways = true){
        this.#owner = owner;
        this.#deckSet = deckSet;
        this.#colCount = colCount;
        this.x = x;
        this.y = y;
        this.#capture = null;
        this.showAlways = showAlways;
        this.active = true;
    }

    cardCount(){
        return this.#deckSet.size();
    }

    watch(index){
        return this.#deckSet.watch(index);
    }

    isCaptureMode(){
        return !(!this.#capture);
    }

    changeDeck(deckSet){
        if(!this.isCaptureMode()){
            this.#deckSet = deckSet;
        }
    }

    beginCaptureMode(dp){
        if(this.#deckSet.size() == 0 || this.#capture){
            return false;
        }
        else{
            this.#capture = new CardCapture(this, this.#colCount, this.x, this.y, dp);
            this.#owner.addSprite(this.#capture);
            this.#owner.addTask(this.#capture, true);
            return true;
        }
    }

    action(){
        const index = this.#capture.selectedIndex();
        const card = this.#deckSet.slice(index);
        this.#owner.cardPicked(this, card);
        if(this.#deckSet.size() == 0){
            this.cancel();
        }
    }

    cancel(){
        this.#capture.active = false;
        this.#capture = null;
    }

    draw(GE, ctx){
        if(!this.showAlways && !this.isCaptureMode()) return;
        ctx.save();
        const w = Card.width*this.#colCount*1.3-Card.width*0.3+40;
        ctx.strokeStyle = "rgb(0,20,70,0.5)";
        ctx.lineWidth = 5;
        ctx.strokeRect(this.x-20, this.y-20, w, Card.height+40);
        ctx.fillStyle = "rgba(0,0,0,0.5)";
        ctx.fillRect(this.x-20, this.y-20, w, Card.height+40);
        const scroll = this.#capture ? this.#capture.scroll : 0;
        if(this.#deckSet.size() == 0){
            ctx.fillStyle = "white";
            ctx.font = "27px Sans-Serif";
            ctx.fillText("カードがありません", this.x, this.y + 20);
        }
        else{
            for(let i = 0; i < this.#colCount; i++){
                const x = this.x + i*Card.width*1.3;
                const y = this.y;
                const card = this.#deckSet.watch(scroll + i);
                card.paint(GE, ctx, x, y);
            }
        }
        ctx.restore();
    }
}

/**
 * CardPanel2Dで利用するカーソルを実装するクラス.
 * @class
 * @prop {number} x
 * @prop {number} y
 * @prop {number} scroll
 * @prop {number} r
 * @prop {number} c
 * @prop {boolean} active
 */
let CardCapture2D = class {
    #select;
    #owner;
    #itemCount;
    #rowCount;
    #colCount;
    #busy;
    #descPainter;

    /**
     * @param {CardPanel2D} owner
     * @param {number} rowCount;
     * @param {number} colCount;
     * @param {number} x
     * @param {number} y
     * @param {function(CanvasRenderingContext2D, Card):void} dp
     */
    constructor(owner, rowCount, colCount, x, y, dp){
        this.#select = new stdtask.Select(owner.cardCount(), ["", ""], "", "");
        this.#owner = owner;
        this.#itemCount = owner.cardCount();
        this.#rowCount = rowCount;
        this.#colCount = colCount;
        this.#busy = 0;
        this.#descPainter = dp;
        this.x = x;
        this.y = y;
        this.scroll = this.r = this.c = 0;
        this.active = true;
    }

    selectedIndex(){
        return this.#select.index;
    }

    canMove(n){
        return ((this.scroll + this.r) * this.#colCount + n >= this.#itemCount);
    }

    move(k, n){
        if(k == 0 && this.#select.index < n) return;
        if(k == 1 && this.canMove(n)) return;
        for( ; n > 0; n--) this.#select.move(k, this.#itemCount);
        this.#updateState();
    }

    #updateState(){
        this.#itemCount = this.#owner.cardCount();
        this.#select.resize(this.#itemCount);
        const quo = Math.floor(this.#select.index / this.#colCount);
        while(quo < this.scroll + this.r){
            if(this.r > 0) this.r--;
            else this.scroll--;
        }
        while(quo > this.scroll + this.r){
            if(this.r < this.#rowCount - 1) this.r++;
            else this.scroll++;
        }
        this.c = this.#select.index % this.#colCount;
    }

    #checkInput(GE){
        if(this.#busy > 0) this.#busy--;
        const codes1 = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "KeyA", "KeyS"];
        const codes2 = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"];
        return GE.input.checkInput(codes1, codes2, this.#busy);
    }

    execute(GE){
        const k = this.#checkInput(GE)[1];
        if(k >= 0) this.#busy = 10;
        if(k == 0 || k == 1){
            this.move(k, this.#colCount);
        }
        if(k == 2 || k == 3){
            this.move(k - 2, 1);
        }
        if(k == 4){
            this.#owner.action();
            this.#updateState();
        }
        if(k == 5){
            this.#owner.cancel();
        }
        return false;
    }

    draw(GE, ctx){
        if(this.active){
            ctx.save();
            const x = this.x - 10 + this.c*Card.width*1.3;
            const y = this.y - 10 + this.r*Card.height*1.2;
            ctx.strokeStyle = "rgba(255,130,130,0.6)";
            ctx.lineWidth = 10;
            ctx.strokeRect(x, y, Card.width + 20, Card.height + 20);
            ctx.fillStyle = "white";

            if(this.#descPainter){
                const card = this.#owner.watch(this.selectedIndex());
                this.#descPainter.paint(ctx, card);
            }
            ctx.restore();
        }
    }
}

/**
 * 指定されたCardSetの内容を縦・横に並べて展示するコンポーネント.
 * 表示に加えて, カードの選択機能も担当する.
 * 対象のCardSetの内容を動的に監視するので, カードの追加・削除は
 * 単にCardSet自体を編集すればよい.
 * @class
 */
let CardPanel2D = class{
    constructor(owner, deckSet, rowCount, colCount, x, y){
        this.owner = owner;
        this.deckSet = deckSet;
        this.rowCount = rowCount;
        this.colCount = colCount;
        this.x = x;
        this.y = y;
        this.capture = false;
        this.active = true;
    }

    cardCount(){
        return this.deckSet.size();
    }

    watch(index){
        return this.deckSet.watch(index);
    }

    isCaptureMode(){
        return !(!this.capture);
    }

    changeDeck(deckSet){
        if(!this.isCaptureMode()){
            this.deckSet = deckSet;
        }
    }

    beginCaptureMode(dp){
        if(this.deckSet.size() == 0 || this.capture){
            return false;
        }
        else{
            this.capture = new CardCapture2D(this, this.rowCount, this.colCount,
                                             this.x, this.y, dp);
            this.owner.addSprite(this.capture);
            this.owner.addTask(this.capture, true);
            return true;
        }
    }

    action(){
        const index = this.capture.selectedIndex();
        const card = this.deckSet.slice(index);
        this.owner.cardPicked(this, card);
        if(this.deckSet.size() == 0){
            this.cancel();
        }
    }

    cancel(){
        this.capture.active = false;
        this.capture = null;
    }

    draw(GE, ctx){
        ctx.save();
        const w = Card.width*this.colCount*1.3-Card.width*0.3+40;
        const h = Card.height*this.rowCount*1.2-Card.height*0.2+40;
        ctx.strokeStyle = "rgb(0,20,70,0.5)";
        ctx.lineWidth = 5;
        ctx.strokeRect(this.x-20, this.y-20, w, h);
        ctx.fillcStyle = "rgba(0,0,0,0.5)";
        ctx.fillRect(this.x-20, this.y-20, w, h);
        if(this.deckSet.size() == 0){
            ctx.fillStyle = "white";
            ctx.font = "27px Sans-Serif";
            ctx.fillText("カードがありません", this.x + 0, this.y + 20);
        }
        else{
            const scroll = this.capture ? this.capture.scroll : 0;
            for(let i = 0; i < this.rowCount; i++){
                for(let j = 0; j < this.colCount; j++){
                    const x = this.x + j*Card.width*1.3;
                    const y = this.y + i*Card.height*1.2;
                    const card = this.deckSet.watch((scroll + i) * this.colCount + j);
                    card.paint(GE, ctx, x, y);
                }
            }
        }
        ctx.restore();
    }
}


// #2. メニューの実装

/**
 * 指定されたCardSetの内容をキャラ別に分割するクラス
 * (元のCardSetに変更は影響しない).
 * @class
 */
let Prism = class{
    static labels = [
        "まどか", "ほむら", "さやか", "マミ", "杏子", "なぎさ", "多人数"
    ];

    constructor(source){
        const tmp = [];
        for(let i = 0; i < Prism.labels.length; i++){
            tmp.push([]);
        }
        source.cards().forEach((e) => {
            if(e.mark < tmp.length - 1) tmp[e.mark].push(e);
            else tmp[tmp.length-1].push(e);
        });
        this.subsets = tmp.map((subset) => new CardSet(subset));
    }

    size(){
        return this.subsets.length;
    }

    union(){
        const a = [];
        for(let i = 0; i < this.subsets.length; i++){
            a.push(...this.subsets[i].cards());
        }
        return a;
    }
}

/**
 * カードの内容の描画を担当するオブジェクト. 
 * これ自体はSceneに登録するオブジェクトではなく, 他のオブジェクトから
 * 必要に応じてpaintメソッドを呼び出されることで描画を行う.
 * @type {Object}
 */
const displayObject = {
    y: 400,
    prevDesc: null,
    lines: null,

    setPosition(i){
        this.x = 320;
        this.y = 400 + 100*i;
    },
    paint(ctx, card){
        ctx.save();
        ctx.strokeStyle = "rgb(255,255,255,0.5)";
        ctx.lineWidth = 5;
        ctx.strokeRect(this.x, this.y-40, 600, 30*4+20);
        ctx.fillStyle = "rgb(0,0,0,0.5)";
        ctx.fillRect(this.x, this.y-40, 600, 30*4+20);

        ctx.fillStyle = "white";
        ctx.font = "22px Sans-Serif";
        ctx.fillText(`${getCharacterName(Suits[card.mark])} ${card.value} /  MP ${card.MP}`,
                     this.x + 20, this.y);
        if(card.skill){
            ctx.fillText(`サブスキル 『${card.skill.caption}』`, this.x + 20, this.y + 30);
            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 + 20, this.y + 30*(i+2));
            }
        }
        ctx.restore();
    }
};

/**
 * BaseMenuのサブメニューを実装するクラス.
 * 指定されたメニューを縦に並べて表示し,
 * ユーザーの入力に応じてデッキ編集作業を実行する.
 * @class
 * @prop {string[]} menu
 * @prop {number} x
 * @prop {number} y
 * @prop {number} width;
 * @prop {number} height;
 * @extends stdtask.Select
 */
let MenuDialog = class extends stdtask.CyclicSelect{
    #padding;
    #step;
    #font;

    /**
     * 指定された位置にメニューを表示するインスタンスを生成する.
     * オプションリストを与えることにより, 以下の要素を指定することも可能.
     * - padding: 内側の余白 (縦・横共通)
     * - step: テキストを描画するときのy座標をいくつずつ増やすか
     * - font: テキストのフォント
     *
     * @param {string[]} menu
     * @param {number} x - 左上隅のx座標
     * @param {number} y - 左上隅のy座標
     * @param {number} innerWidth - 内側の横幅
     * @param {number} innerHeight - 内側の縦幅
     * @param {Object.<string,*>} [opt={}] - オプションリスト
     */
    constructor(menu, x, y, innerWidth, innerHeight, opt = {}){
        super(menu.length, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS");
        this.menu = menu;
        this.x = x;
        this.y = y;
        this.#padding = opt.padding || 20;
        this.#step = opt.step || 40;
        this.#font = opt.font || "22px Sans-Serif";
        this.width = innerWidth + this.#padding * 2;
        this.height = innerHeight + this.#padding * 2;
    }

    /**
     * 描画処理を行う.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     */
    draw(GE, ctx){
        ctx.save();
        ctx.strokeStyle = "rgb(0,20,70,0.5)";
        ctx.lineWidth = 5;
        ctx.strokeRect(this.x, this.y, this.width, this.height);

        ctx.fillStyle = "rgba(0,0,0,0.5)";
        ctx.fillRect(this.x, this.y, this.width, this.height);
        ctx.fillStyle = "white";
        ctx.font = this.#font;

        const m = ctx.measureText("→_");
        const ax = this.x + this.#padding;
        const ay = this.y + this.#padding + m.fontBoundingBoxAscent + this.#step*this.index;
        const aw = m.width;
        ctx.fillText("→", ax, ay);

        const px = ax + aw;
        const py = this.y + this.#padding + m.fontBoundingBoxAscent;
        for(let i = 0; i < this.menu.length; i++){
            ctx.fillText(this.menu[i], px, py + this.#step*i);
        }
        ctx.restore();
    }

    /**
     * 1フレーム分のタスク処理を実行する.
     * 基本的に親クラスのexecute()を呼び出すだけだが,
     * indexが変化したときの処理を自前で実行している (将来的にはSelectを直すべきかも).
     * @param {stdgam.GameEngine} GE - タスク処理に用いるGameEngine
     */
    execute(GE){
        const tmp = this.index;
        super.execute(GE);
        if(this.index != tmp) this.onChange(GE, this.index);
        return false;
    }

    /**
     * indexが変化したときに呼び出される (サブクラスで上書きする).
     * @param {stdgam.GameEngine} GE - タスク処理を実行するために使うGameEngine
     * @param {number} index - this.indexの値
     */
    onChange(GE, n){ }
}

/**
 * デッキ編集画面のベースメニューを実装するクラス.
 * @class
 */
let createBaseMenu = function(owner, panels){
    panels[0].changeDeck(owner.prisms[0].subsets[0]);
    panels[1].changeDeck(owner.prisms[1].subsets[0]);
    const obj = new MenuDialog(Prism.labels, 60, 30, 200, 40*owner.prisms[0].size());
    obj.action = (GE, n) => {
        const sub = new MenuDialog(["カードを増やす", "カードを減らす"], 60, 60, 200, 80);
        sub.action = (GE, n) => {
            displayObject.setPosition(n);
            panels[1-n].beginCaptureMode(displayObject);
        }
        sub.cancel = (GE, n) => { sub.active = false; }
        owner.addSprite(sub);
        owner.addTask(sub, true);
    };
    obj.cancel = (GE, n) => {
        owner.used.init(owner.prisms[0].union());
        owner.notUsed.init(owner.prisms[1].union());
        if(LocalStorageInfo.isUsed()){
            LocalStorageInfo.saveDeck(owner.used.cards());
        }
        GE.changeScene("select");
    };
    obj.onChange = (GE, n) => {
        panels[0].changeDeck(owner.prisms[0].subsets[n]);
        panels[1].changeDeck(owner.prisms[1].subsets[n]);
    };
    return obj;
}


// #3. editSceneの実装

/**
 * デッキ編集画面を実装するSceneオブジェクト.
 * @namespace
 * @prop {CardSet} used
 * @prop {CardSet} notUsed
 * @prop {Prism[]} prisms
 * @prop {CardPanel2D} panel1
 * @prop {CardPanel} panel2
 * @prop {MenuDialog} menu
 */
edit.editScene = new stdgam.Scene({
/**
 * シーンがロードされた時に実行される初期化処理.
 * @param {stdgam.GameEngine} GE - このシーンをロードしたGameEngine
 * @param {Object.<string, *>} args - このシーンに渡されたオプションリスト
 * @memberof edit.editScene
 */
onLoad(GE, args){
    this.add(T.image(GE.caches.get("BACKGROUND"), {x: 0, y: 0}));
    this.used = args.set1;
    this.notUsed = args.set2;
    this.prisms = [new Prism(this.used), new Prism(this.notUsed)];

    this.panel1 = new CardPanel2D(this, this.used, 3, 5, 330, 50);
    this.panel2 = new CardPanel(this, this.notUsed, 7, 90, 520, false);
    this.add(this.panel1);
    this.add(this.panel2);

    this.menu = createBaseMenu(this, [this.panel1, this.panel2], this.prisms);
    this.addSprite(this.menu);
    this.addTask(this.menu, true);
},

/**
 * CardPanel or CardPanel2Dを通じてカードが選択されたときの処理.
 * @param {(CardPanel|CardPanel2D)} panel - 呼び出し元
 * @param {Card} card 選択されたカード
 * @memberof edit.editScene
 */
cardPicked(panel, card){
    const i = this.menu.index;
    if(panel == this.panel1) this.prisms[1].subsets[i].push(card);
    else this.prisms[0].subsets[i].push(card);
}
});

})(edit);