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


<!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: linear-gradient(135deg, #1e3c72, #2a5298);
            color: white;
            font-family: 'MS Gothic', monospace;
            font-size: 32px; /* 文字サイズを倍に */
            line-height: 1.6;
            overflow: hidden;
            background-image: 
                radial-gradient(circle at 20% 20%, rgba(255,255,255,0.1) 1px, transparent 1px),
                radial-gradient(circle at 80% 80%, rgba(255,255,255,0.1) 1px, transparent 1px);
            background-size: 50px 50px;
        }

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

        .title-screen {
            text-align: center;
            opacity: 1;
            transition: opacity 0.8s ease-in-out;
        }

        .title-screen.fade-out {
            opacity: 0;
        }

        .title-text {
            font-size: 48px;
            font-weight: bold;
            margin-bottom: 50px;
            text-shadow: 3px 3px 6px rgba(0,0,0,0.7);
            background: linear-gradient(45deg, #ffd700, #ffed4e);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            background-clip: text;
        }

        .click-message {
            position: absolute;
            bottom: 20px;
            font-size: 18px;
            cursor: pointer;
            animation: pulse 2s infinite;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.7);
        }

        @keyframes pulse {
            0% { opacity: 0.7; }
            50% { opacity: 1; }
            100% { opacity: 0.7; }
        }

        .game-screen {
            width: 100%;
            height: 100%;
            display: none;
            flex-direction: column;
            opacity: 0;
            transition: opacity 0.8s ease-in-out;
        }

        .game-screen.fade-in {
            opacity: 1;
        }

        .image-area {
            width: calc(100% - 40px);
            height: 400px; /* 挿絵エリアを高くする */
            margin: 20px 20px 0 20px;
            background-color: #000;
            display: flex;
            justify-content: center;
            align-items: center;
            border: 8px solid #4a4a4a;
            border-radius: 12px;
            box-shadow: 
                inset -3px -3px 6px rgba(0,0,0,0.8),
                inset 3px 3px 6px rgba(255,255,255,0.1),
                0 8px 16px rgba(0,0,0,0.5);
            position: relative;
            overflow: hidden;
        }

        .image-area::before {
            content: '';
            position: absolute;
            top: 4px;
            left: 4px;
            right: 4px;
            bottom: 4px;
            border: 2px solid #666;
            border-radius: 8px;
            pointer-events: none;
        }

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

        .text-area {
            flex: 1;
            margin: 20px;
            padding: 20px;
            background: linear-gradient(145deg, #2a2a2a, #1a1a1a);
            border: 8px solid #4a4a4a;
            border-radius: 12px;
            box-shadow: 
                inset -3px -3px 6px rgba(0,0,0,0.8),
                inset 3px 3px 6px rgba(255,255,255,0.1),
                0 8px 16px rgba(0,0,0,0.5);
            cursor: pointer;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
            position: relative;
        }

        .text-area::before {
            content: '';
            position: absolute;
            top: 4px;
            left: 4px;
            right: 4px;
            bottom: 4px;
            border: 2px solid #666;
            border-radius: 8px;
            pointer-events: none;
        }

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

        .text-line {
            margin-bottom: 8px;
            min-height: 24px;
            color: #f0f0f0;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
        }

        .next-indicator {
            text-align: center;
            font-size: 24px;
            margin-top: 20px;
            opacity: 0;
            transition: opacity 0.3s ease;
            color: #ffd700;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
        }

        .next-indicator.visible {
            opacity: 1;
            animation: bounce 1.5s infinite;
        }

        @keyframes bounce {
            0%, 100% { transform: translateY(0); }
            50% { transform: translateY(-5px); }
        }

        .choices-area {
            margin: 0 20px 20px 20px;
            padding: 20px;
            background: linear-gradient(145deg, #2a2a2a, #1a1a1a);
            border: 8px solid #4a4a4a;
            border-radius: 12px;
            box-shadow: 
                inset -3px -3px 6px rgba(0,0,0,0.8),
                inset 3px 3px 6px rgba(255,255,255,0.1),
                0 8px 16px rgba(0,0,0,0.5);
            position: relative;
        }

        .choices-area::before {
            content: '';
            position: absolute;
            top: 4px;
            left: 4px;
            right: 4px;
            bottom: 4px;
            border: 2px solid #666;
            border-radius: 8px;
            pointer-events: none;
        }

        .choice-button {
            display: block;
            width: calc(100% - 20px);
            padding: 12px;
            margin: 8px 10px;
            background: linear-gradient(145deg, #3a3a3a, #2a2a2a);
            border: 4px solid #555;
            border-radius: 8px;
            cursor: pointer;
            font-family: inherit;
            font-size: 16px;
            color: #f0f0f0;
            text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
            transition: all 0.2s ease;
            box-shadow: 
                inset -2px -2px 4px rgba(0,0,0,0.6),
                inset 2px 2px 4px rgba(255,255,255,0.1);
        }

        .choice-button:hover {
            background: linear-gradient(145deg, #4a4a4a, #3a3a3a);
            border-color: #777;
            transform: translateY(-2px);
            box-shadow: 
                inset -2px -2px 4px rgba(0,0,0,0.6),
                inset 2px 2px 4px rgba(255,255,255,0.15),
                0 4px 8px rgba(0,0,0,0.3);
        }

        .choice-button:active {
            transform: translateY(0);
            box-shadow: 
                inset 2px 2px 4px rgba(0,0,0,0.8),
                inset -2px -2px 4px rgba(255,255,255,0.1);
        }

        .choice-button.link-choice {
            border-color: #4a90e2;
            background: linear-gradient(145deg, #2a4a6a, #1a3a5a);
        }

        .choice-button.link-choice:hover {
            background: linear-gradient(145deg, #3a5a7a, #2a4a6a);
            border-color: #5aa0f2;
        }

        .ending-screen {
            width: 100vw;
            height: 100vh;
            display: none;
            justify-content: center;
            align-items: center;
            background: linear-gradient(135deg, #1e3c72, #2a5298);
            color: #ffd700;
            font-size: 48px;
            font-weight: bold;
            text-shadow: 3px 3px 6px rgba(0,0,0,0.7);
            opacity: 0;
            transition: opacity 1.2s ease-in-out;
        }

        .ending-screen.fade-in {
            opacity: 1;
        }

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

        @keyframes blink {
            0%, 50% { opacity: 1; }
            51%, 100% { opacity: 0; }
        }

        .transition-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, rgba(0,0,0,1) 100%);
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.5s ease-in-out;
            z-index: 1000;
        }

        .transition-overlay.active {
            opacity: 1;
        }

        .loading-message {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: white;
            font-size: 24px;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
        }
    </style>
</head>
<body>
    <div class="transition-overlay" id="transition_overlay">
        <div class="loading-message" id="loading_message">読み込み中...</div>
    </div>

    <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 = 16; // 約60fps (1000ms / 60fps ≈ 16ms)
        const XML_URL = 'game_data.xml';  // XMLファイルのURL
        const CLICK_SOUND_URL = 'click.mp3';  // クリック音のURL
        const BACKGROUND_IMAGE_URL = '';  // 背景画像のURL(空の場合はグラデーション)
        
        // ゲーム状態
        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;
        let click_sound = null;

        // DOM要素の取得
        const transition_overlay = document.getElementById('transition_overlay');
        const loading_message = document.getElementById('loading_message');
        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データ(XMLファイルが読み込めない場合のフォールバック)
        const sample_xml = `<?xml version="1.0" encoding="UTF-8"?>
        <gamebook title="魔法の森の冒険">
            <start image="https://picsum.photos/400/200?random=1" sound="">
                あなたは深い森の入り口に立っています。
                古い言い伝えによると、この森の奥には伝説の宝物が眠っているとか。
                しかし、多くの冒険者がこの森で行方不明になっているのも事実です。
                どうしますか?
                <path name="森に入る">
                    勇気を振り絞って森に足を踏み入れました。
                    木々が鬱蒼と茂り、薄暗い森の中を進んでいきます。
                    しばらく歩くと、道が二手に分かれています。
                    <path name="深い道を進む" image="https://picsum.photos/400/200?random=2">
                        より深い森の奥へと向かいました。
                        突然、光る石を発見しました!
                        これが伝説の宝物かもしれません。
                        おめでとうございます!
                        あなたの冒険は成功に終わりました。
                    </path>
                    <path name="明るい道を進む" image="https://picsum.photos/400/200?random=3">
                        明るい道を選びました。
                        安全に森を抜けることができましたが、
                        宝物は見つかりませんでした。
                        また別の機会に挑戦しましょう。
                        今日の冒険はここまでです。
                    </path>
                    <path name="別の章へ進む" link="chapter2.xml">
                        突然、光の扉が現れました!
                        別の世界への入り口のようです。
                        扉をくぐりますか?
                    </path>
                </path>
                <path name="引き返す">
                    危険を感じて引き返すことにしました。
                    時には勇気よりも慎重さが大切です。
                    今度はもっと準備を整えてから挑戦しましょう。
                    賢明な判断でした。
                </path>
            </start>
        </gamebook>`;

        // 背景画像を設定
        function set_background_image() {
            if (BACKGROUND_IMAGE_URL) {
                document.body.style.backgroundImage = `url(${BACKGROUND_IMAGE_URL})`;
                document.body.style.backgroundSize = 'cover';
                document.body.style.backgroundPosition = 'center';
                document.body.style.backgroundAttachment = 'fixed';
            }
        }

        // クリック音を初期化
        function init_click_sound() {
            if (CLICK_SOUND_URL) {
                click_sound = new Audio(CLICK_SOUND_URL);
                click_sound.volume = 0.3;
                click_sound.preload = 'auto';
            }
        }

        // クリック音を再生
        function play_click_sound() {
            if (click_sound) {
                click_sound.currentTime = 0;
                click_sound.play().catch(e => {
                    console.log('クリック音の再生に失敗しました:', e);
                });
            }
        }

        // 画面遷移エフェクト
        function show_transition_effect(callback_, message_ = '読み込み中...') {
            loading_message.textContent = message_;
            transition_overlay.classList.add('active');
            
            setTimeout(() => {
                if (callback_) callback_();
                
                setTimeout(() => {
                    transition_overlay.classList.remove('active');
                }, 300);
            }, 500);
        }

        // XMLファイルを読み込み
        async function load_xml_file(url_) {
            try {
                const response = await fetch(url_);
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                const xml_text = await response.text();
                return parse_xml(xml_text);
            } catch (error) {
                console.warn(`XMLファイル ${url_} の読み込みに失敗しました:`, error);
                
                // メインのXMLファイルの場合のみサンプルデータを使用
                if (url_ === XML_URL) {
                    console.warn('サンプルデータを使用します');
                    return parse_xml(sample_xml);
                } else {
                    throw error;
                }
            }
        }

        // 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 = '';
            }
            
            // 一行ずつ順番に表示する再帰関数
            function display_next_line(line_index_) {
                if (line_index_ >= page_lines_.length) {
                    // 全ての行の表示が完了
                    is_typing = false;
                    next_indicator.classList.add('visible');
                    if (callback_) callback_();
                    return;
                }
                
                const line_element = document.getElementById(`line_${line_index_}`);
                const line_text = page_lines_[line_index_];
                
                // 現在の行を一文字ずつ表示し、完了したら次の行へ
                type_text(line_text, line_element, () => {
                    display_next_line(line_index_ + 1);
                });
            }
            
            // 最初の行から開始
            display_next_line(0);
        }

        // 選択肢を表示
        function display_choices(element_) {
            const path_children = Array.from(element_.children).filter(child => child.tagName === 'path');
            
            if (path_children.length === 0) {
                // 葉の要素の場合、選択肢は表示しない
                return;
            }
            
            choices_area.innerHTML = '';
            choices_area.style.display = 'block';
            
            path_children.forEach(path => {
                const choice_name = path.getAttribute('name');
                const link_url = path.getAttribute('link');
                
                if (choice_name) {
                    const button = document.createElement('button');
                    button.className = link_url ? 'choice-button link-choice' : 'choice-button';
                    button.textContent = choice_name;
                    
                    button.onclick = () => {
                        play_click_sound();
                        if (link_url) {
                            // 外部XMLファイルを読み込み
                            load_external_xml(link_url);
                        } else {
                            // 通常の選択肢処理
                            select_choice(path);
                        }
                    };
                    choices_area.appendChild(button);
                }
            });
        }

        // 外部XMLファイルを読み込んで新しいゲームを開始
        async function load_external_xml(xml_url_) {
            show_transition_effect(async () => {
                try {
                    const new_xml = await load_xml_file(xml_url_);
                    const gamebook_element = new_xml.getElementsByTagName('gamebook')[0];
                    
                    if (!gamebook_element) {
                        throw new Error('有効なgamebook要素が見つかりません');
                    }
                    
                    // 新しいXMLをセット
                    game_xml = new_xml;
                    
                    // ルート要素を取得
                    const root_element = gamebook_element.children[0];
                    if (!root_element) {
                        throw new Error('ルート要素が見つかりません');
                    }
                    
                    current_element = root_element;
                    choices_area.style.display = 'none';
                    
                    // 新しい章を開始
                    show_element_content(current_element);
                    
                } catch (error) {
                    console.error(`外部XMLファイル ${xml_url_} の読み込みに失敗しました:`, error);
                    alert(`ファイル ${xml_url_} の読み込みに失敗しました。\n${error.message}`);
                }
            }, `${xml_url_} を読み込み中...`);
        }

        // 選択肢を選択
        function select_choice(selected_element_) {
            show_transition_effect(() => {
                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.volume = 0.5;
                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';
                current_image.onerror = () => {
                    current_image.style.display = 'none';
                };
            } else {
                current_image.style.display = 'none';
            }
        }

        // 要素の内容を表示
        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) {
                        const path_children = Array.from(element_.children).filter(child => child.tagName === 'path');
                        if (path_children.length > 0) {
                            // 選択肢がある場合は選択肢を表示
                            display_choices(element_);
                        }
                        // 選択肢がない場合(葉ノード)は何もしない - ユーザーのクリック待ち
                    }
                });
            }
        }

        // テキストエリアのクリックハンドラ
        function handle_text_click() {
            if (is_typing) {
                return; // タイピング中はクリックを無視
            }
            
            play_click_sound();
            
            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) {
                        const path_children = Array.from(current_element.children).filter(child => child.tagName === 'path');
                        if (path_children.length === 0) {
                            // 葉の要素の場合、選択肢は表示しない(エンディング待機状態)
                        } else {
                            display_choices(current_element);
                        }
                    }
                });
            } else {
                const path_children = Array.from(current_element.children).filter(child => child.tagName === 'path');
                if (path_children.length === 0) {
                    // 葉の要素で最後のページの場合、ユーザーのクリックでエンディングへ
                    show_ending();
                }
            }
        }

        // エンディングを表示
        function show_ending() {
            show_transition_effect(() => {
                if (current_audio) {
                    current_audio.pause();
                    current_audio = null;
                }
                
                game_screen.style.display = 'none';
                ending_screen.style.display = 'flex';
                
                setTimeout(() => {
                    ending_screen.classList.add('fade-in');
                }, 100);
            }, '物語は終わりに近づいています...');
        }

        // ゲーム開始
        async function start_game() {
            play_click_sound();
            
            // タイトル画面をフェードアウト
            title_screen.classList.add('fade-out');
            
            show_transition_effect(async () => {
                try {
                    game_xml = await load_xml_file(XML_URL);
                    const gamebook_element = game_xml.getElementsByTagName('gamebook')[0];
                    const game_title = gamebook_element.getAttribute('title') || 'ゲームブック';
                    
                    const root_element = gamebook_element.children[0];
                    current_element = root_element;
                    
                    // タイトル画面を非表示にしてゲーム画面を表示
                    title_screen.style.display = 'none';
                    game_screen.style.display = 'flex';
                    
                    setTimeout(() => {
                        game_screen.classList.add('fade-in');
                        show_element_content(current_element);
                    }, 100);
                } catch (error) {
                    console.error('ゲームの初期化に失敗しました:', error);
                    alert('ゲームの読み込みに失敗しました');
                }
            });
        }

        // 初期化処理
        function init_game() {
            set_background_image();
            init_click_sound();
            
            // イベントリスナーの設定
            click_message.onclick = start_game;
            text_area.onclick = handle_text_click;
        }

        // ページ読み込み完了時に初期化
        window.addEventListener('DOMContentLoaded', init_game);
    </script>
</body>
</html>

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

コメント

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