簡単なゲームブック風ブラウザゲーム


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ゲームブック</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            background-color: white;
            color: black;
            font-family: 'MS Gothic', monospace;
            font-size: 16px;
            line-height: 1.6;
            overflow: hidden;
        }

        .game-container {
            width: 100vw;
            height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            position: relative;
        }

        .title-screen {
            text-align: center;
        }

        .title-text {
            font-size: 32px;
            font-weight: bold;
            margin-bottom: 50px;
        }

        .click-message {
            position: absolute;
            bottom: 20px;
            font-size: 18px;
            cursor: pointer;
        }

        .game-screen {
            width: 100%;
            height: 100%;
            display: none;
            flex-direction: column;
        }

        .image-area {
            width: 100%;
            height: 200px;
            background-color: black;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .image-area img {
            max-width: 100%;
            max-height: 100%;
            object-fit: contain;
        }

        .text-area {
            flex: 1;
            padding: 20px;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            cursor: pointer;
        }

        .text-content {
            min-height: 160px;
            position: relative;
        }

        .text-line {
            margin-bottom: 8px;
            min-height: 24px;
        }

        .next-indicator {
            text-align: center;
            font-size: 24px;
            margin-top: 20px;
            opacity: 0;
        }

        .next-indicator.visible {
            opacity: 1;
        }

        .choices-area {
            padding: 20px;
            background-color: #f0f0f0;
        }

        .choice-button {
            display: block;
            width: 100%;
            padding: 10px;
            margin: 5px 0;
            background-color: white;
            border: 2px solid black;
            cursor: pointer;
            font-family: inherit;
            font-size: 16px;
        }

        .choice-button:hover {
            background-color: #e0e0e0;
        }

        .ending-screen {
            width: 100vw;
            height: 100vh;
            display: none;
            justify-content: center;
            align-items: center;
            background-color: white;
            color: black;
            font-size: 32px;
            font-weight: bold;
        }

        .cursor-blink {
            animation: blink 1s infinite;
        }

        @keyframes blink {
            0%, 50% { opacity: 1; }
            51%, 100% { opacity: 0; }
        }
    </style>
</head>
<body>
    <div class="game-container">
        <!-- タイトル画面 -->
        <div class="title-screen" id="title_screen">
            <div class="title-text" id="title_text">ゲームブック</div>
            <div class="click-message" id="click_message">クリックしてください</div>
        </div>

        <!-- ゲーム画面 -->
        <div class="game-screen" id="game_screen">
            <div class="image-area" id="image_area">
                <img id="current_image" style="display: none;">
            </div>
            <div class="text-area" id="text_area">
                <div class="text-content" id="text_content">
                    <div class="text-line" id="line_0"></div>
                    <div class="text-line" id="line_1"></div>
                    <div class="text-line" id="line_2"></div>
                    <div class="text-line" id="line_3"></div>
                </div>
                <div class="next-indicator" id="next_indicator">▼</div>
            </div>
            <div class="choices-area" id="choices_area" style="display: none;"></div>
        </div>

        <!-- エンディング画面 -->
        <div class="ending-screen" id="ending_screen">
            おしまい
        </div>
    </div>

    <script>
        // 定数定義
        const MAX_LINES_PER_PAGE = 4;
        const TEXT_SPEED_MS = 50;
        
        // ゲーム状態
        let game_xml = null;
        let current_element = null;
        let current_text = '';
        let current_pages = [];
        let current_page_index = 0;
        let is_typing = false;
        let current_audio = null;

        // DOM要素の取得
        const title_screen = document.getElementById('title_screen');
        const game_screen = document.getElementById('game_screen');
        const ending_screen = document.getElementById('ending_screen');
        const title_text = document.getElementById('title_text');
        const click_message = document.getElementById('click_message');
        const image_area = document.getElementById('image_area');
        const current_image = document.getElementById('current_image');
        const text_area = document.getElementById('text_area');
        const text_content = document.getElementById('text_content');
        const next_indicator = document.getElementById('next_indicator');
        const choices_area = document.getElementById('choices_area');

        // サンプルXMLデータ(実際のファイルの代わり)
        const sample_xml = `<?xml version="1.0" encoding="UTF-8"?>
        <gamebook title="魔法の森の冒険">
            <start image="forest.jpg" sound="bgm1.mp3">
                あなたは深い森の入り口に立っています。
                古い言い伝えによると、この森の奥には伝説の宝物が眠っているとか。
                しかし、多くの冒険者がこの森で行方不明になっているのも事実です。
                どうしますか?
                <path1 name="森に入る">
                    勇気を振り絞って森に足を踏み入れました。
                    木々が鬱蒼と茂り、薄暗い森の中を進んでいきます。
                    しばらく歩くと、道が二手に分かれています。
                    <deep name="深い道を進む" image="deep_forest.jpg">
                        より深い森の奥へと向かいました。
                        突然、光る石を発見しました!
                        これが伝説の宝物かもしれません。
                        おめでとうございます!
                    </deep>
                    <safe name="明るい道を進む" image="bright_path.jpg">
                        明るい道を選びました。
                        安全に森を抜けることができましたが、
                        宝物は見つかりませんでした。
                        また別の機会に挑戦しましょう。
                    </safe>
                </path1>
                <path2 name="引き返す">
                    危険を感じて引き返すことにしました。
                    時には勇気よりも慎重さが大切です。
                    今度はもっと準備を整えてから挑戦しましょう。
                </path2>
            </start>
        </gamebook>`;

        // XMLの解析
        function parse_xml(xml_string_) {
            const parser = new DOMParser();
            const xml_doc = parser.parseFromString(xml_string_, 'text/xml');
            
            if (xml_doc.getElementsByTagName('parsererror').length > 0) {
                throw new Error('XMLの解析に失敗しました');
            }
            
            return xml_doc;
        }

        // テキストをページ単位に分割
        function split_text_into_pages(text_) {
            const lines = text_.split('\n').filter(line => line.trim() !== '');
            const pages = [];
            
            for (let i = 0; i < lines.length; i += MAX_LINES_PER_PAGE) {
                const page_lines = lines.slice(i, i + MAX_LINES_PER_PAGE);
                pages.push(page_lines);
            }
            
            return pages;
        }

        // 一文字ずつテキストを表示
        function type_text(text_, line_element_, callback_) {
            let char_index = 0;
            line_element_.textContent = '';
            
            function type_next_char() {
                if (char_index < text_.length) {
                    line_element_.textContent += text_[char_index];
                    char_index++;
                    setTimeout(type_next_char, TEXT_SPEED_MS);
                } else {
                    if (callback_) callback_();
                }
            }
            
            type_next_char();
        }

        // ページを表示
        function display_page(page_lines_, callback_) {
            is_typing = true;
            next_indicator.classList.remove('visible');
            
            // 全ての行をクリア
            for (let i = 0; i < MAX_LINES_PER_PAGE; i++) {
                const line_element = document.getElementById(`line_${i}`);
                line_element.textContent = '';
            }
            
            let completed_lines = 0;
            
            // 各行を順番に表示
            page_lines_.forEach((line, index) => {
                const line_element = document.getElementById(`line_${index}`);
                
                setTimeout(() => {
                    type_text(line, line_element, () => {
                        completed_lines++;
                        if (completed_lines === page_lines_.length) {
                            is_typing = false;
                            next_indicator.classList.add('visible');
                            if (callback_) callback_();
                        }
                    });
                }, index * 500);
            });
        }

        // 選択肢を表示
        function display_choices(element_) {
            const children = Array.from(element_.children);
            
            if (children.length === 0) {
                // 葉の要素の場合、選択肢は表示しない
                return;
            }
            
            choices_area.innerHTML = '';
            choices_area.style.display = 'block';
            
            children.forEach(child => {
                const choice_name = child.getAttribute('name');
                if (choice_name) {
                    const button = document.createElement('button');
                    button.className = 'choice-button';
                    button.textContent = choice_name;
                    button.onclick = () => select_choice(child);
                    choices_area.appendChild(button);
                }
            });
        }

        // 選択肢を選択
        function select_choice(selected_element_) {
            choices_area.style.display = 'none';
            current_element = selected_element_;
            show_element_content(current_element);
        }

        // BGMを再生
        function play_bgm(sound_url_) {
            if (current_audio) {
                current_audio.pause();
                current_audio = null;
            }
            
            if (sound_url_) {
                current_audio = new Audio(sound_url_);
                current_audio.loop = true;
                current_audio.play().catch(e => {
                    console.log('音声の再生に失敗しました:', e);
                });
            }
        }

        // 画像を表示
        function display_image(image_url_) {
            if (image_url_) {
                current_image.src = image_url_;
                current_image.style.display = 'block';
                image_area.style.backgroundColor = 'transparent';
                current_image.onerror = () => {
                    current_image.style.display = 'none';
                    image_area.style.backgroundColor = 'black';
                };
            } else {
                current_image.style.display = 'none';
                image_area.style.backgroundColor = 'black';
            }
        }

        // 要素の内容を表示
        function show_element_content(element_) {
            // 画像とBGMを設定
            const image_url = element_.getAttribute('image');
            const sound_url = element_.getAttribute('sound');
            
            display_image(image_url);
            play_bgm(sound_url);
            
            // テキスト内容を取得
            let text_content_str = '';
            for (let node of element_.childNodes) {
                if (node.nodeType === Node.TEXT_NODE) {
                    text_content_str += node.textContent;
                }
            }
            
            current_text = text_content_str.trim();
            current_pages = split_text_into_pages(current_text);
            current_page_index = 0;
            
            if (current_pages.length > 0) {
                display_page(current_pages[current_page_index], () => {
                    // 最後のページで子要素がない場合は選択肢を表示
                    if (current_page_index === current_pages.length - 1) {
                        if (element_.children.length === 0) {
                            // 葉の要素の場合、次のクリックでエンディング
                        } else {
                            display_choices(element_);
                        }
                    }
                });
            }
        }

        // テキストエリアのクリックハンドラ
        function handle_text_click() {
            if (is_typing) {
                return; // タイピング中はクリックを無視
            }
            
            if (current_page_index < current_pages.length - 1) {
                // 次のページを表示
                current_page_index++;
                display_page(current_pages[current_page_index], () => {
                    if (current_page_index === current_pages.length - 1) {
                        if (current_element.children.length === 0) {
                            // 葉の要素の場合、選択肢は表示しない
                        } else {
                            display_choices(current_element);
                        }
                    }
                });
            } else if (current_element && current_element.children.length === 0) {
                // 葉の要素で最後のページの場合、エンディングへ
                show_ending();
            }
        }

        // エンディングを表示
        function show_ending() {
            if (current_audio) {
                current_audio.pause();
                current_audio = null;
            }
            
            game_screen.style.display = 'none';
            ending_screen.style.display = 'flex';
        }

        // ゲーム開始
        function start_game() {
            try {
                game_xml = parse_xml(sample_xml);
                const gamebook_element = game_xml.getElementsByTagName('gamebook')[0];
                const game_title = gamebook_element.getAttribute('title') || 'ゲームブック';
                
                title_text.textContent = game_title;
                
                const root_element = gamebook_element.children[0];
                current_element = root_element;
                
                // タイトル画面を非表示にしてゲーム画面を表示
                title_screen.style.display = 'none';
                game_screen.style.display = 'flex';
                
                show_element_content(current_element);
            } catch (error) {
                console.error('ゲームの初期化に失敗しました:', error);
                alert('ゲームの読み込みに失敗しました');
            }
        }

        // イベントリスナーの設定
        click_message.onclick = start_game;
        text_area.onclick = handle_text_click;
    </script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

ログイン または 会員登録 するとコメントできます。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとかいった要望があれば コメント欄に書いてみて下さい。 ひまをみて対応します。
簡単なゲームブック風ブラウザゲーム|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1