前回は場面の遷移処理を作りました。
これにより、画像を表示したりメッセージや選択肢を表示するのに配列変数の値を変更するだけで、処理を変更する必要がなくなりました。
しかし、この処理ではステータス画面やフィールド画面のような特殊なレイアウトの画面を表示することはできません。
今回はこの問題を解消する方法を記します。
以下の例では、選択肢から「ステータスを見る」をクリックするとステータス画面を表示します。

ファイル構成

ファイルとそれを入れるフォルダーを以下のような構成します。
※背景と人物の画像はBing Image Creatorで生成しています(AI生成)。

index.html - HTML

1. ステータスウィンドウ

<article>タグでステータスウィンドウを追加しています。

<article class="ステータスウィンドウ 消去 透明化">
  <!-- 内部に画像やテキストなどさまざまな要素が配置 -->
</article>

sample.css - CSS

1. キーフレームアニメーション

この CSS では、いくつかのアニメーションを定義しています。

2. ステータスウィンドウのスタイル

sampleData.js - サンプルデータクラス

1. クラス「サンプルデータ」の全体構成

このコードは export class サンプルデータ { … } の中に全てのデータを定義しています。

2. 画像データリスト

static 画像データリスト = [
    { id: '町', src: 'img/bg02.jpg' },
    { id: '宿屋', src: 'img/bg03.jpg' },
    { id: 'プレイヤー', src: 'img/chara01.png' },
    { id: '受付', src: 'img/chara03.png' }
];

解説:

3. 遷移データリスト

static 遷移データリスト = [
    ["スタート", , , [["町の広場へ行く",]],],
    ["町の広場へ行く", "<var>キャラクター情報管理.0.名前</var>は町の広場に来た<w>10</w>", , [["町の広場",]], [ /* 表示時の演出用の配列 */ ]],
    // …(さらに複雑なデータが続きます)
];

解説:

具体的な動き:

ポイント:

4. キャラクター情報データリスト

static キャラクター情報データリスト = [
    [0, "プレイヤー", "プレイヤー", 0, 0, 640, 640, 1, 0, 200, 100, 20, 20, 20, 20, 200, 0, 0, 0]
];

解説:

5. 武器情報、鎧情報、盾情報リスト

武器情報データリスト:

static 武器情報データリスト = [
    [0, "青銅の剣", "剣", 10, 100, "青銅で出来たまあまあな切れ味の剣"],
    [1, "鉄の剣", "剣", 20, 1000, "鉄製のよく斬れる剣"],
    [2, "鋼の剣", "剣", 50, 2000, "鋼鉄製のかなり斬れる剣"]
];

鎧情報データリスト:

static 鎧情報データリスト = [
    [0, "革の鎧", "鎧", 5, 20, "軽くてまあまあ丈夫な鎧"],
    [1, "青銅の鎧", "鎧", 10, 500, "青銅で出来た結構丈夫な鎧"],
    [2, "鉄の鎧", "鎧", 20, 1000, "鉄製の頑丈な鎧"],
    [3, "鋼の鎧", "鎧", 50, 3000, "鋼鉄製でかなり守備力が高い鎧"]
];

盾情報データリスト:

static 盾情報データリスト = [
    [0, "木の盾", "盾", 5, 20, "軽くてまあまあ丈夫な盾"],
    [1, "青銅の盾", "盾", 10, 300, "青銅で出来た結構丈夫な盾"],
    [2, "鉄の盾", "盾", 25, 1000, "鉄製の頑丈な盾"],
    [3, "鋼の盾", "盾", 50, 2000, "鋼鉄製でかなり守備力が高い盾"]
];

解説:

charaInfo.js - キャラクター情報クラス

export class キャラクター情報クラス {
    constructor(
        [
            id, 名前, 画像id, 画像x1, 画像y1, 画像x2, 画像y2,
            レベル, 経験値, 生命力, 魔力, 技力, ちから, 素早さ, 強靭さ, 所持金
        ],
        画像要素, 装備武器, 装備鎧, 装備盾
    ) {
        Object.assign(this, {
            id, 名前, 画像id, 画像x1, 画像y1, 画像x2, 画像y2,
            レベル, 経験値, 生命力, 魔力, 技力, ちから, 素早さ, 強靭さ, 所持金,
            最大生命力: 生命力, 最大魔力: 魔力,
            画像要素, 装備武器, 装備鎧, 装備盾
        });
    }

    get 攻撃力() {
        return this.ちから + (this.装備武器?.攻撃力 ?? 0);
    }

    get 守備力() {
        return this.強靭さ + (this.装備鎧?.守備力 ?? 0) + (this.装備盾?.守備力 ?? 0);
    }
}

1. クラスの基本構造とコンストラクタ

配列を使った引数の受け取り:

Object.assign() でプロパティを自動セット:

2. ゲッターで計算プロパティを作成

このクラスでは、計算によって値を求めるために getter を使用しています。

攻撃力のゲッター:

get 攻撃力() {
    return this.ちから + (this.装備武器?.攻撃力 ?? 0);
}

守備力のゲッター:

get 守備力() {
    return this.強靭さ + (this.装備鎧?.守備力 ?? 0) + (this.装備盾?.守備力 ?? 0);
}

itemInfo.js - アイテム関連のクラス

1. 基底クラス「アイテム情報クラス」

class アイテム情報クラス {
    constructor(p) {
        Object.assign(this, p);
    }
}

このクラスは、すべてのアイテム情報の基本となるクラスです。

2. 武器情報クラス「武器情報クラス」

export class 武器情報クラス extends アイテム情報クラス {
    constructor([id, 名前, 種別, 攻撃力, 価格, 説明]) {
        super({ id, 名前, 種別, 価格, 説明 });
        Object.assign(this, { 攻撃力 });
    }
}

このクラスは「武器」に特有の情報を管理するために作っています。

3. 防具情報クラス「防具情報クラス」

export class 防具情報クラス extends アイテム情報クラス {
    constructor([id, 名前, 種別, 守備力, 価格, 説明]) {
        super({ id, 名前, 種別, 価格, 説明 });
        Object.assign(this, { 守備力 });
    }
}

こちらは「防具」に固有の情報を管理するクラスです。

scene.js - 場面関連のクラス

1. 選択肢情報クラス

class 選択肢情報クラス {
    constructor([id, 名称]) {
        Object.assign(this, { id, 名称 });
    }
}

このクラスは、画面上に表示する選択肢(例:「次へ進む」「戻る」など)のデータを管理するためのものです。

2. 画像情報クラス

class 画像情報クラス {
    constructor([
        スプライトid, レイヤーid,
        画像id, x1, y1, x2, y2,
        表示メソッド, p1, p2, p3
    ]) {
        Object.assign(this, { スプライトid, レイヤーid, 画像id, x1, y1, x2, y2, 表示メソッド, p1, p2, p3 });
    }
}

このクラスは、画像を表示する際の情報である、どのスプライトを使うか、どのレイヤーに配置するか、画像のID、画像の表示位置(x1, y1, x2, y2)や表示のためのメソッド、さらに補助的なパラメータ(p1, p2, p3)をまとめて管理します。

3. 場面情報クラス

export class 場面情報クラス {
    constructor([id, メッセージ, 遷移メソッド, 選択肢情報リスト, 画像情報リスト]) {
        Object.assign(this, { id, メッセージ, 遷移メソッド });

        this.選択肢情報リスト = Array.isArray(選択肢情報リスト)
            ? 選択肢情報リスト.map(x => new 選択肢情報クラス(x))
            : [];

        this.画像情報リスト = Array.isArray(画像情報リスト)
            ? 画像情報リスト.map(x => new 画像情報クラス(x))
            : [];
    }
}

このクラスは、ひとつの「場面」に関する情報をまとめて管理するクラスです

sample.js - サンプルクラス

このコードは、さまざまなモジュール(画像管理、ループ処理、ウィンドウ、データ管理など)を連携させて、一連の処理を順番に初期化し、最終的にシーン(場面)の遷移を開始する仕組みになっています。
コード全体は、各パーツのインスタンス化と初期化を行い、プログラムをセットアップするための「ブートストラップ」的な役割を果たしています。

1. インポート部

import { サンプルデータ } from './sampleData.js'; // クラスをインポート
import { 画像管理クラス } from './imageManager.js'; // クラスをインポート
import { ループ処理クラス } from './loop.js'; // クラスをインポート
import { データ管理クラス } from './dataManager.js'; // クラスをインポート
import { メッセージウィンドウクラス } from './messageWindow.js'; // クラスをインポート
import { コマンドウィンドウクラス } from './commandWindow.js'; // クラスをインポート
import { ステータスウィンドウクラス } from './statusWindow.js'; // クラスをインポート
import { レイヤークラス } from './layer.js'; // クラスをインポート
import { 武器情報クラス, 防具情報クラス } from './itemInfo.js'; // クラスをインポート
import { キャラクター情報クラス } from './charaInfo.js'; // クラスをインポート
import { 画像表示クラス } from './imageDisplay.js'; // クラスをインポート
import { 場面遷移クラス } from './sceneTransition.js'; // クラスをインポート
import { Com } from './com.js'; // クラスをインポート

まず、必要な各モジュール(クラス)をインポートしています。
たとえば、画像管理、ループ処理、各ウィンドウ(メッセージ、コマンド、ステータス)、場面遷移、そしてキャラクターやアイテムのデータを扱うクラスなど、多岐にわたるコンポーネントがここに含まれています。

2. クラス「サンプル」の全体構成

export class サンプル {
    constructor() {
        this.main();
    }

3. main メソッド

    async main() {
        await this.使用する画像を読み込む();
        this.ループ処理を作成する();
        this.データ管理を作成する();
        this.メッセージウィンドウを作成する();
        this.コマンドウィンドウを作成する();
        this.ステータスウィンドウを作成する();
        this.レイヤーを作成する();
        this.武器情報を作成する();
        this.鎧情報を作成する();
        this.盾情報を作成する();
        this.キャラクター情報を作成する();
        this.画像表示を作成する();
        this.場面遷移を作成する();
        this.場面遷移.開始する();
    }

この main メソッドは非同期処理(async)になっていて、初期化の手順を順番に呼び出しています。
手順としては以下の通りです。

  1. 画像の読み込み: 使用する画像を読み込む() で、サンプルデータに定義した画像リストを読み込み、画像管理クラスのインスタンスにセットします。
  2. ループ処理の開始: ループ処理クラスのインスタンスを作成し、アニメーションや定期的な更新処理などを開始します。
  3. データ管理、ウィンドウ類の作成: データ管理、メッセージウィンドウ、コマンドウィンドウ、ステータスウィンドウの各インスタンスを作成して、画面に配置する準備をします。
    ウィンドウクラスには、どのDOM要素に表示するかや、処理を走らせるための情報(ループ処理や共通コンポーネント Com など)が渡されます。
  4. レイヤーの作成: レイヤークラスを使って、背景用(レイヤー1)と前景用(レイヤー2)のキャンバスを管理します。
    これにより、複数のグラフィック要素を適切な順序で描画することができます。
  5. アイテム情報の作成: 武器、鎧、防具の情報をそれぞれ管理するために、サンプルデータから配列を読み込み、各アイテム情報クラスのインスタンスに変換して、管理用オブジェクトに格納しています。
  6. キャラクター情報の作成: キャラクター情報も同様に、キャラクターごとのデータを読み込み、対応する画像(画像管理から取得)、装備情報(武器、鎧、防具)をセットして、キャラクター情報クラスのインスタンスとして作成します。
  7. 画像表示の作成: 画像表示クラスのインスタンスを作成し、画像管理とレイヤー管理の情報をもとに、実際の画面上に画像を描画する準備をします。
  8. 場面遷移の作成と開始: 最後に、場面遷移クラスのインスタンスを作り、場面データ(遷移データリスト)を渡して、場面の切り替え処理を開始しています。

4. 画像読み込み

    async 使用する画像を読み込む() {
        this.画像管理 = new 画像管理クラス();
        await this.画像管理.画像を読み込む(サンプルデータ.画像データリスト);
    }

5. ループ処理の作成

    ループ処理を作成する() {
        this.ループ処理 = new ループ処理クラス(Com);
        this.ループ処理.開始する();
    }

6. データ管理・ウィンドウの作成

    メッセージウィンドウを作成する() {
        this.メッセージウィンドウ = new メッセージウィンドウクラス(".スクリーン", ".メッセージウィンドウ", this.ループ処理, this.データ管理, Com, 0.5);
    }

コマンドウィンドウやステータスウィンドウも同様にして作成します。

7. レイヤーとアイテム、キャラクターの管理

8. 画像表示と場面遷移

9. エントリーポイントの設定

addEventListener('load', () => new サンプル());

sceneTransition.js - 場面遷移クラス

このクラスは、場面を切り替える処理を一手に担っています。

1. コンストラクタでの初期化

constructor(遷移データリスト, 開始id, メッセージウィンドウ, コマンドウィンドウ, データ管理, 画像表示) {
    this.場面情報管理 = {};
    遷移データリスト.forEach(x => this.場面情報管理[x[0]] = new 場面情報クラス(x));
    this.場面情報 = this.場面情報管理[開始id];
    this.メッセージウィンドウ = メッセージウィンドウ;
    this.コマンドウィンドウ = コマンドウィンドウ;
    this.データ管理 = データ管理;
    this.画像表示 = 画像表示;
}

コンストラクタでは、外部から渡される遷移データリストや各ウィンドウ、データ管理、画像表示などの必要なコンポーネントを受け取り、初期の状態を設定しています。

2. 開始する() メソッド

async 開始する() {
    if (!this.場面情報) throw new Error("場面情報が初期化されていません");

    while (1) {
        await this.画像表示.画像を表示する(this.場面情報);
        await this.メッセージを表示する();
        if (await this.遷移メソッドを実行する(this.場面情報)) continue;
        if (this.選択肢が不要な場合は1つ目の場面に遷移する()) continue;
        await this.コマンドウィンドウを表示する();
    }
}

このメソッドは、場面の遷移処理をループして実行します。

このループを通して、場面の画像表示、テキスト表示、遷移処理、そしてユーザーの選択が順次実行され、プログラムが進行していきます。

3. メッセージを表示する() メソッド

async メッセージを表示する() {
    if (!this.場面情報.メッセージ) return; // 表示するメッセージが無い場合は何もしない
    await this.メッセージウィンドウ.表示する();
    await this.メッセージウィンドウ.メッセージを表示する(this.場面情報.メッセージ);
}

4. 遷移メソッドを実行する() メソッド

async 遷移メソッドを実行する(場面情報) {
    if (!場面情報.遷移メソッド) return false;
    await this.メッセージウィンドウ.消去する();

    const メソッド情報 = {
        メソッドの構文: 場面情報.遷移メソッド,
        メソッド部: 場面情報.遷移メソッド,
        引数リスト: [],
        カレントオブジェクト: null,
        メソッド名: null
    };
    this.データ管理.メソッドの構文からメソッド部と引数リストを取得する(メソッド情報);
    this.データ管理.メソッド部からカレントオブジェクトとメソッド名を取得する(メソッド情報);

    const id = await メソッド情報.カレントオブジェクト[メソッド情報.メソッド名](...メソッド情報.引数リスト);
    const 選択肢情報 = 場面情報.選択肢情報リスト[id];
    if (!選択肢情報 || !this.場面情報管理[選択肢情報.id]) {
        throw new Error(`不正な遷移IDまたは選択肢情報が見つかりません。id: ${id}`);
    }
    this.場面情報 = this.場面情報管理[選択肢情報.id];
    return true;
}

このメソッドは、特殊なレイアウトの画面に遷移させたい場合に使います。

5. 選択肢が不要な場合は1つ目の場面に遷移する() メソッド

選択肢が不要な場合は1つ目の場面に遷移する() {
    const 選択肢情報 = this.場面情報.選択肢情報リスト[0];
    if (選択肢情報.名称) return false;

    if (!this.場面情報管理[選択肢情報.id]) {
        throw new Error(`遷移IDが不正です。id: ${選択肢情報.id}`);
    }
    this.場面情報 = this.場面情報管理[選択肢情報.id];
    return true;
}

6. コマンドウィンドウを表示する() メソッド

async コマンドウィンドウを表示する() {
    const id = await this.コマンドウィンドウ.表示する(this.場面情報.選択肢情報リスト);
    this.場面情報 = this.場面情報管理[id];
    if (!this.場面情報) {
        throw new Error(`場面情報が見つかりません。id: ${id}`);
    }
}

statusWindow.js - ステータスウィンドウクラス

1. クラスの概要

このクラスは、プログラム内でキャラクターのステータス(名前、レベル、各種能力値、装備、体力や魔力のバーなど)を表示するためのウィンドウを管理するために作りました。 ユーザーがステータスウィンドウを表示させ、表示中にウィンドウが徐々に出現し、画面をクリックするとフェードアウトして消えるという演出を実現しています。

2. コンストラクタ

constructor(スクリーンセレクタ, ステータスウィンドウセレクタ, Com, 現滅時間 = 0.5) {
    this.Com = Com;
    this.現滅時間 = 現滅時間;
    this.スクリーン要素 = Com.DOM要素を取得する(document, スクリーンセレクタ);
    this.ステータスウィンドウ要素 = Com.DOM要素を取得する(this.スクリーン要素, ステータスウィンドウセレクタ);
    this.キャラクター画像要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".キャラクター画像");
    this.名前要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".名前エリア .名前");
    this.レベル要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".レベルエリア .レベル");
    this.経験値要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".経験値エリア .経験値");
    this.技力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".基本パラメータ1 .技力");
    this.ちから要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".基本パラメータ1 .ちから");
    this.素早さ要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".基本パラメータ2 .素早さ");
    this.強靭さ要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".基本パラメータ2 .強靭さ");
    this.攻撃力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".攻撃力エリア .攻撃力");
    this.守備力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".守備力エリア .守備力");
    this.武器要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".装備エリア .武器");
    this.鎧要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".装備エリア .鎧");
    this.盾要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".装備エリア .盾");
    this.生命力バー要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".バーエリア .生命力バー");
    this.魔力バー要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".バーエリア .魔力バー");
    this.生命力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".バーエリア .生命力");
    this.最大生命力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".バーエリア .最大生命力");
    this.魔力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".バーエリア .魔力");
    this.最大魔力要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".バーエリア .最大魔力");
    this.所持金要素 = Com.DOM要素を取得する(this.ステータスウィンドウ要素, ".所持金エリア .所持金");
}

解説:

3. 表示する() メソッド

async 表示する(キャラクター情報) {
    if (typeof キャラクター情報 !== "object" || キャラクター情報 === null) {
        throw new Error(`キャラクター情報がありません。キャラクター情報: ${キャラクター情報}`);
    }
    this.ステータスを表示する(キャラクター情報);
    await this.ウィンドウを表示する();
    return this.画面をクリックしたら終了する();
}

解説:

4. ステータスを表示する() メソッド

ステータスを表示する(キャラクター情報) {
    this.キャラクター画像要素.src = キャラクター情報.画像要素.src;
    this.名前要素.textContent = キャラクター情報.名前;
    this.レベル要素.textContent = キャラクター情報.レベル;
    this.経験値要素.textContent = キャラクター情報.経験値;
    this.技力要素.textContent = キャラクター情報.技力;
    this.ちから要素.textContent = キャラクター情報.ちから;
    this.素早さ要素.textContent = キャラクター情報.素早さ;
    this.強靭さ要素.textContent = キャラクター情報.強靭さ;
    this.攻撃力要素.textContent = キャラクター情報.攻撃力;
    this.守備力要素.textContent = キャラクター情報.守備力;

    if (キャラクター情報.装備武器) this.武器要素.textContent = キャラクター情報.装備武器.名前;
    else this.武器要素.textContent = "なし";
    if (キャラクター情報.装備鎧) this.鎧要素.textContent = キャラクター情報.装備鎧.名前;
    else this.鎧要素.textContent = "なし";
    if (キャラクター情報.装備盾) this.盾要素.textContent = キャラクター情報.装備盾.名前;
    else this.盾要素.textContent = "なし";

    this.生命力要素.textContent = キャラクター情報.生命力;
    this.最大生命力要素.textContent = キャラクター情報.最大生命力;
    this.生命力バー要素.value = キャラクター情報.生命力 / キャラクター情報.最大生命力 * 100;
    this.魔力要素.textContent = キャラクター情報.魔力;
    this.最大魔力要素.textContent = キャラクター情報.最大魔力;
    this.魔力バー要素.value = キャラクター情報.魔力 / キャラクター情報.最大魔力 * 100;
    this.所持金要素.textContent = キャラクター情報.所持金;
}

解説:

5. ウィンドウを表示する() メソッド

async ウィンドウを表示する() {
    this.ステータスウィンドウ要素.classList.remove('消去');
    await this.Com.一時停止(this.現滅時間); // 少しずつ出現する時間
    this.ステータスウィンドウ要素.classList.remove('透明化');
}

解説:

6. 画面をクリックしたら終了する() メソッド

画面をクリックしたら終了する() {
    return new Promise(resolve => {
        this.以前のクリックハンドラ = this.スクリーン要素.onclick;
        this.スクリーン要素.onclick = async () => {
            this.スクリーン要素.onclick = this.以前のクリックハンドラ;
            this.ステータスウィンドウ要素.classList.add('透明化');
            await this.Com.一時停止(this.現滅時間); // 少しずつ消える時間
            this.ステータスウィンドウ要素.classList.add('消去');
            resolve(0);
        };
    });
}

解説: