ChatGPT4に作ってもらったRPGゲームの素その④。~バトル~


大きな特徴。

キャラクターの設定
および各種戦闘コマンドにおけるダメージ計算を全てJSONで定義する。

ここでの要点はJSON読み込んでのGUI表示と
JSON読み込んでの抽象構文木(AST木)によるダメージ計算である。

AST木ダメージ計算

nodeがAST木のノードである。
ここでのnode種は定数と変数と二項演算であって、
二項演算の場合は木を左右に降りていく。
定数の場合は即座に値を返すから木の末端となる。
変数は外部から入力されたcontext変数からプロパティを読み取って返す。これも木の末端である。

function evaluateAst(node, context) {
    switch (node.type) {
        case 'literal':
            return node.value;
        case 'variable':
            const [objName, propName] = node.name.split('.');
            return context[objName][propName];
        case 'add':
            return operations.add(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'subtract':
            return operations.subtract(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'multiply':
            return operations.multiply(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'divide':
            return operations.divide(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'mod':
            return operations.mod(evaluateAst(node.left, context), evaluateAst(node.right, context));
        // 他の演算も同様に実装
        default:
            throw new Error(`Unknown node type: ${node.type}`);
    }
}

各種二項演算

const operations = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
    mod: (a, b) => a % b,
    // 必要に応じて他の関数も追加できます
};

コマンドに対応したダメージ計算JSON

例えば以下はdamage=player.attack + 10

    "たたかう": {
        "type": "add",
        "left": { "type": "variable", "name": "player.attack" },
        "right": { "type": "literal", "value": 10 }
    },

このJSONはfetchとjson()関数によってロードされて文字列からJavaScriptオブジェクトになる。その後AST木に解釈される。
上記の場合、leftはkeyで { "type": "variable", "name": "player.attack" }はオブジェクト。typeがkeyで"variable"は文字列。nameがkeyで"player.attack"は文字列である。

function evaluateAst(node, context) {
    switch (node.type) {
        case 'literal':
            return node.value;
        case 'variable':
            const [objName, propName] = node.name.split('.');
            return context[objName][propName];

たたかうコマンドノードのleftのtypeプロパティは"variable"文字列であって、これは変数ノードである。JSON定義はその変数ノードのプロパティattackの使用を求めている。splitにより文字列が切り出され
const [objName, propName] = node.name.split('.');
次にobjNameがキーとして使われる。
context[objName][propName];

これはつまりcontextオブジェクトのキーとしてobjNameは使用可能なものでなければならない。

なのでevaluateAstの引数は

const damage = evaluateAst(commandAst, { player, enemy });
const damage = evaluateAst(commandAst, { player: activePlayer, enemy });

のようになる。
これはつまりJSONで定義したダメージ計算で使用する変数は、
evaluateAst時にcontextオブジェクトのキーとしてアクセス可能でなければならないということである。
以下のようなキーはAST木の途中でJSONで定義されたキーにアクセスできずに失敗する。

const damage = evaluateAst(commandAst, { player2: activePlayer, enemy });

ドラクエ式の場合

ダメージ = (自分の攻撃力 ÷ 2) - (敵の防御力 ÷ 4)

{
    "type": "subtract",
    "left": {
        "type": "divide",
        "left": {
            "type": "variable",
            "name": "self.attack"
        },
        "right": {
            "type": "literal",
            "value": 2
        }
    },
    "right": {
        "type": "divide",
        "left": {
            "type": "variable",
            "name": "enemy.defense"
        },
        "right": {
            "type": "literal",
            "value": 4
        }
    }
}



fetch

//単にJSONのロード
async function loadJSON(url) {
    return await fetch(url).then(res => res.json());
}

fetch API

基本的な使用方法:

fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('Error:', error));
  1. 定義: `fetch` APIは、ウェブブラウザやJavaScriptの実行環境で使用される、リソース(通常はネットワークリソース)の取得やネットワーク通信のためのモダンなAPIです。

  2. 特性:

    • Promiseベース: `fetch`は非同期操作を扱いやすくするPromiseを基盤としています。

    • 柔軟性: さまざまなリクエストタイプ、ヘッダー、ボディコンテンツをサポートしています。

  3. Response オブジェクト:

    • リソースの取得が成功すると、`Response` オブジェクトが返されます。

    • このオブジェクトには、ヘッダー、ステータスコード、メソッド(json(), text(), blob()など)が含まれています。

  4. エラーハンドリング:

    • `fetch`はネットワークエラーの際には失敗しますが、HTTP 404 や 500 といったエラーステータスの際には失敗しません。

    • エラーハンドリングを適切に行うには、Responseオブジェクトの`ok`プロパティを確認するなどの追加のステップが必要です。

  5. 規格:

    • `fetch` APIはWHATWG (Web Hypertext Application Technology Working Group) によって定められた「Fetch Standard」に基づいています。

  6. JavaScript特有:

    • `fetch` API自体はJavaScriptのためのものですが、その背後の概念や「Fetch Standard」は、JavaScript以外の言語や技術にも影響を与えています。

`fetch` APIは、前の技術であるXMLHttpRequestよりも柔軟で直感的な方法でリソースの取得やネットワーク通信を行うことができるため、現代のフロントエンド開発やJavaScriptアプリケーション開発で広く使われています。


オブジェクトリテラル

`{ player: activePlayer, enemy }` のような記述は、ES6(ECMAScript 2015)から導入されたオブジェクトリテラルの新しい構文を使用しています。この構文を使用すると、変数から直接オブジェクトを作成することが簡単になります。

プロパティ名のショートハンド(短縮形): 以下のように変数名とプロパティ名が同じ場合、1つの名前だけを使用してオブジェクトのプロパティを作成できます。

let enemy = "Dragon";
let obj = { enemy };
console.log(obj); // { enemy: "Dragon" }

異なるプロパティ名を持つ変数からのオブジェクトの作成: プロパティ名と変数名が異なる場合、以下のように通常の方法でオブジェクトのプロパティを作成できます。

let activePlayer = "Knight";
let obj = { player: activePlayer };
console.log(obj); // { player: "Knight" }

上記の2つの機能を組み合わせると、以下のようになります:

let activePlayer = "Knight";
let enemy = "Dragon";
let obj = { player: activePlayer, enemy };
console.log(obj); // { player: "Knight", enemy: "Dragon" }

この機能はコードの冗長性を減少させるため、特に変数からオブジェクトを頻繁に生成する際に非常に便利です。


ソース

characters.json

{
    "勇者": {
        "name": "勇者",
        "hp": 100,
        "attack": 20,
        "level": 5,
        "commands" : ["たたかう", "まほう", "にげる"]
    },
    "戦士": {
        "name": "戦士",
        "hp": 150,
        "attack": 30,
        "level": 5,
        "commands" : ["たたかう", "にげる"]
    },
    "スライム": {
        "name": "スライム",
        "hp": 250,
        "attack": 10,
        "commands" : ["たたかう"]        
    },
    "ゴブリン": {
        "name": "ゴブリン",
        "hp": 60,
        "attack": 20,
        "commands" : ["たたかう"]        
    }
}

commands.json



{
    "たたかう": {
        "type": "add",
        "left": { "type": "variable", "name": "player.attack" },
        "right": { "type": "literal", "value": 10 }
    },
    "まほう": {

    },    
    "にげる" : {

    }
}

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>バトル</title>
    <script src="game.js" defer></script>
</head>
<body></body>
</html>

game.js

const operations = {
    add: (a, b) => a + b,
    subtract: (a, b) => a - b,
    multiply: (a, b) => a * b,
    divide: (a, b) => a / b,
    mod: (a, b) => a % b,
    // 必要に応じて他の関数も追加できます
};

//単にJSONのロード
async function loadJSON(url) {
    return await fetch(url).then(res => res.json());
}

function evaluateAst(node, context) {
    switch (node.type) {
        case 'literal':
            return node.value;
        case 'variable':
            const [objName, propName] = node.name.split('.');
            return context[objName][propName];
        case 'add':
            return operations.add(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'subtract':
            return operations.subtract(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'multiply':
            return operations.multiply(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'divide':
            return operations.divide(evaluateAst(node.left, context), evaluateAst(node.right, context));
        case 'mod':
            return operations.mod(evaluateAst(node.left, context), evaluateAst(node.right, context));
        // 他の演算も同様に実装
        default:
            throw new Error(`Unknown node type: ${node.type}`);
    }
}

document.addEventListener('DOMContentLoaded', async () => {

    const charactersData = await loadJSON('characters.json');
    const players = [
        charactersData["勇者"],
        charactersData["戦士"]
    ];

    const enemy = charactersData["スライム"];
    const enemies = [
        charactersData["スライム"],
        charactersData["ゴブリン"]
    ];

    const commands = await loadJSON('commands.json');

    //プレイヤーに接続されたコマンド文字列をコマンドオブジェクトに変換する
    function setupCommands(characters) {
        for (const character of characters) {
            character.commands = character.commands.map(command => commands[command]);
        }
    }
    setupCommands(players);
    setupCommands(enemies);




    const gameContainer = document.createElement('div');

    const upperGUI = document.createElement('div');
    upperGUI.style.border = "1px solid black";
    upperGUI.style.display = 'flex';
    upperGUI.style.flexDirection = 'row';

    const playersStatus = document.createElement('div');
    playersStatus.style.border = "1px solid black";
    playersStatus.style.display = 'flex';
    playersStatus.style.flexDirection = 'row';
    upperGUI.appendChild(playersStatus);

    const enemiesStatus = document.createElement('div');
    enemiesStatus.style.display = 'flex';
    enemiesStatus.style.flexDirection = 'row';

    const bottomGUI = document.createElement('div');
    bottomGUI.style.border = "1px solid black";
    bottomGUI.style.display = 'flex';
    bottomGUI.style.flexDirection = 'row';

    const actionMenu = document.createElement('div');
    actionMenu.style.border = "1px solid black";
    const messageBox = document.createElement('div');
    messageBox.style.border = "1px solid black";
    bottomGUI.appendChild(actionMenu);
    bottomGUI.appendChild(messageBox);

    gameContainer.appendChild(upperGUI);
    gameContainer.appendChild(enemiesStatus);
    gameContainer.appendChild(bottomGUI);
    document.body.appendChild(gameContainer);


    //GUIを更新
    function updateStatus() {

        // 既存の内容をクリア
        while (playersStatus.firstChild) {
            playersStatus.removeChild(playersStatus.firstChild);
        }
        while (enemiesStatus.firstChild) {
            enemiesStatus.removeChild(enemiesStatus.firstChild);
        }

        // プレイヤーステータスの更新
        for (const player of players) {
            const playerDiv = document.createElement('div');
            playerDiv.style.border = "1px solid black";
            playerDiv.innerHTML = `${player.name}:<br> HP ${player.hp}`;
            playersStatus.appendChild(playerDiv);
        }

        // エネミーステータスの更新
        for (const enemy of enemies) {
            const enemyDiv = document.createElement('div');
            enemyDiv.style.border = "1px solid black";
            enemyDiv.innerHTML = `${enemy.name}:<br> HP ${enemy.hp}`;
            enemiesStatus.appendChild(enemyDiv);
        }
    }

    function showMessage(message) {
        messageBox.textContent = message;
        // setTimeout(() => {
        //     messageBox.textContent = '';
        // }, 1500);
    }

    const commandList = Object.keys(commands);
    commandList.forEach(commandName => {
        const btn = document.createElement('div');
        btn.textContent = commandName;
        actionMenu.appendChild(btn);
    });

    let selectedCommandIndex = 0;

    //focusの管理
    let focusOnMessage = false;

    function updateCommandSelection() {
        Array.from(actionMenu.children).forEach((btn, index) => {
            if (index === selectedCommandIndex) {
                btn.style.border = "2px solid black";
            } else {
                btn.style.border = "";
            }
        });
    }


    const FocusState = {
        COMMANDS: "COMMANDS",
        MESSAGE: "MESSAGE"
    };
    let focusState = FocusState.COMMANDS;

    document.addEventListener('keydown', (e) => {

        //focusがメッセージウィンドウにある場合
        if (focusState === FocusState.MESSAGE) {
            switch (e.key) {
                case 'a':
                case 'Enter':                    
                    focusState = FocusState.COMMANDS;
                    showMessage(`コマンド?`);                    
                    break;
            }
            return;
        }

        if (focusState === FocusState.COMMANDS) {
            switch (e.key) {
                case 'ArrowUp':
                    selectedCommandIndex = (selectedCommandIndex - 1 + commandList.length) % commandList.length;
                    updateCommandSelection();
                    break;
                case 'ArrowDown':
                    selectedCommandIndex = (selectedCommandIndex + 1) % commandList.length;
                    updateCommandSelection();
                    break;
                case 'a':
                case 'Enter':
                    const commandName = commandList[selectedCommandIndex];
                    const commandAst = commands[commandName];
                    const damage = evaluateAst(commandAst, { player, enemy });
                    enemy.hp -= damage;
                    showMessage(`${enemy.name}${damage}のダメージを与えた!`);
                    if (enemy.hp <= 0) {
                        showMessage(`${enemy.name}をたおした!`);
                        actionMenu.innerHTML = '';
                    }
                    updateStatus();
                    //updateCommandSelection();                    

                    //focusOnMessage = true;
                    focusState = FocusState.MESSAGE;
                    break;
            }
        }
    });

    updateStatus();
    updateCommandSelection();
});

この記事が気に入ったらサポートをしてみませんか?