<!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: 16 px;
line-height: 1.6 ;
overflow: hidden;
}
.game-container {
width: 100 vw;
height: 100 vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.title-screen {
text-align: center;
}
.title-text {
font-size: 32 px;
font-weight: bold;
margin-bottom: 50 px;
}
.click-message {
position: absolute;
bottom: 20 px;
font-size: 18 px;
cursor: pointer;
}
.game-screen {
width: 100 %;
height: 100 %;
display: none;
flex-direction: column;
}
.image-area {
width: 100 %;
height: 200 px;
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: 20 px;
display: flex;
flex-direction: column;
justify-content: space-between;
cursor: pointer;
}
.text-content {
min-height: 160 px;
position: relative;
}
.text-line {
margin-bottom: 8 px;
min-height: 24 px;
}
.next-indicator {
text-align: center;
font-size: 24 px;
margin-top: 20 px;
opacity: 0 ;
}
.next-indicator.visible {
opacity: 1 ;
}
.choices-area {
padding: 20 px;
background-color: #f0f0f0;
}
.choice-button {
display: block;
width: 100 %;
padding: 10 px;
margin: 5 px 0 ;
background-color: white;
border: 2 px solid black;
cursor: pointer;
font-family: inherit;
font-size: 16 px;
}
.choice-button:hover {
background-color: #e0e0e0;
}
.ending-screen {
width: 100 vw;
height: 100 vh;
display: none;
justify-content: center;
align-items: center;
background-color: white;
color: black;
font-size: 32 px;
font-weight: bold;
}
.cursor-blink {
animation: blink 1 s 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 ;
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' );
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>`;
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);
}
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_ ) {
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>
copy
コメント