/**
* @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();
} // ----------(*)
Source