Source

nagibato.js

/**
 * @file
 * ゲームの初期化処理と画像・音声の準備, およびタイトルページの実装を行う.
 *
 * @author lenuser
 */


// #1. GameEngineの生成, stdgamに関連する定数の定義

/**
 * stdgamから必要な要素をインポートしてきたもの.
 * - GameEngine: {Class} ゲームのメインエンジンを実装するクラス
 * - Scene: {Class} シーンを表すクラス
 * - ImageCutter: {Class} 画像を分割して扱うためのクラス
 * - Templates: {Object.<string, function>} 定番のオブジェクトを作るためのテンプレート群
 */
const { GameEngine, Scene, ImageCutter, Templates } = stdgam;

/**
 * このプログラムで使うGameEngineオブジェクト.
 * @type {stdgam.GameEngine}
 */
const GE = new GameEngine("gameCanvas");

/**
 * stdgam.Templatesの略称.
 * @type {Object.<string, function>}
 */
const T = Templates;


// #2. 画像や音声の準備

/*
 * プログラムで使う画像や音声をまとめて登録しておく.
 */

GE.images.load("CARDIMAGES", "./image/cardimages.png");

GE.se.register("tick", [0.8,0,1025,.01,.005, 0.05 , 0 ,1.2,1, 10 ,50,.1,.01,1,1,.1,,.6,.01,.01]);
GE.se.register("powerup", [1,0,580,.08,.23,.12,0,2.1,0,0,-72,.06,.07,0,0,0,.12,.54,.24,.01,0]);
GE.se.register("heal", [.7,0,334,.01,.12,.08,1,1.1,-13,0,388,.19,0,0,0,0,.14,.85,.26,0,123]);
GE.se.register("battleFinished", [1,0,482,.01,.07,.06,0,1.3,4,0,100,0,.02,0,4,0,.01,.65,.01,0,0]);
GE.se.register("optionSelected", [0.7,0,530,.01,.08,.15,1,.5,-9,-110,0,0,0,0,0,0,.1,.9,.04,0,0]);
GE.se.register("hit0", [1,0,426,.02,.16,.15,1,4,-5,6,0,0,0,1.4,0,.4,.01,.88,.1,0,0]);
GE.se.register("hit1", [1,0,426,.02,.16,.15,1,4,-5,6,0,0,.01,1.4,0,.4,.01,.88,.1,0,0]);
GE.se.register("powerup", [0.7,0,126,.01,.22,.3,1,.5,-4,0,0,0,0,0,0,0,.15,.6,.24,0,0]);
GE.se.register("chargeUp", [0.3,0,220,.09,.19,.34,0,3,8,300,50,0,.02,0,13,.1,0,.61,.16,.17,0]);
GE.se.register("skill", [1.5,0,154,.01,.1,.09,1,1.1,0,0,200,.05,.03,0,0,0,.15,.56,.01,0,0]);
GE.se.register("enemyAction", [2,0,65.40639,.16,.33,.33,1,3.2,0,0,0,0,0,.2,0,0,.17,.36,.06,0,0]);
GE.se.register("shield", [5,0,571,.09,.09,.3,0,4,0,0,0,0,.06,0,0,.1,0,.7,.25,.36,-671]);

GE.caches.createCache("BACKGROUND", 1000, 700, (ctx) => {
    ctx.save();
    const grad = ctx.createLinearGradient(0, 0, 0, 1000);
    grad.addColorStop(0, "#002753"); 
    grad.addColorStop(1, "#001839"); // 下端(影)
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, 1000, 700);
    ctx.restore();
});
GE.caches.createCache("CARDMAT", 1000, 700, (ctx) => {
    ctx.save();
    ctx.fillStyle = "#502020";
    ctx.fillRect(200, 120, 400, 190);
    ctx.fillStyle = "#604040";
    ctx.fillRect(203, 123, 394, 184);

    ctx.fillStyle = "#502020";
    ctx.fillRect(230, 340, 340, 190);
    ctx.fillStyle = "#604040";
    ctx.fillRect(233, 343, 334, 184);
    ctx.restore();
});
GE.caches.createCache("DIALOG", 1000, 700, (ctx) => {
    ctx.save();
    ctx.globalAlpha = 0.5;
    ctx.fillStyle = "black";
    ctx.fillRect(25, 25, 950, 650);
    ctx.restore();
});
GE.caches.createCache("BOOKLIKE_0", 360, 60, (ctx) => {
    const w = 360, h = 60, r = 5;
    ctx.save();
    ctx.fillStyle = "rgb(239,228,176)";
    ctx.fillRect(0, 0, 360, 60);

    ctx.shadowColor = "rgb(255,255,76)";
    ctx.shadowBlur = 10;
    ctx.strokeStyle = "black";
    ctx.lineWidth = 5;
    ctx.strokeRect(-2, -2, w + 4, h + 4); // 少し外側を叩く
    ctx.restore();
});
GE.caches.createCache("BOOKLIKE_1", 360, 60, (ctx) => {
    const w = 360, h = 60, r = 5;
    ctx.save();
    ctx.fillStyle = "rgb(195,195,195)";
    ctx.fillRect(0, 0, 360, 60);

    ctx.shadowColor = "rgb(125,125,125)";
    ctx.shadowBlur = 10;
    ctx.strokeStyle = "black";
    ctx.lineWidth = 5;
    ctx.strokeRect(-2, -2, w + 4, h + 4); // 少し外側を叩く
    ctx.restore();
});


// #3. タイトル画面の実装

// (a) intermediateSceneの定義

/**
 * タイトル画面からバトルへ移行する際, 途中に挟む仲介役のシーン.
 * 逆に, バトルから戻って来るときもこのシーンを経由する.
 * @namespace
 * @prop {number} state - このシーンの状態遷移を管理する数字
 * @prop {Object.<string,*>} args - このシーンに渡されたオプションリスト
 * @prop {Deck} deck - 一番最近のバトルで使用したデッキ. これ自体ではなく複製したものをバトルで使う.
 * @prop {Deck} sideboard - 一番最近のバトルで使用したサイドボード. これ自体ではなく複製したものをバトルで使う.
 * @extends stdgam.Scene
 */
const intermediateScene = new Scene({
    state: 0,

    /**
     * 最初の1回だけ実行する初期化処理.
     * @param {stdgam.GameEngine} GE - このシーンをロードしたGameEngine
     * @memberof intermediateScene
     */
    ensure(GE){
        this.add(T.image(GE.caches.get("BACKGROUND"), {x: 0, y: 0}));
        this.ensure = () => {};
    },

    /**
     * シーンがロードされた時に実行される初期化処理.
     * @param {stdgam.GameEngine} GE - このシーンをロードしたGameEngine
     * @param {Object.<string, *>} args - このシーンに渡されたオプションリスト
     * @memberof intermediateScene
     */
    onLoad(GE, args){
        this.ensure(GE);
        if(this.state == 0 || args.tutorial){
            this.args = args;
            this.deck = args.deck.clone();
            this.sideboard = args.sideboard.clone();
            if(args.tutorial) this.state = 1;
        }

        if(this.state == 0){
            const practiceData = {...args};
            practiceData.enemyData = null;
            practiceData.deck = this.deck.clone();
            practiceData.sideboard = this.sideboard.clone();
            this.state++;
            GE.changeScene("main", practiceData);
        }
        else if(this.state == 1){
            const mainData = {...this.args};
            mainData.deck = this.deck.clone();
            mainData.sideboard = this.sideboard.clone();
            this.state++;
            GE.changeScene("main", mainData);
        }
        else{
            this.state = 0;
            GE.changeScene("select");
        }
    }
});


// (b) タイトル画面のUIの構成要素

/**
 * バトル・チュートリアルの選択に使うダイアログを実装するクラス.
 * @class
 * @prop {string[]} menu - 各項目の表示テキストをリストにしたものs
 * @prop {number} x - 表示位置のx座標
 * @prop {number} y - 表示位置のy座標
 * @extends stdtask.Scroll
 */
class SelectWindow extends stdtask.Scroll{
    #callback;
    #rowCount;
    #width;
    #height;
    #step;  // 隣接する2行のy座標の差
    #padY;  // y方向のパディングの量

    /**
     * @param {function(number): void} callback - 項目が選択されたときに呼び出される関数.
     * 引数として選択された項目のインデックスを受け取る
     * @param {number} rowCount - 一度に表示できる最大項目数
     */
    constructor(menu, callback, rowCount, x, y, w, h){
        const select = new stdtask.Select(
            menu.length, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS", 0, 10
        );
        super(select, rowCount);

        this.menu = menu;
        this.#callback = callback;
        this.#rowCount = rowCount;

        this.x = x;
        this.y = y;
        this.#step = 60;
        this.#padY = 40 + (h - this.#step * rowCount) / 2;
        this.#width = w;
        this.#height = h;
    }

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

        ctx.font = "42px monospace";
        ctx.fillStyle = "white";
        const m = ctx.measureText("→_");
        const ax = this.x + 30;
        const ay = this.y + this.#padY + this.offset*this.#step;
        const aw = m.width;
        ctx.fillText("→", ax, ay);

        for(let i = 0; i < this.#rowCount; i++){
            const n = this.scroll + i;
            if(n < this.menu.length){
                ctx.fillText(this.menu[n], ax+aw, this.y+this.#padY+this.#step*i);
            }
            else break;
        }
        if(this.scroll > 0){
            ctx.font = "20px Sans-Serif";
            ctx.textAlign = "center";
            ctx.fillText("▲", this.x + this.#width/2, this.y + this.#padY-this.#step);
        }
        if(this.#rowCount < this.menu.length && this.scroll < this.menu.length-this.#rowCount){
            ctx.font = "20px Sans-Serif";
            ctx.textAlign = "center";
            ctx.fillText("▼", this.x + this.#width/2, this.y+this.#padY+this.#step*(this.#rowCount-1)+40);
        }
        ctx.restore();
    }

    /**
     * 項目が選択決定されたときの処理を実行する.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {number} n - 選択中の項目のインデックス
     */
    action(GE, n){
        this.#callback(n);
    }

    /**
     * 選択がキャンセルされたときの処理を実行する.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {number} n - 選択中の項目のインデックス
     */
    cancel(GE, n){
        this.active = false;
    }
}

/**
 * Shelflikeに格納される個々のアイテムを実装するクラス.
 * @class
 * @prop {string} text - 表示するテキスト
 * @prop {number} x - 表示位置のx座標
 * @prop {number} y - 表示位置のy座標
 * @prop {boolean} selected - この項目が選択中ならばtrue, そうでなければfalse
 * @prop {function(stdgam.GameEngine, stdgam.Scene, Booklike): void} action - この項目が選択されたときに呼び出される関数
 */
class Booklike{
    static boxColors = [ "rgb(239,228,176)", "rgb(195,195,195)"];
    static textColors = [ "rgb(255,100,0)", "gray" ];
    static borderColor = "rgb(0,20,40)";

    /**
     * 指定された設定でインスタンスを作る.
     * @prop {string} text - 表示するテキスト
     * @prop {number} x - 表示位置のx座標
     * @prop {number} y - 表示位置のy座標
     * @prop {function(stdgam.GameEngine, stdgam.Scene, Booklike): void} action -
     * この項目が選択されたときに呼び出される関数
     */
    constructor(text, x, y, action = null){
        this.text = text;
        this.action = action;
        this.x = x;
        this.y = y;
    }

    /**
     * この項目が選択決定されたときに呼び出される.
     * this.actionが設定されていればこれを実行し, そうでなければ何もしない.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {stdgam.Scene} scene - 操作対象のScene
     */
    open(GE, scene){
        if(this.action) this.action(GE, scene, this);
    }

    /**
     * 描画処理を行う.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     */
    draw(GE, ctx){
        ctx.save();
        const n = this.selected ? 0 : 1;
        const x = this.selected ? this.x - 360*0.2 : this.x;

        const img = GE.caches.get(`BOOKLIKE_${n}`);
        ctx.drawImage(img, x, this.y);

        ctx.font = this.selected ? "bold 32px Serif" : "32px Serif";
        ctx.fillStyle = Booklike.textColors[this.selected ? 0 : 1];
        ctx.fillText(this.text, x+20, this.y+40);
        ctx.restore();
    }
}

/**
 * タイトル画面のメインメニューを実装するクラス.
 * @class
 * @prop owner
 * @prop {number} x - 表示位置のx座標
 * @prop {number} y - 表示位置のy座標
 * @prop books
 * @extends stdtask.CyclicSelect
 */
class Shelflike extends stdtask.CyclicSelect{
    #width;
    #height;

    static boxColor = "rgb(30,30,30)";
    static borderColor = "rgb(10,10,10)";

    /**
     * booksを項目とするインスタンスを生成する.
     * @param {stdgam.Scene} owner - このオブジェクトを所有するシーンオブジェクト
     * @param {Booklike[]} books - 項目として使うBooklikeオブジェクトの配列
     * @param {number} x - 表示位置のx座標
     * @param {number} y - 表示位置のy座標
     * @param {number} w - 表示する横幅
     * @param {number} h - 表示する縦幅
     */
    constructor(owner, books, x, y, w, h){
        super(books.length, ["ArrowUp", "ArrowDown"], "KeyA", "KeyS", 0, 12, false);
        this.owner = owner;
        this.x = x;
        this.y = y;
        this.#width = w;
        this.#height = h;
        this.books = books;
        books[0].selected = true;
    }

    /**
     * 描画処理を行う.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     */
    draw(GE, ctx){
        ctx.save();
        ctx.fillStyle = Shelflike.borderColor;
        ctx.fillRect(this.x-5, this.y-5, this.#width+10, this.#height*this.books.length+10);
        ctx.fillStyle = Shelflike.boxColor;
        ctx.fillRect(this.x, this.y, this.#width, this.#height*this.books.length);
        ctx.restore();
    }

    /**
     * 1フレーム分のタスク処理を実行する.
     * @param {stdgam.GameEngine} GE - このタスク処理に用いるGameEngine
     */
    execute(GE){
        const prev = this.index;
        const f = super.execute(GE);
        if(this.index != prev){
            this.books[prev].selected = false;
            this.books[this.index].selected = true;
        }
        return f;
    }

    /**
     * 項目が選択決定されたときの処理を実行する.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {number} n - 選択中の項目のインデックス
     */
    action(GE, index){
        this.books[this.index].open(GE, this.owner);
    }
}


// (c) selectSceneの実装

/**
 * タイトル画面を実装するSceneオブジェクト.
 * @namespace
 * @prop yesno
 * @prop setting
 * @prop books
 * @prop shelf
 */
const selectScene = new Scene({

// バトル開始前の処理
*confirmationBattle(GE, opt){
    this.yesno = createConfirmatingQB_BeforeBattle(this.setting);
    this.addSprite(this.yesno);
    this.addTask(this.yesno, true);
    while(this.yesno.active) yield true;

    const ans = this.yesno.result;
    if(!ans) return;

    const deckObj = new Deck(this.setting.deckSet.cards());
    const sideboardObj = new Deck(RAW_CARD_DATA.map((e) => CardAtlas.get(e.id)));
    const mainCard = CardAtlas.get(this.setting.mainCardData.id);
    deckObj.remove(mainCard);
    sideboardObj.remove(mainCard);
    deckObj.shuffle();

    opt2 = {
        deck: deckObj, sideboard: sideboardObj,
        playerData: this.setting.mainCardData,
        chainRule: this.setting.chainRule,
        QBChance: this.setting.QBChance,  ...opt
    };
    GE.changeScene("intermediate", opt2);
},
battleSelected(GE, opt){
    this.useCoroutine(GE, this.confirmationBattle, opt);
},

// チュートリアル開始前の処理
*confirmationTutorial(GE, opt){
    this.yesno = createTutorialQB(this.setting);
    this.addSprite(this.yesno);
    this.addTask(this.yesno, true);
    while(this.yesno.active) yield true;

    const ans = this.yesno.result;
    if(!ans) return;

    GE.changeScene("intermediate", opt);
},
tutorialSelected(GE, opt){
    this.useCoroutine(GE, this.confirmationTutorial, opt);
},

actions:  [
    function(GE, scene, book){
        const menu = [];
        const values = [];
        for(let data of TutorialInfo){
            const usedCards = data.cardIDs.map((id) => CardAtlas.get(id) || data.extraCard);
            let sideboardCards = [];
            if(data.extraScan){
                sideboardCards = RAW_CARD_DATA.map((e) => CardAtlas.get(e.id));
            }
            menu.push(data.caption);
            values.push({
                deck: new Deck(usedCards), sideboard: new Deck(sideboardCards),
                playerData: data.playerData, enemyData: data.enemyData,
                chainRule: data.chainRule, QBChance: data.QBChance, 
                tutorial: data.tutorial
            });
        }
        const op = (i) => {
            scene.tutorialSelected(GE, values[i]);
        };
        const sw = new SelectWindow(menu, op, 6, 50, 180, 700, 450);
        scene.addSprite(sw);
        scene.addTask(sw, true);
    },
    function(GE, scene, book){
        const menu = [
            "ゲルトルート", "イザベル", "ギーゼラ", "シャルロッテ",
            "パトリシア", "エルザマリア", "H.N.エリー", "ロベルタ",
            "オクタヴィア", "ワルプルギスの夜", "ナイトメア", "ホムリリー"
        ];
        const values = menu.map((name) => { return {enemyData: EnemyData[name]}; });
        const op = (i) => {
            scene.battleSelected(GE, values[i]);
        };
        const sw = new SelectWindow(menu, op, 6, 50, 180, 700, 450);
        scene.addSprite(sw);
        scene.addTask(sw, true);
    },
    function(GE, scene, book){
        const opt = {set1: scene.setting.deckSet, set2: scene.setting.sideboardSet};
        GE.se.play("optionSelected");
        GE.changeScene("edit", opt);
    },
    function(GE, scene, book){
        GE.se.play("optionSelected");
        GE.changeScene("config", scene.setting);
    },
    function(GE, scene, book){
        GE.se.play("optionSelected");
        GE.changeScene("config2", scene.setting);
    }
],

initShelf(){
    this.books = [
        new Booklike("チュートリアル",  600, 260, this.actions[0]),
        new Booklike("魔女を選んでバトル",  600, 320,this.actions[1]),
        new Booklike("デッキ編集", 600, 380, this.actions[2]),
        new Booklike("設定", 600, 440, this.actions[3]),
        new Booklike("特殊設定", 600, 500, this.actions[4])
    ];
    this.shelf = new Shelflike(this, this.books, 600, 260, 360, 60);
},

initData(){
    CardAtlas.init();
    const usedCards = [];
    const sideboardCards = [];

    let configData = [ "2-039", 1, true ];
    if(LocalStorageInfo.isUsed()){
        const IDs = LocalStorageInfo.deckInfo;
        CardAtlas.forEach((card) => {
            if(IDs.includes(card.cardAtlasID)) usedCards.push(card);
            else sideboardCards.push(card);
        });
        configData = LocalStorageInfo.config;
        Card.MarkerFlag = LocalStorageInfo.config[2] ?? true;
    }
    else{
        CardAtlas.forEach((card) => {
            if(card.value < 8 || (card.mark > 5 && card.value < 10)) sideboardCards.push(card);
            else usedCards.push(card);
        });
    };
    this.setting = {
        deckSet: new CardSet(usedCards),
        sideboardSet: new CardSet(sideboardCards),
        mainCardData: {...RAW_CARD_DATA.find((e) => e.id == configData[0])},
        chainRule: configData[1],
        QBChance: configData[3] ?? true
    };
},

/**
 * シーンがロードされた時に実行される初期化処理.
 * @param {stdgam.GameEngine} GE - このシーンをロードしたGameEngine
 * @param {Object.<string, *>} args - このシーンに渡されたオプションリスト
 * @memberof selectScene
 */
onLoad(GE, args){
    if(!this.setting) this.initData();
    if(this.shelf) return;
    this.initShelf();
    this.add(T.image(GE.caches.get("BACKGROUND"), {x: 0, y: 0}));
    this.add(T.text("MAGICARD BATTLE", {x: 50, y:130, color: "white", font: "60px monospace", shadowBlur: 10, shadowColor: "rgb(255,255,255,0.2)", shadowOffsetY: 3}));
    this.add(T.text("シミュレータ", {x: 520, y:130, color: "white", font: "30px monospace", shadowBlur: 10, shadowColor: "rgb(255,255,255,0.2)", shadowOffsetY: 3}));
    this.add(T.text("A : 決定,  S : キャンセル,  矢印キー : 移動", {x: 50, y:650, color: "white", font: "30px monospace"}));

    this.add(this.shelf);
    for(const book of this.books){
        this.add(book);
    }
}

});


// #4. ゲームの開始

/*
 * 以上の準備のもとで次を実行する
 */

/**
 * ロード画面.
 * 画像のロードを完了してからじゃないとゲームを開始できないので,
 * ここでロード待ちをする. もし将来タイトル画面で音声を使いたい場合は,
 * キー入力待ちも一緒に済ませてしまうとよい.
 * @namespace
 * @prop message
 * @prop flag
 */
const bootstrap = new Scene({
    /**
     * シーンがロードされた時に実行される初期化処理.
     * @param {stdgam.GameEngine} GE - このシーンをロードしたGameEngine
     * @param {Object.<string, *>} args - このシーンに渡されたオプションリスト
     * @memberof intermediateScene
     */
    onLoad(GE, args){
        this.add(T.image(GE.caches.get("BACKGROUND"), {x: 0, y: 0}));
        this.message = new QBTalk("セ~ガ~♪", 30);
        this.add(this.message);
        this.flag = false;
        GE.ready(() => { this.flag = true; });
    },

    /**
     * 1フレーム分のタスク処理を実行する.
     * @param {stdgam.GameEngine} GE - このタスク処理に用いるGameEngine
     */
    execute(GE){
        if(this.flag && !this.message.active){
            Card.init(GE, 90, 120);
            GE.addScene("select", selectScene);
            GE.addScene("edit", edit.editScene);
            GE.addScene("config", config.configScene);
            GE.addScene("config2", config.configScene2);
            GE.addScene("intermediate", intermediateScene);
            GE.addScene("main", battle.mainScene);
            GE.changeScene("select");
        }
    }
});

var exitflag = false;

try{
    LocalStorageInfo.init();
} catch(e) {
    window.alert(e.message);
    const text = "通常はデータを一度消去しなければいけません。消去してよろしいですか?(消去せずに動作停止する場合は「いいえ」を選択してください)";
    if(window.confirm(text)){
        LocalStorageInfo.removeStorage(true);
    }
    else{
        exitflag = true;
    }
}

if(!exitflag){   // ----------(*)
    PrismaticCard.init(GE, 90, 120);
    GE.addScene("bootstrap", bootstrap);
    GE.changeScene("bootstrap");
    GE.run();
}  // ----------(*)