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