Source

card.js

/**
 * @file
 * カードに関連するオブジェクトを実装する.
 *
 * @author lenuser
 */


/**
 * @typedef {(Card|PrismaticCard)} Cardlike
 * @prop {number} mark - このカードの属性をSuits内のインデックスで表した値 (マーク数値)
 * @prop {number} value - このカードのコスト
 * @prop {PlayerSkill_skill} [skill] - このカードが持つスキル
 * @prop {string} [cardAtlasID] - cardlist.jsにおけるこのカードのID
 */

// #1. 属性の定義・実装

/*
 * カードは「属性」と「コスト」を持つ.
 * このうち, 属性はさらに「基本属性」と「複合属性」に分かれる.
 *
 * 基本属性は PrimitiveSuits で定義される.
 * また, Suits にも初期値としてこれらの値が格納される. 複合属性はそれが
 * 必要になったとき自動的に Suits に追加される.
 * 以下, 「属性の値」とはこれらの文字列のこととする.
 *
 * 現実には, 属性の値そのものを直接扱うことは少なく, 多くは「 Suits 内における
 * その属性のインデックス」を利用する (しばしば「マーク数値」と略す).
 * ただし, ファイル名の生成やImagePool などで使う文字列キーの生成では
 * 属性の値自体を直接使う.
 *
 * また, コンボの判定に使うデータを ChainMask に格納する.
 * 複合属性が Suits に追加されるとき ChainMask にも適切な値が追加される.
 * これらの値を使ってコンボの判定を行う関数が ChainFunc に設定される.
 */


// (a) 属性

/**
 * 基本属性を定義するリスト.
 * @type string[]
 */
const PrimitiveSuits = [ "Md", "Hm", "Sy", "Mm", "Ky", "Ng" ];

/**
 * プログラムで使われている属性を登録するリスト.
 * @type string[]
 */
const Suits = [...PrimitiveSuits];

/**
 * 複合属性の属性名を生成する
 * @param {number[]} marks - 各構成要素のPrimitiveSuitsにおけるインデックス
 * @returns {string} 生成された属性名
 */
const getPolysuitName = function(marks){
    const names = marks.map((m) => PrimitiveSuits[m]);
    return names.join("");
}


// (b) キャラクター名 (or カード名) への変換

/**
 * 基本属性をキャラクターのフルネームに変換する連想配列.
 * @type Object.<string, string>
 */
const CharacterNames = {
    Md: "鹿目まどか",  Hm: "暁美ほむら",  Sy: "美樹さやか",  Mm: "巴マミ",
    Ky: "佐倉杏子",  Ng: "百江なぎさ"
};

/**
 * 基本属性をキャラクターの下の名前に変換する連想配列.
 * @type Object.<string, string>
 */
const ShortCharacterNames ={
    Md: "まどか",  Hm: "ほむら",  Sy: "さやか",  Mm: "マミ",
    Ky: "杏子",  Ng: "なぎさ"
};

/**
 * 指定された属性に対応するカード名を生成する.
 * @param {string} suitString - 属性の値
 * @returns {string} 生成されたカード名
 */
const getCharacterName = function(suitString){
    if(suitString.length >= 10) return "魔法少女";
    if(suitString.length == 2) return CharacterNames[suitString];
    const splitted = suitString.match(/.{1,2}/g);
    return splitted.map((e) => ShortCharacterNames[e]).join("&");
}


// (c) コンボ判定用の値 (マスク)

/**
 * 各属性に対して割り当てられたコンボ判定用のマスク.
 * @type number[]
 */
const ChainMask = [ 1, 2, 4, 8, 16, 32 ];

/**
 * 複合クラスのためのマスクを計算する
 * @param {number[]} indices - 各構成要素のPrimitiveSuitsにおけるインデックス
 * @returns {number} 生成されたマスクの値
 */
const getPolysuitMask = function(indices){
    let m = 0;
    for(const i of indices) m |= ChainMask[i];
    return m;
}

/**
 * 各属性の強化倍率を扱うオブジェクト.
 * 一部のスキル (敵スキルを含む) では「特定の属性だけMPが増加」のように
 * 属性ごとに影響の違う効果を引き起こすことがある.
 * これを管理するために用意する (複合属性は全部ひとまとめにして扱う).
 * @type {Object}
 * @prop {number[]} primitive - 各基本属性に対するMPの補正量をパーセントで表した値
 * @prop {number} prismatic - 複合属性 ("Ng"を含むもの以外) に対するMPの補正量をパーセントで表した値
 * @namespace
 */
const MPBoostBySuit = {
    primitive: PrimitiveSuits.map((e) => 100),
    prismatic: 100,

    /**
     * このオブジェクトを初期化する.
     */
    init(){
        this.primitive.fill(100);
        this.prismatic = 100;
    },

    /**
     * Suits[m]に対する強化倍率を計算する.
     * 複合属性は基本的に全部「白属性」という単一の括りで扱うが,
     * "Ng" を含むものは例外的に "Ng" として扱う (理由は不明・・・)
     * @param {number} m - Suitsにおけるその属性のインデックス
     * @returns {number} その属性に対するMPの補正量をパーセントで表した値
     */
    get(m){
        if(ChainMask[m] & ChainMask[5]) m = 5;
        return (this.primitive[m] ?? this.prismatic) / 100;
    }
};


// (d) コンボ判定関数

/*
 * コンボの成立条件はバージョンによって異なるため, 採用するルールに応じて
 * これらの関数を使い分けること.
 */

/**
 * MAGICARD BATTLE第1弾のルールに従い, 3枚のカードがコンボの条件を
 * 満たしているか判定する
 * (3枚目のカードがスキルを持つかどうかはチェックしない).
 * @param {Cardlike} a
 * @param {Cardlike} b
 * @param {Cardlike} c
 * @returns {boolean} コンボが成立していればtrue, 不成立ならfalse
 */
const ChainFunc = function(a, b, c){
    return ((a.mask & c.mask) == a.mask) && ((b.mask & c.mask) == b.mask);
}

/**
 * MAGICARD BATTLE第2弾のルールに従い, 3枚のカードがコンボの条件を
 * 満たしているか判定する
 * (3枚目のカードがスキルを持つかどうかはチェックしない).
 * @param {Cardlike} a
 * @param {Cardlike} b
 * @param {Cardlike} c
 * @returns {boolean} コンボが成立していればtrue, 不成立ならfalse
 */
const ChainFuncVer2 = function(a, b, c){
    if(c.mark < PrimitiveSuits.length){
        return ((a.mark == b.mark) && (b.mark == c.mark));
    }
    else{
        return (a.mark >= PrimitiveSuits.length)
               && (b.mark >= PrimitiveSuits.length);
    }
}


// #2. カード, およびカードに関連する機能

// (a) スキルとその実行インターフェース

/**
 * プレイヤー側のスキルの実行インターフェース.
 * スキルを表すオブジェクトAと, その実行者Bの間の仲介を行う.
 * Aはこのクラスが提供する機能を利用してスキルを実行する.
 * Bはこのクラスの *upkeep や *deal によりAの実行を依頼する.
 *
 * SkillDealerBase自体は各スキル効果の具体的な処理方法を知らない.
 * 具体的な処理内容はサブクラスで実装する.
 * - enemyHP()
 * - *addHP(percent)
 * - *addMP(percent)
 * - *addSG(percent)
 * - *addHPSG(percent)
 * - *addShield(n)
 * - *chargeUp(percent)
 * - *reduceEnemyMP(percent)
 * - *suitSpecificBoost(str, mark, percent)
 * - *damage(percent)
 * - *extendTime(n)
 * - *timeWarp()
 * - *heal(percent)
 * - *SGHeal(percent)
 * - *crisisBoostTask(percent)
 *
 * @class
 * @prop healRate - ターン開始時のHP回復量を最大HPに対するパーセント表示で表した値
 * @prop SGHealRate - ターン開始時のSG回復量
 * @prop crisisBonus - crisisBoostの効果量を基本MPに対するパーセント表示で表した値
 * @prop appliedCB - crisisBoostの効果のうち, 現時点で既に適用済みの効果量
 */
class SkillDealerBase{
    constructor(){
        this.healRate = 0;
        this.SGHealRate = 0;
        this.crisisBonus = 0;
        this.appliedCB = 0;
    }

    /**
     * 指定されたフレーム数だけyield trueを繰り返すジェネレータを生成する.
     * framesが0以下の場合は何もしない.
     * @param {number} frames - 待機するフレーム数
     */
    *wait(frames){
        while(frames-- > 0) yield true;
    }

    /**
     * プレイヤー側アップキープの処理を実行するジェネレータを生成する.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     */
    *upkeep(GE){
        // healが実行されない場合, crisisBoostのHPチェックを自分で実行
        if(this.healRate > 0) yield* this.heal(this.healRate);
        else yield *this.crisisBoostTask(this.crisisBonus);

        // SGHealの実行
        if(this.SGHealRate > 0) yield* this.SGHeal(this.SGHealRate);
    }

    /**
     * SkillDealerBase/EnemyActionDealerBaseの作業中に
     * プレイヤーのHPが変化したとき呼び出されるジェネレータ関数.
     */
    *playerChanged(){
        yield *this.crisisBoostTask(this.crisisBonus);
    }

    /**
     * 指定されたスキルの効果を実行するジェネレータを生成する.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {PlayerSkill_skill} skill - 実行するスキル
     */
    *deal(GE, skill){
        yield* skill.effect(this);
    }
}

/**
 * @typedef {Object} PlayerSkill_skill
 * @prop {string} caption - スキル名
 * @prop {string} desc - 効果の説明
 * @prop {GeneratorFunction} effect - 引数として受け取ったSkillDealerBaseを使ってスキルの効果を実装する
 */

/**
 * プレイヤー側のスキルを生成する関数をまとめたもの.
 * @type {Object.<string, function>}
 * @namespace
 */
const PlayerSkill = {
    /**
     * HPを回復するスキル.
     * @param {number} percent - 回復量が最大HPの何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    addHP: function(percent, cap = null, desc = null){
        return {
            caption: cap || "体力回復",
            desc: desc || `HPを${percent}%回復`,
            *effect(dealer){
                yield* dealer.addHP(percent);
            }
        };
    },

    /**
     * MPを増加させるスキル.
     * @param {number} percent - 増加量が基本MPの何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    addMP: function(percent, cap = null, desc = null){
        return {
            caption: cap || "MPアップ",
            desc: desc || `メインカードMPが${percent}%アップ`,  // 「の」が入らないみたい
            *effect(dealer){
                yield* dealer.addMP(percent);
            }
        };
    },

    /**
     * SGを回復するスキル.
     * @param {number} percent - 回復量が何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    addSG: function(percent, cap = null, desc = null){
        return {
            caption: cap || "ソウルジェム回復",
            desc: desc || `ソウルジェムを${percent}%回復`,
            *effect(dealer){
                yield* dealer.addSG(percent);
            }
        };
    },

    /**
     * HPとSGを回復するスキル.
     * @param {number} percent - 回復量がそれぞれの最大値の何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    addHPSG: function(percent, cap = null, desc = null){
        return {
            caption: cap || "HPとソウルジェムを回復",
            desc: desc || `HPとソウルジェムを${percent}%回復`,
            *effect(dealer){
                yield* dealer.addHPSG(percent);
            }
        };
    },

    /**
     * ターン開始時にHPを回復するスキル.
     * @param {number} percent - 回復量が最大HPの何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    heal: function(percent, cap = null, desc = null){
        return {
            caption: cap || "癒し効果",
            desc: desc || `ターンの開始時にHPが${percent}%回復`,
            *effect(dealer){
                dealer.healRate += percent;
            }
        };
    },

    /**
     * ターン開始時にSGを回復するスキル.
     * @param {number} percent - 回復量が何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    SGHeal: function(percent, cap = null, desc = null){
        return {
            caption: cap || "浄化効果",
            desc: desc || `ターンの開始時にソウルジェムが${percent}%回復`,
            *effect(dealer){
                dealer.SGHealRate += percent;
            }
        };
    },

    /**
     * シールドを増やすスキル.
     * @param {number} n - シールドの増加量
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    addShield: function(n, cap = null, desc = null){
        return {
            caption: cap || "防御シールド",
            desc: desc || `敵の攻撃を${n}回防御`,
            *effect(dealer){
                yield* dealer.addShield(n);
            }
        };
    },

    /**
     * チャージMPを増加させるスキル.
     * @param {number} percent - 増加量が基本チャージMP (チャージボーナスを加算する前のMP合計値) の何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    chargeUp: function(percent, cap = null, desc = null){
        return {
            caption: cap || "チャージボーナス",
            desc: desc || `このターンのチャージMPを${percent}%アップ`,
            *effect(dealer){
                yield* dealer.chargeUp(percent);
            }
        };
    },

    /**
     * 残りHPが最大値の半分以下のときにMPを増加させるスキル.
     * @param {number} percent - 増加量が基本MPの何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    crisisBoost: function(percent, cap = null, desc = null){
        return {
            caption: cap || "ピンチでMPアップ",
            desc: desc || `残りHP50%以下でメインMP${percent}%上昇`,
            *effect(dealer){
                dealer.crisisBonus += percent;
                yield* dealer.crisisBoostTask(dealer.crisisBonus);
            }
        };
    },

    /**
     * 特定の属性のカード&プレイヤーのMPを増加させるスキル.
     * @param {Array<string|number>} option - 「効果対象を説明する文字列,
     * 対象の属性のインデックス, 効果量をパーセントで表した値」をこの順番に並べた配列
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    suitSpecificBoost: function(option, cap = null, desc = null){
        const [str, mark, percent] = option;
        return {
            caption: cap || "属性強化",
            desc: desc || `${str}カードのMPが${percent}%アップ`,
            *effect(dealer){
                yield* dealer.suitSpecificBoost(str, Suits.indexOf(mark), percent);
            }
        };
    },

    /**
     * 敵にダメージを与えるスキル.
     * @param {number} percent - ダメージ量がプレイヤーのMPの何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    attack: function(percent, cap = null, desc = null){
        return {
            caption: cap || "強力な一撃",
            desc: desc || `MPの${percent}%ダメージ`,
            *effect(dealer){
                yield* dealer.damage(percent);
            }
        };
    },

    /**
     * 敵に複数回ダメージを与えるスキル.
     * @param {number[]} option - 「1回のダメージ量がプレイヤーMPの何%か指定する値」と
     * 「攻撃回数」をこの順番に並べた配列
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    multiAttack: function(option, cap = null, desc = null){
        const [percent, n] = option;
        return {
            caption: cap || "多段攻撃",
            desc: desc || `MPの${percent}%のダメージを${n}回与える`,
            *effect(dealer){
                for(let i = 0; i < n; i++){
                    yield* dealer.damage(percent);
                    if(dealer.enemyHP() <= 0) break;
                    else yield* dealer.wait(60);
                }
            }
        };
    },

    /**
     * 敵のMPを減少させるスキル.
     * @param {number} percent - 増加量が敵の基本MPの何%か指定する
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    reduceEnemyMP: function(percent, cap = null, desc = null){
        return {
            caption: cap || "敵MPの低下",
            desc: desc || `相手のMPを${percent}%減少`,
            *effect(dealer){
                yield* dealer.reduceEnemyMP(percent);
            }
        };
    },

    /**
     * 各ターンの制限時間を延長するスキル.
     * @param {number} n - 延長される秒数
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    extendTime: function(n, cap = null, desc = null){
        return {
            caption: cap || "制限時間延長",
            desc: desc || `制限時間を${n}秒延長`,
            *effect(dealer){
                yield* dealer.extendTime(n);
            }
        };
    },

    /**
     * HP, SGを全回復して1ターン目に戻すスキル.
     * @param {number} n - 使用しないが型を揃えるために存在している
     * @param {string} [cap=null] - スキル名. nullのときはデフォルトの名前が使われる
     * @param {string} [desc=null] - 効果の説明文. nullのときはデフォルトの説明が使われる
     * @returns {PlayerSkill_skill} 生成されたスキル
     */
    timeWarp: function(n, cap = null, desc = null){
        return {
            caption: cap || "時間跳躍",
            desc: desc || `HPとソウルジェムを回復しターン数回復`,
            *effect(dealer){
                yield* dealer.timeWarp();   // 引数は使わない
            }
        };
    }
};


// (b) 複合属性のカードの画像

/*
 * 基本属性のカードの画像は cardimages.png から読み込む.
 * 一方, 複合属性のカードの画像は, 各マークの画像を組み合わせて
 * プログラム内で生成する.
 */

/**
 * 複合属性のカードを描画するクラスの土台を提供する.
 * サブクラスは次のメソッドを実装する.
 * - paintBackground(ctx, marks)
 * - paintMark(ctx, pics)
 * - paintCost(ctx, x, y, marks, n)
 *
 * @class
 * @prop {number} width - カードの横幅
 * @prop {number} height - カードの縦幅
 * @prop {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
 * @prop {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
 * 指定された座標にカードの左上端があるものとしてカードのコストを描画する (サブクラスが実装する)
 */
class Polysuit{
    /**
     * 外枠などに使う配色.
     * @type {string[]}
     */
    static ccolors = [
        "rgb(204,96,96)", "rgb(17,17,119)", "rgb(96,96,204)",
        "rgb(192,192,0)", "rgb(186,115,87)", "rgb(207,141,154)"
    ];

    /**
     * カードの地の部分などに使う配色.
     * @type {string[]}
     */
    static wcolors = [
        "rgb(238,128,128)", "rgb(119,119,204)", "rgb(192,192,255)",
        "rgb(208,208,0)", "rgb(222,150,122)", "rgb(247,202,203)"
    ];

    /**
     * @param {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
     * @param {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
     * @param {number} width - カードの横幅
     * @param {number} height - カードの縦幅
     */ 
    constructor(images, caches, width, height){
        this.images = images;
        this.caches = caches;
        this.width = width;
        this.height = height;
        for(let i = 0; i < Suits.length; i++){
            this.images.load(Suits[i], `./image/${Suits[i]}.png`);
        }
    }

    /**
     * グラデーションを作成する.
     * @param {CanvasRenderingContext2D} ctx - 描画処理に用いているコンテクスト
     * @param {string} c1 - 左上に設定する色
     * @param {string} c2 - 右下に設定する色
     * @param {number} [off1=0] - カードの左上の角よりも指定された値だけ外側に
     * 飛び出した場所を第1色の配置位置とする (x座標, y座標のそれぞれからoff1を引く)
     * @param {number} [off2=0] - カードの右下の角よりも指定された値だけ外側に
     * 飛び出した場所を第2色の配置位置とする (x座標, y座標のそれぞれにoff1を足す)
     * @param {string} [med=null] - 中間色を設定する場合はそのカラーコードを指定する
     * @returns {CanvasGradient} 作成されたグラデーション
     */
    gradation(ctx, c1, c2, off1=0, off2=0, med=null){
        const g = ctx.createLinearGradient(-off1, -off1, this.width+off2, this.height+off2);
        g.addColorStop(0, c1);
        if(med) g.addColorStop(0.5, med); 
        g.addColorStop(1, c2);
        return g;
    }

    /**
     * marksで指定された複合属性のカード画像が既に作成済みならそれを返す.
     * そうでない場合, サブクラスの
     * - this.paintBackgroud
     * - this.paintMark
     *
     * を使用してカード画像を作り, これをthis.cachesに登録する.
     * その後, 生成したカード画像を返す.
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @returns {HTMLCanvasElement} その複合属性のカード画像
     */
    getCache(marks){
        const newName = getPolysuitName(marks);
        if(!this.caches.get(newName)){
            const pics = marks.map((m) => this.images.get(PrimitiveSuits[m]));
            const p = this.caches.createCache(newName, this.width, this.height, (ctx) => {
                ctx.save();
                this.paintBackground(ctx, marks);
                this.paintMark(ctx, pics);
                ctx.restore();
            });
        }
        return this.caches.get(newName);
    }
}

/**
 * 2つのマークを持つカードの画像を生成・管理するためのクラス.
 * @class
 * @prop {number} width - カードの横幅
 * @prop {number} height - カードの縦幅
 * @prop {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
 * @prop {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
 * @extends Polysuit
 */
class DuosuitGenerator extends Polysuit{
    /**
     * カードの背景を描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     */
    paintBackground(ctx, marks){
        ctx.save();
        const [m1, m2] = marks;
        ctx.fillStyle = this.gradation(ctx, Polysuit.ccolors[m1], Polysuit.ccolors[m2]);
        ctx.fillRect(0, 0, this.width, this.height);
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[m1], Polysuit.wcolors[m2], 50, 250);
        ctx.fillRect(3, 3, this.width-6, this.height-6);
        ctx.restore();
    }

    /**
     * カードのマークを描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {HTMLImageElement[]} pics - 含まれる基本属性のマークの画像
     */
    paintMark(ctx, pics){
        ctx.save();
        const [img1, img2] = pics;
        const w = this.width * 0.7;
        const h = this.height * 0.7;
        ctx.drawImage(img1, 0, 0, this.width, this.height,
                      0, this.height*0.04, w, h);
        ctx.drawImage(img2, 0, 0, this.width, this.height,
                      this.width-w, this.height-h, w, h);
        ctx.restore();
    }

    /**
     * 指定された座標にカードの左上端があるものとしてカードのコストを描画する.
     * 他の描画メソッドとは違い, コストはキャッシュ画像には書き込まず,
     * PrismaticCardのpaintメソッドから毎フレーム呼び出される (さもなければ
     * キャッシュする画像の枚数がコストの種類数だけ倍増してしまう).
     * そのため, 基点となる(x,y)の情報が必要になる.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number} x - カードの配置位置のx座標
     * @param {number} y - カードの配置位置のy座標
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @param {number} n - カードのコスト
     */
    paintCost(ctx, x, y, marks, n){
        ctx.save();
        const [m1, m2] = marks;
        ctx.strokeStyle = Polysuit.ccolors[m2];
        ctx.lineWidth = 4;
        ctx.fillStyle = "white";
        ctx.font = "italic bold 30px Serif";
        ctx.textAlign = "center";
        ctx.strokeText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.fillText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.restore();
    }
}

/**
 * 3つのマークを持つカードの画像を生成・管理するためのクラス.
 * @class
 * @prop {number} width - カードの横幅
 * @prop {number} height - カードの縦幅
 * @prop {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
 * @prop {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
 * @extends Polysuit
 */
class TriosuitGenerator extends Polysuit{
    /**
     * カードの背景を描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     */
    paintBackground(ctx, marks){
        ctx.save();
        const [m1, m2, m3] = marks;
        ctx.fillStyle = this.gradation(ctx, Polysuit.ccolors[m2], Polysuit.ccolors[m1], 0, 0);
        ctx.fillRect(0, 0, this.width, this.height);
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[m1], Polysuit.wcolors[m3], 50, 250);
        ctx.fillRect(3, 3, this.width-6, this.height-6);
        ctx.restore();
    }

    /**
     * カードのマークを描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {HTMLImageElement[]} pics - 含まれる基本属性のマークの画像
     */
    paintMark(ctx, pics){
        ctx.save();
        const [img1, img2, img3] = pics;
        const w = this.width * 0.65;
        const h = this.height * 0.65;
        ctx.drawImage(img1, 0, 0, this.width, this.height,
                      this.width*0.35, 0, w, h);
        ctx.drawImage(img2, 0, 0, this.width, this.height,
                      0, this.height*0.15, w, h);
        ctx.drawImage(img3, 0, 0, this.width, this.height,
                      this.width*0.32, this.height*0.37, w, h);
        ctx.restore();
    }

    /**
     * 指定された座標にカードの左上端があるものとしてカードのコストを描画する.
     * 他の描画メソッドとは違い, コストはキャッシュ画像には書き込まず,
     * PrismaticCardのpaintメソッドから毎フレーム呼び出される (さもなければ
     * キャッシュする画像の枚数がコストの種類数だけ倍増してしまう).
     * そのため, 基点となる(x,y)の情報が必要になる.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number} x - カードの配置位置のx座標
     * @param {number} y - カードの配置位置のy座標
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @param {number} n - カードのコスト
     */
    paintCost(ctx, x, y, marks, n){
        ctx.save();
        const [m1, m2, m3] = marks;
        ctx.strokeStyle = this.gradation(ctx, Polysuit.wcolors[m3], Polysuit.wcolors[m2], 0, 0);
        ctx.lineWidth = 4;
        ctx.fillStyle = "white";
        ctx.font = "italic bold 30px Serif";
        ctx.textAlign = "center";
        ctx.strokeText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.fillText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.restore();
    }
}

/**
 * 4つのマークを持つカードの画像を生成・管理するためのクラス.
 * @class
 * @prop {number} width - カードの横幅
 * @prop {number} height - カードの縦幅
 * @prop {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
 * @prop {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
 * @extends Polysuit
 */
class QuartetsuitGenerator extends Polysuit{
    /**
     * カードの背景を描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     */
    paintBackground(ctx, marks){
        ctx.save();
        const [m1, m2, m3, m4] = marks;
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[m2], Polysuit.wcolors[m4], 0, 0);
        ctx.fillRect(0, 0, this.width, this.height);
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[m1], Polysuit.wcolors[m3], 50, 250);
        ctx.fillRect(3, 3, this.width-6, this.height-6);
        ctx.restore();
    }

    /**
     * カードのマークを描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {HTMLImageElement[]} pics - 含まれる基本属性のマークの画像
     */
    paintMark(ctx, pics){
        ctx.save();
        const [img1, img2, img3, img4] = pics;
        const w = this.width * 0.52;
        const h = this.height * 0.52;
        ctx.drawImage(img1, 0, 0, this.width, this.height,
                      this.width*0.25, -2, w, h);
        ctx.drawImage(img2, 0, 0, this.width, this.height,
                      0, this.height*0.25, w, h);
        ctx.drawImage(img3, 0, 0, this.width, this.height,
                      this.width*0.25, this.height*0.5, w, h);
        ctx.drawImage(img4, 0, 0, this.width, this.height,
                      this.width*0.5, this.height*0.25, w, h);
        ctx.restore();
    }

    /**
     * 指定された座標にカードの左上端があるものとしてカードのコストを描画する.
     * 他の描画メソッドとは違い, コストはキャッシュ画像には書き込まず,
     * PrismaticCardのpaintメソッドから毎フレーム呼び出される (さもなければ
     * キャッシュする画像の枚数がコストの種類数だけ倍増してしまう).
     * そのため, 基点となる(x,y)の情報が必要になる.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number} x - カードの配置位置のx座標
     * @param {number} y - カードの配置位置のy座標
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @param {number} n - カードのコスト
     */
    paintCost(ctx, x, y, marks, n){
        ctx.save();
        const [m1, m2, m3, m4] = marks;
        ctx.strokeStyle = Polysuit.ccolors[m2];
        ctx.lineWidth = 4;
        ctx.fillStyle = "white";
        ctx.font = "italic bold 30px Serif";
        ctx.textAlign = "center";
        ctx.strokeText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.fillText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.restore();
    }
}

/**
 * 「魔法少女」のカードのうち, なぎさを含まないものについて
 * 画像を生成・管理するためのクラス.
 * @class
 * @prop {number} width - カードの横幅
 * @prop {number} height - カードの縦幅
 * @prop {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
 * @prop {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
 * @extends Polysuit
 */
class QuintetsuitGenerator extends Polysuit{
    /**
     * カードの背景を描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     */
    paintBackground(ctx, marks){
        ctx.save();
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[1], Polysuit.wcolors[2], 50, 250);
        ctx.fillRect(0, 0, this.width, this.height);
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[0], Polysuit.wcolors[3], 50, 250);
        ctx.fillRect(3, 3, this.width-6, this.height-6);
        ctx.restore();
    }

    /**
     * カードのマークを描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {HTMLImageElement[]} pics - 含まれる基本属性のマークの画像
     */
    paintMark(ctx, pics){
        ctx.save();
        const [img1, img2, img3, img4, img5] = pics;
        const w = this.width * 0.5;
        const h = this.height * 0.5;
        ctx.drawImage(img1, 0, 0, this.width, this.height,
                      0, 0, w, h);
        ctx.drawImage(img2, 0, 0, this.width, this.height,
                      this.width*0.5, 0, w, h);
        ctx.drawImage(img3, 0, 0, this.width, this.height,
                      0, this.height*0.5, w, h);
        ctx.drawImage(img5, 0, 0, this.width, this.height,
                      this.width*0.5, this.height*0.5, w, h);
        ctx.drawImage(img4, 0, 0, this.width, this.height,
                      this.width*0.25, this.height*0.25, w, h);
        ctx.restore();
    }

    /**
     * 指定された座標にカードの左上端があるものとしてカードのコストを描画する.
     * 他の描画メソッドとは違い, コストはキャッシュ画像には書き込まず,
     * PrismaticCardのpaintメソッドから毎フレーム呼び出される (さもなければ
     * キャッシュする画像の枚数がコストの種類数だけ倍増してしまう).
     * そのため, 基点となる(x,y)の情報が必要になる.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number} x - カードの配置位置のx座標
     * @param {number} y - カードの配置位置のy座標
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @param {number} n - カードのコスト
     */
    paintCost(ctx, x, y, marks, n){
        ctx.save();
        ctx.strokeStyle = Polysuit.ccolors[5];
        ctx.lineWidth = 4;
        ctx.fillStyle = "white";
        ctx.font = "italic bold 30px Serif";
        ctx.textAlign = "center";
        ctx.strokeText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.fillText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.restore();
    }

    /**
     * 魔法少女5人組のカード画像が既に作成済みならそれを返す.
     * そうでない場合, カード画像を生成してthis.cacheに登録する.
     * その後, 生成したカード画像を返す.
     *
     * Quintetの場合, marksとして何が渡された場合でも
     * 必ずmarksを [0, 1, 2, 3, 4] に置き換えて作業を行う (並び順が違うだけの
     * 画像を無駄に増やさないため).
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @returns {HTMLCanvasElement} その複合属性のカード画像
     */
    getCache(marks){
        marks = [0, 1, 2, 3, 4];
        const newName = getPolysuitName(marks);
        if(!this.caches.get(newName)){
            const pics = marks.map((m) => this.images.get(PrimitiveSuits[m]));
            const p = this.caches.createCache(newName, this.width, this.height, (ctx) => {
                ctx.save();
                this.paintBackground(ctx, marks);
                this.paintMark(ctx, pics);
                ctx.restore();
            });
        }
        return this.caches.get(newName);
    }
}

/**
 * なぎさを含む「魔法少女」のカードの画像を生成・管理するためのクラス.
 * @class
 * @prop {number} width - カードの横幅
 * @prop {number} height - カードの縦幅
 * @prop {stdgam.ImagePool} images - 必要な画像を読み込むために使うImagePool
 * @prop {stdgam.CachePool} caches - 生成した画像を登録するために使うCachePool
 * @extends Polysuit
 */
class SestetsuitGenerator extends Polysuit{
    /**
     * カードの背景を描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     */
    paintBackground(ctx, marks){
        ctx.save();
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[1], Polysuit.wcolors[2], 50, 250);
        ctx.fillRect(0, 0, this.width, this.height);
        ctx.fillStyle = this.gradation(ctx, Polysuit.wcolors[0], Polysuit.wcolors[3], 50, 250);
        ctx.fillRect(3, 3, this.width-6, this.height-6);
        ctx.restore();
    }

    /**
     * カードのマークを描画する.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {HTMLImageElement[]} pics - 含まれる基本属性のマークの画像
     */
    paintMark(ctx, pics){
        ctx.save();
        const [img1, img2, img3, img4, img5, img6] = pics;
        const w = this.width * 0.5;
        const h = this.height * 0.5;
        ctx.drawImage(img6, 0, 0, this.width, this.height,
                      0, this.height*0.12, w, h);
        ctx.drawImage(img2, 0, 0, this.width, this.height,
                      this.width*0.5, this.height*0.1, w, h);
        ctx.drawImage(img3, 0, 0, this.width, this.height,
                      0, this.height*0.38, w, h);
        ctx.drawImage(img5, 0, 0, this.width, this.height,
                      this.width*0.5, this.height*0.4, w, h);
        ctx.drawImage(img1, 0, 0, this.width, this.height,
                      this.width*0.25, this.height*0, w, h);
        ctx.drawImage(img4, 0, 0, this.width, this.height,
                      this.width*0.25, this.height*0.5, w, h);
        ctx.restore();
    }

    /**
     * 指定された座標にカードの左上端があるものとしてカードのコストを描画する.
     * 他の描画メソッドとは違い, コストはキャッシュ画像には書き込まず,
     * PrismaticCardのpaintメソッドから毎フレーム呼び出される (さもなければ
     * キャッシュする画像の枚数がコストの種類数だけ倍増してしまう).
     * そのため, 基点となる(x,y)の情報が必要になる.
     * @param {CanvasRenderingContext2D} ctx - 描画に使うコンテクスト
     * @param {number} x - カードの配置位置のx座標
     * @param {number} y - カードの配置位置のy座標
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @param {number} n - カードのコスト
     */
    paintCost(ctx, x, y, marks, n){
        ctx.save();
        ctx.strokeStyle = Polysuit.ccolors[5];
        ctx.lineWidth = 4;
        ctx.fillStyle = "white";
        ctx.font = "italic bold 30px Serif";
        ctx.textAlign = "center";
        ctx.strokeText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.fillText(`${n}`, x+this.width*0.8, y+10+this.height*0.8);
        ctx.restore();
    }

    /**
     * 魔法少女5人組のカード画像が既に作成済みならそれを返す.
     * そうでない場合, カード画像を生成してthis.cacheに登録する.
     * その後, 生成したカード画像を返す.
     *
     * Sestetの場合, marksとして何が渡された場合でも
     * 必ずmarksを [0, 1, 2, 3, 4, 5] に置き換えて作業を行う (並び順が違うだけの
     * 画像を無駄に増やさないため).
     * @param {number[]} marks - 含まれる基本属性をSuitsにおけるインデックスで指定したリスト
     * @returns {HTMLCanvasElement} その複合属性のカード画像
     */
    getCache(marks){
        marks = [0, 1, 2, 3, 4, 5];
        const newName = getPolysuitName(marks);
        if(!this.caches.get(newName)){
            const pics = marks.map((m) => this.images.get(PrimitiveSuits[m]));
            const p = this.caches.createCache(newName, this.width, this.height, (ctx) => {
                ctx.save();
                this.paintBackground(ctx, marks);
                this.paintMark(ctx, pics);
                ctx.restore();
            });
        }
        return this.caches.get(newName);
    }
}


// (c) カードを表すクラスの実装

/**
 * 基本属性を持つカードのクラス. 最初に Card.init(GE, width, height) を
 * 実行してからインスタンスの生成を行う.
 *
 * 各インスタンスは次のプロパティを持つ.
 * - mark
 * - value
 * - MP
 * - skill (任意)
 * - cardAtlasID (任意)
 *
 * (1) mark は, このカードの属性をSuitsにおけるインデックスで表した数値である.
 * 便宜上, このクラスのコメントでは「マーク数値」と略す.
 * mark の取りうる値は 0 ~ (PrimitiveSuits.length-1) の範囲の整数である.
 *
 * (2) value はカードの「コスト」を表す. 範囲チェック・整数チェックは行わないが,
 * 0 ~ 10 の範囲の整数であることを想定して扱われる.
 * (整数でないときの挙動は保証しない!)
 *
 * ただし, value == 0 のカードは「カードが存在しない」状態を表すインスタンスとして
 * 扱われる. たとえば, paintメソッドで何も描画されない.
 *
 * (3) MP はこのカードの基本攻撃力を表す. 実際はこの値を直接使うことは滅多に無く,
 * MPBoostBySuitの補正を施した値を getMP() メソッドで計算して使う.
 *
 * 以上の3要素はどんなインスタンスも必ず定義されている.
 * これに対し, 他の2つは定義されている場合も未定義の場合も両方ある.
 *
 * (4) skill はこのカードの持つスキルを表す.
 * 原則として PlayerSkill に属するいずれかの生成関数で作成されたオブジェクトを持つ.
 *
 * (5) cardAtlasID はcardlist.jsにおけるこのカードのIDである.
 * cardAtlasに登録されているカードには自動的に付与される.
 * @class
 * @prop {number} mark - このカードのマーク数値
 * @prop {number} value - このカードのコスト
 * @prop {PlayerSkill_skill} [skill] - このカードが持つスキル
 * @prop {string} [cardAtlasID] - cardlist.jsにおけるこのカードのID
 */
class Card{
    /**
     * スキル持ちカードのマーカーを表示する場合true, 表示しないときfalse.
     * このフラグはPrismaticCardからも参照される
     * @type {boolean}
     */
    static MarkerFlag = true;

    /**
     * 該当するカードが存在しないことを表すオブジェクト.
     * コストが0なのでpaint()で何も描画されない
     * @type {Card}
     */
    static NullCard = Object.freeze(new Card(0, 0));

    /**
     * Cardクラスを初期化する.
     * 具体的には, "CARDIMAGES"で登録されている画像をGE.imagesから読み込み,
     * stdgam.ImageCutterで分割する. 以降, これをカードの描画に用いる.
     * @param {stdgam.GameEngine} GE - 画像のロードに用いるGameEngine
     * @param {number} width - カードの横幅
     * @param {number} height - カードの縦幅
     */
    static init(GE, width, height){
        this.IC = new ImageCutter(GE.images.get("CARDIMAGES"), width, height);
        this.width = width;
        this.height = height;
    }

    /**
     * カードの整列に用いる比較関数.
     * もしマーク数値が違えばマーク数値の大小を比較する.
     * そうでないとき, コストの大小を比較する.
     * @param {Cardlike} c1 - 1個目のオブジェクト
     * @param {Cardlike} c2 - 2個目のオブジェクト
     * @returns {number} 比較結果を表す数字
     */
    static compare = function(c1, c2){
        if(c1.mark != c2.mark){
            return c1.mark - c2.mark;
        }
        return c1.value - c2.value;
    }

    /**
     * 指定されたマーク数値, コスト, MPを持つカードを生成する.
     * @param {number} [mark] - このカードのマーク数値
     * @param {number} [n=0] - このカードのコスト
     * @param {?number} [mp=null] - このカードのMP. 省略時は n*20 で代用する
     */
    constructor(mark = 0, n = 0, mp = null){
        this.mark = mark;
        this.mask = ChainMask[mark];
        this.value = n;
        this.MP = mp || n * 20;
    }

    /**
     * MPBoostBySuitの補正を適用した後のMPを計算する.
     * @returns {number} 計算結果
     */
    getMP(){
        return Math.floor(this.MP * MPBoostBySuit.get(this.mark));
    }

    /**
     * 指定された座標を左上端としてこのカードを描画する.
     * ただし, コストが 0 のカードは何も描画しない.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {CanvasRenderingContext2D} ctx - 描画処理に用いるコンテクスト
     * @param {number} x - 描画位置のx座標
     * @param {number} y - 描画位置のy座標
     */
    paint(GE, ctx, x, y){
        if(this.value == 0) return;
        Card.IC.paint(ctx, x, y, this.value, this.mark);
        if(this.skill && Card.MarkerFlag){
            const indicator = this.version ? "*".repeat(this.version) : "*!";
            ctx.save();
            ctx.font = "bold 20px Sans-Serif";
            ctx.fillStyle = "white";
            ctx.fillText(indicator, x+Card.width*0.1, y+Card.height*0.2);
            ctx.restore();
        }
    }
}

/**
 * 複合属性を持つカードのクラス. 最初に  PrismaticCard.init(GE, width, height) を
 * 実行してからインスタンスの生成を行う.
 *
 * Cardクラスと同様, 各インスタンスは次のプロパティを持つ.
 * - mark
 * - value
 * - MP
 * - skill (任意)
 * - CardAtlassId (任意)
 *
 * 各要素の意味はCardクラスと共通である. ただし, 複合属性を持つため,
 * mark の取りうる値は PrimitiveSuits.length ~ (Suits.length-1) の範囲の整数である.
 *
 * @class
 * @prop {number} mark - このカードのマーク数値
 * @prop {number} value - このカードのコスト
 * @prop {PlayerSkill_skill} [skill] - このカードが持つスキル
 * @prop {string} [cardAtlasID] - cardlist.jsにおけるこのカードのID
 */
class PrismaticCard{
    /**
     * PrismaticCardクラスを初期化する.
     * 具体的には, 複合カードの描画に必要なオブジェクト (より正確には,
     * Polysuitの「すべての」サブクラスのインスタンスを1つずつ) を生成する.
     * 以降, これらをカードの描画に用いる.
     * @param {stdgam.GameEngine} GE - 画像のロードに用いるGameEngine
     * @param {number} width - カードの横幅
     * @param {number} height - カードの縦幅
     */
    static init(GE, width, height){
        PrismaticCard.generators = [
            new DuosuitGenerator(GE.images, GE.caches, width, height),
            new TriosuitGenerator(GE.images, GE.caches, width, height),
            new QuartetsuitGenerator(GE.images, GE.caches, width, height),
            new QuintetsuitGenerator(GE.images, GE.caches, width, height),
            new SestetsuitGenerator(GE.images, GE.caches, width, height)
        ];
    }

    /**
     * 複合属性のカードを生成する. どの基本属性を構成要素として含むかは,
     * マーク数値のリスト marks により指定する.
     * @param {number[]} [marks] - マーク数値のリスト
     * @param {number} [n=0] - このカードのコスト
     * @param {?number} [mp=null] - このカードのMP. 省略時は n*20 で代用する
     */
    constructor(marks, n, mp = null){
        const key = getPolysuitName(marks);
        if(!Suits.includes(key)){
            Suits.push(key);
            ChainMask.push(getPolysuitMask(marks));
        }
        this.components = marks;
        this.mark = Suits.indexOf(key);
        this.mask = ChainMask[this.mark];
        this.value = n;
        this.MP = mp || n * 20;

        this.generator = PrismaticCard.generators[marks.length-2];
        this.image = this.generator.getCache(marks);
    }

    /**
     * MPBoostBySuitの補正を適用した後のMPを計算する.
     * @returns {number} 計算結果
     */
    getMP(){
        return Math.floor(this.MP * MPBoostBySuit.get(this.mark));
    }

    /**
     * 指定された座標を左上端としてこのカードを描画する.
     * ただし, コストが 0 のカードは何も描画しない.
     * @param {stdgam.GameEngine} GE - この処理に用いるGameEngine
     * @param {CanvasRenderingContext2D} ctx - 描画処理に用いるコンテクスト
     * @param {number} x - 描画位置のx座標
     * @param {number} y - 描画位置のy座標
     */
    paint(GE, ctx, x, y){
        if(this.value == 0) return;
        ctx.save();
        ctx.drawImage(this.image, x, y);
        this.generator.paintCost(ctx, x, y, this.components, this.value);
        if(this.skill && Card.MarkerFlag){
            ctx.save();
            const indicator = this.version ? "*".repeat(this.version) : "*!";
            ctx.font = "bold 20px Sans-Serif";
            ctx.fillStyle = "white";
            ctx.fillText(indicator, x+Card.width*0.1, y+Card.height*0.2);
            ctx.restore();
        }
        ctx.restore();
    }
}


// #3. カードに関連するクラス

/*
 * カードの集合にはいくつか種類がある.
 * (1) 単なる配列 (Array)
 *     主に cards などのように表す.
 * (2) CardSet
 *     要素の重複がなく Card.compare でソートされたコレクション.
 *     主に cardset または deckSet のように表す.
 * (3) Deck
 *     shift() や shuffle() などバトルに必要な機能を持たせたコレクション.
 *     主に deck または deckObj のように表す.
 */

/**
 * 重複を持たず, Card.compareでソートされたカードの集合を表す.
 * Deckクラスが「実際にゲーム中に使用する山札」を表すのに対し,
 * このクラスは「デッキに採用するカードのカタログ」として扱われる.
 * たとえば, Deckクラスには「シャッフルする」という概念が存在するが,
 * CardSetは常にCard.compareの順序でソートされている.
 * @class
 */
class CardSet{
    #cards;

    /**
     * cardsと同じ要素を持つインスタンスを生成する.
     * shallow copyをとるので, このインスタンスを変更してもcardsには影響しない.
     * @param {Cardlike[]} cards - 使用するカードを並べた配列
     */
    constructor(cards){
        this.init(cards);
    }

    /**
     * cardsと同じ要素を持つように初期化する.
     * shallow copyをとるので, このインスタンスを変更してもcardsには影響しない.
     * @param {Cardlike[]} cards - 使用するカードを並べた配列
     */
    init(cards){
        this.#cards = [...cards];
        this.#cards.sort(Card.compare);
    }

    /** @returns {number} 含まれているカードの枚数 */
    size(){
        return this.#cards.length;
    }

    /**
     * このCardSetに含まれるカードを並べた配列を新しく生成する.
     * @returns {Cardlike[]} 生成された配列
     */
    cards(){
        return [...this.#cards];
    }

    /**
     * cardがこのCardSetに含まれているか判定する.
     * @param {Cardlike} card - 調べるカード
     * @returns {boolean} 含まれているときtrue, そうでないときfalse
     */
    includes(card){
        return this.#cards.includes(card);
    }

    /**
     * 前からn番目のカードを返す (nは0から数え始める).
     * もし該当するカードが無ければCard.NullCardを返す.
     * @returns {Cardlike} n番目のカード. 該当するカードが無ければCard.NullCard
     */
    watch(n){
        if(n < this.#cards.length) return this.#cards[n];
        return Card.NullCard;
    }

    /**
     * cardをこのCardSetに追加する. もし既にCardSetの中に存在する場合は何もしない.
     * @param {Cardlike} card - 追加するカード
     */
    push(card){
        if(!this.#cards.includes(card)) this.#cards.push(card);
        this.#cards.sort(Card.compare);
    }

    /**
     * 前からn番目の要素をこのCardSetから削除し, そのカードを返す
     * (nは0から数え始める). もし該当するカードが無ければCard.NullCardを返す.
     * @param {number} n - 抜き出すカードのインデックス
     * @returns {Cardlike} 抜き出されたカード. 該当するカードが無ければCard.NullCard
     */
    slice(n){
        const card = this.watch(n);
        if(card !== Card.NullCard) this.#cards.splice(n, 1);
        return card;
    }
}

/**
 * バトルで使うデッキを実装するクラス.
 * シャッフルしてカードを並び替えたり, 先頭から1枚ずつカードを引いたりできる.
 * @class
 */
class Deck{
    #cards;

    /**
     * cardsに格納されているカード達をそのままの並び順で使って
     * インスタンスを生成する. shallow copyをとるので, このインスタンスを
     * 変更してもcardsには影響しない.
     * @param {Cardlike[]} cards - 使用するカードを並べた配列
     */
    constructor(cards){
        this.#cards = [...cards];
    }

    /**
     * このデッキを複製して新しいインスタンスを生成する.
     * 片方を変更しても, もう片方に影響しない.
     * @returns {Deck} 複製して作られたオブジェクト
     */
    clone(){
        return new Deck(this.#cards);
    }

    /**
     * このデッキに含まれるカードを並べた配列を新しく生成する.
     * @returns {Cardlike[]} 生成された配列
     */
    cards(){
        return [...this.#cards];
    }

    /** @returns {number} 含まれているカードの枚数 */
    size(){
        return this.#cards.length;
    }

    /** @returns {boolean} このデッキが空ならtrue, そうでなければfalse */
    isEmpty(){
        return this.#cards.length == 0;
    }

    /**
     * このデッキの一番最初のカードを削除して, そのカードを返す.
     * もし該当するカードが無ければ, Card.NullCardを返す
     * @returns {Cardlike} 取り除かれたカード. 該当するカードが無ければCard.NullCard
     */
    shift(){
        if(this.isEmpty()){
            return Card.NullCard;
        }
        const card = this.#cards[0];
        this.#cards.splice(0, 1);
        return card;
    }

    /**
     * このデッキから指定されたカードを取り除く.
     * もし複数の箇所に含まれるなら, それらをすべて削除する.
     * @param {Cardlike} card - 削除するカード
     */
    remove(card){
        this.#cards = this.#cards.filter((e) => e !== card);
    }

    /**
     * 前からn番目のカードを返す (nは0から数え始める).
     * もし該当するカードが無ければ, Card.NullCardを返す
     * @returns {Cardlike} n番目のカード. 該当するカードが無ければCard.NullCard
     */
    watch(i){
        if(this.#cards.length <= i){
            return Card.NullCard;
        }
        return this.#cards[i];
    }

    /**
     * このデッキの中身をシャッフルする.
     */
    shuffle(){
        let i, len;
        i = this.#cards.length;
        while(i > 0){
            let j = Math.floor(Math.random()*i);
            let t = this.#cards[(--i)];
            this.#cards[i] = this.#cards[j];
            this.#cards[j] = t;
        }
    }
}

/**
 * 場に出されたカードを管理するクラス. 主に次の3つの情報を公開する.
 * - このターン出されたカードの履歴
 * - 現在のチャージMP
 * - 成立しているスキルのリスト
 *
 * ここで, チャージMPは次の計算式で算出される.
 * 1. ベースMP = すべてのカードのgetMP()の値を合計した値
 * 2. 選択肢補正 = (プレイヤーがSG回復を選択した ? 0.5 : 1)
 * 3. チャージMP = Math.floor( Math.floor(ベースMP * チャージボーナス) *  選択肢補正)
 *
 * @class
 */
class Pool{
    #cards;
    #baseMP;
    #chargeBonus;
    #correctionFlag;
    #hyped;
    #skills;
    #ChainFunc;

    /**
     * 空のインスタンスを作る.
     * @param {number} [version=1] - 使用するコンボ成立条件のバージョン
     */
    constructor(version = 1){
        this.init();
        this.#skills = [];
        this.#ChainFunc = (version == 1 ? ChainFunc : ChainFuncVer2);
    }

    /** @returns {number} 場に出されているカードの枚数 */
    size(){
        return this.#cards.length;
    }

    /** @returns {number} 保持されているスキルの個数 */
    skillCount(){
        return this.#skills.length;
    }

    /**
     * ターンが切り替わったときの処理を行う.
     * (前ターンに成立したスキルは持ち越される)
     */
    init(){
        this.#cards = [];
        this.#baseMP = 0;
        this.#chargeBonus = 0;
        this.#correctionFlag = false;
        this.#hyped = false;
    }

    /**
     * チャージMPの値を計算する.
     * @returns {number} 現在のチャージMPの値
     */
    chargedMP(){
        if(this.#chargeBonus == 0 && !this.#correctionFlag) return this.#baseMP;
        const v = this.#baseMP + Math.floor(this.#baseMP * this.#chargeBonus / 100);
        return this.#correctionFlag ? Math.floor(v / 2) : v;
    }

    /**
     * 指定した値をチャージボーナスに加算する. 加算する量はベースMPに対するパーセント表示で表現する.
     * @param {number} percent - 加算する量をベースMPに対するパーセント表示で表した値
     */
    addChargeBonus(percent){
        this.#chargeBonus += percent;
    }

    /**
     * fが真ならば選択肢補正を0.5にする. 一方, fが偽ならば選択肢補正を1にする.
     * @param {boolean} f - SG回復が選択された場合はtrue, 他の選択肢が選ばれた場合はfalseを指定する
     */
    setCorrectionFlag(f){
        this.#correctionFlag = f;
    }

    /**
     * 一番最後に出したカードでコンボが成立していればtrueを返す.
     * @returns {boolean} コンボが成立していればtrue, そうでなければfalse
     */
    hyped(){
        return this.#hyped;
    }

    /**
     * 保持されているスキルのうち一番最初のものを削除し, これを返す.
     * 該当するスキルがなければ何もせずにundefinedを返す.
     * @returns {PlayerSkill_skill} 取り出されたスキル. 存在しなければundefined
     */
    shiftSkill(){
        return this.#skills.shift();
    }

    /**
     * ベースMPを再計算する
     * (MPBoostBySuitが変更された場合などに使用する)
     */
    recalculate(){
        this.#baseMP = 0;
        for(const card of this.#cards){
            this.#baseMP += card.getMP();
        }
    }

    /**
     * 場にカードを出す. 具体的には, 次の処理を行う.
     * 1. ベースMPを増加させる.
     * 2. cardをカードリストに追加する.
     * 3. コンボの判定を行い, 成立時はcard.skillをスキルリストに追加する.
     * このときhypeの値も更新する.
     *
     * ただし, コスト0のカードの場合は何もしない.
     * @param {Cardlike} card - 場に出すカード
     */
    push(card){
        if(card.value == 0) return;
        this.#baseMP += card.getMP();
        this.#cards.push(card);
        const len = this.#cards.length;
        this.#hyped = (len >= 3
                      && this.#ChainFunc(this.#cards[len-3], this.#cards[len-2], card)
                      && card.skill);
        if(this.#hyped){
            this.#skills.push(card.skill);
        }
    }

    /**
     * 前からn番目のカードを返す (nは0から数え始める).
     * もし該当するカードが無ければ, Card.NullCardを返す
     * @returns {Cardlike} n番目のカード. 該当するカードが無ければCard.NullCard
     */
    watch(n){
        if(this.#cards.length <= n){
            return Card.NullCard;
        }
        return this.#cards[n];
    }

    /**
     * 追加スキャンの処理を行う.
     * 基本的にはpush(card)と同じだが, この場合は無条件でコンボ成立扱いになる.
     * @param {Cardlike} card - 追加スキャンで読み込んだカード
     */
    extraScan(card){
        if(card.value == 0) return;
        this.push(card);
        if(card.skill){
            this.#hyped = true;  // 追加スキャンは無条件でスキルが発動
            this.#skills.push(card.skill);
        }
    }
}


// #4. カードデータベース

/*
 * cardlist.jsをロードすると RAW_CARD_DATA にカード情報が格納される.
 * これを元にカードのデータベースを作成する.
 */

/**
 * RAW_CARD_DATAに格納されているカード情報からカードを生成するオブジェクト.
 * 以下のメソッドを持つ.
 * - get(id) : 指定されたカードIDのカードを返す
 * - forEach(callback) : すべてのカード x に対して callback(x) を実行する
 *
 * CardAtlasにより生成されたオブジェクトはcardAtlasID属性に
 * カードのIDを代入される.
 * @namespace
 */
const CardAtlas = {
    /**
     * cardlist.jsにおけるsuit_stringをパースし, 含まれる属性のリストを返す.
     * 各属性は「Suitsにおけるその属性のインデックス」で表す.
     * もし未知の短縮名が含まれる場合は「なぎさ」として扱う.
     * @param {string} ss - パース対象の文字列
     * @returns {number[]} 含まれる属性のリスト. 各属性はSuitsにおけるインデックスで表す
     */
    parseSuitString(ss){
        if(ss == "Quintet") return [0, 1, 2, 3, 4];  // 5人の魔法少女
        if(ss == "Sestet") return [0, 1, 2, 3, 4, 5];  // 6人の魔法少女
        const splitted = ss.match(/.{1,2}/g);
        return splitted.map((e) => {
            const i = PrimitiveSuits.indexOf(e);
            if(i >= 0) return i;
            else return 5;  // "Bb" と"Re" は「なぎさ」として扱う
        });
    },

    /**
     * cardlist.jsのnagibato_codeに基づいて, cardにスキルを設定する.
     * もしsubが偽ならば何もしない.
     * @param {Cardlike} card - 操作対象のカード
     * @param {Object.<string,*>} sub - サブスキルの設定を格納する連想配列
     */
    setSkill(card, sub){
        if(!sub) return;
        const info = sub.nagibato_code;
        const fn = PlayerSkill[info[0]];
        if(!fn) throw new Error(`PlayerSkill.${info[0]}は未実装です`);
        card.skill = fn(...info.slice(1));
    },

    /**
     * RAW_CARD_DATAの情報に基づいて初期化する.
     */
    init(){
        if(!RAW_CARD_DATA) throw new Error("'cardlist.js' が読み込まれていません");
        const _data = {};
        for(const item of RAW_CARD_DATA){
            let card;

            const marks = this.parseSuitString(item.suit_string);
            const mp = Math.floor(item.MP / 10);  // サブカードとして使うので
            if(marks.length == 1){
                card = new Card(marks[0], item.cost, mp);
            }
            else{
                card = new PrismaticCard(marks, item.cost, mp);
            }
            card.cardAtlasID = item.id;
            card.version = parseInt(item.id[0]);
            this.setSkill(card, item.sub);
            _data[item.id] = card;
        }

        this.get = (id) => { return _data[id]; };
        this.forEach = (callback) => {
            for(const key in _data){ callback(_data[key]); }
        };
    },

    /**
     * 指定されたIDのカードオブジェクトを返す.
     * @param {string} id - カードのID
     */
    get(id){
        throw new Error("CardAtlas: 初期化前にgetが呼び出されました");
    },

    /**
     * すべてのカード x に対して callback(x) を実行する.
     * @param {function(Cardlike): void} callback - コールバック関数
     */
    forEach(callback){
        throw new Error("CardAtlas: 初期化前にforEachが呼び出されました");
    }
};