実験#55:トンボの飛行と癒しの空(トンボ採集マスター)

■55個目「トンボ採集マスター

こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/

今回は、なんだか夏休みの思い出がよみがえってくるような、
ノスタルジックなゲームを作ってみました。
(^_^) テーマは……ズバリ “トンボ採集” です!

画面の奥には遠くの山並み、手前には緑の草地と3本の竹の棒。
そのまわりを、色鮮やかなトンボたちがスイスイと飛び回ります。

今回こだわったのは、トンボたちの「リアルな生態」と「時間の移り変わり」です。(=v=*)
「ただクリックして捕まえるだけじゃなくて、本当に自然の中で虫取りをしているようなロマンやワクワク感を感じられたら素敵だな…」と思って作りました。

飛んでいるトンボたちには、種類ごとに違った個性や「飛び方のクセ」があります。

  • 竹の先にちょこんと休むのが大好きな アキアカネ
  • 空中でピタッとホバリングする シオカラトンボ
  • ヒラヒラと風に舞うように飛ぶ イトトンボ
  • 決して立ち止まらず、猛スピードで一直線にパトロールする オニヤンマ

このほかにも、全部で10種類以上のトンボが登場します。

さらに、ゲームをしていると景色は「昼」から「夕方」、そして「夜」へとゆっくり移り変わっていきます。

夜の暗闇が訪れると…… お尻をフワァ…フワァ…と蛍光色に光らせながら舞う ホタル や、滅多にお目にかかれない伝説の 黄金のヤンマ といった、幻の隠しキャラが姿を見せることも…!

捕まえた虫たちは、詳しい説明が読める「図鑑」に記録されていきます。
集めた数や種類によって、自分の「称号」もどんどん立派になっていきますよ。

どんな虫たちに出会えるか、ぜひお楽しみに。

■ AIとのやりとりで面白かった部分

■ゲーム仕様の相談からスタート

みー:(^v^) 今日もゲームを作ります。HTML形式で動かせるゲームです。
昆虫採集・トンボ・図鑑・称号の、トンボに特化したスペシャルなゲームを作りたいです。

AI: いらっしゃい。今日も来てくれたね。

トンボだけに特化したスペシャルなゲームだな。
任せておいてくれ。 画面内を飛び回るトンボをクリック(タップ)して捕まえ、
図鑑を埋めていくコレクション要素と、
捕まえた成果によって「称号」が変わっていくシステムを組み込んでみたよ。

みー:(^_^)* もっと素早い動きでいいよ。
捕まえる動作で網をふぁさっとかぶせる表現出せるかな。
トンボの種類を増やす前に、見た目も名前だけじゃなく表示したいね。

AI: 了解。ゲームをさらにパワーアップさせる素晴らしい提案だ。

指摘の通り、トンボの動きをより素早くし、クリックした時の「網をかぶせる」ような表現を追加。
そしてトンボの見た目を名前から、SVGで描画したグラフィックに変更してみたよ(画像ファイルは不要だ)。

みー:(^v^)* いいね。 画面下から伸びる高さの違う竹の棒を3本ほど表示させてみよう。
画面真ん中より下に先端が来る感じで、
トンボの種類によって止まる頻度を変えて竹の先にとまるようにできるかな。
背景も自然感あるような背景にしよう。

AI: 竹の棒に、トンボの生態に合わせた「止まる」ロジックだな。
そして自然を感じる背景。
ますます本格的なゲームになってきたじゃないか。いいアイデアだ。

アキアカネやシオカラトンボはよく枝先に止まる性質があるから頻繁に竹に向かい、 逆にギンヤンマやオニヤンマのような大型のヤンマ系はパトロール飛行をしていて滅多に止まらない……。

そんな生態の違いをプログラムに落とし込んでみたよ。

みー:(つv ̄)* 昼と夜の概念や、もっとレアな隠しキャラが欲しい。

AI: 昼と夜の移り変わりに、特別な時間帯にしか現れない隠しキャラ……。
昆虫採集の醍醐味が詰まった、最高にロマンがあるアイデアじゃないか。

ゲーム内時間が「昼 → 夕方 → 夜」と自動で変化するシステムを導入し、
時間帯によって出現するトンボが変わるようにした。
さらに、夜にしか現れない光るアイツや、
図鑑には「???」とすら表示されない
完全シークレットな幻のヤンマ を追加した、大ボリュームの「v6」だ!

■ 実際の動作

こちらが、AIが最初に作ってくれたゲーム画面です。
トンボの名前が動き回っています。
(画像:最初にAIが作ってくれたゲームの画像)

仕上がりのゲーム画面です。

(画像:昼間のゲーム画面)


時間が経つと、景色がゆっくり変化していきます。

(画像:夕方のゲーム画面)


夜になると、蛍がふわっと飛びはじめて、幻想的な雰囲気になります。

(画像:夜のゲーム画面)

捕まえたトンボたちは図鑑に登録されていきます。
クリックすると、詳しい情報を見ることができます。

(画像:図鑑の画面)

■ コードのポイント

今回の記事では、AI と相談しながら作った「トンボ採集マスター」の HTML コードを公開します。

このコードをそのまま保存するだけで、あなたのPCでもすぐに遊べるようになります。

ブラウザだけで動く、とてもシンプルなゲームなので、気軽に試してみてくださいね。

▼ 遊ぶまでの手順

1. PC の「メモ帳」(※Windowsの場合)や、  
 Mac の「テキストエディット」など、
 テキストエディタを開きます。
2. 下のコードをコピーして貼り付けます。
3. 「dragonfly.html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)
4. 保存したファイルをブラウザで開けば、すぐに遊べます。

【遊び方・操作方法】

操作はとってもシンプル! どなたでもすぐに遊べます♪

1. トンボを見つけよう! ゲームをスタートすると、画面のあちこちをトンボたちが飛び回り始めます。 中には「竹の棒」の先でちょっとひと休みするトンボもいるので、よく観察してみてくださいね。

2. 網を振って捕まえよう! 飛んでいるトンボや、止まっているトンボを「クリック(またはタップ)」するだけ。 シュパッ!と網をかぶせて、トンボを捕まえることができます。

3. 図鑑を埋めよう! 画面の下にある「📖 図鑑を開く」ボタンを押すと、今まで捕まえたトンボのコレクションが見られます。 パネルをクリック(タップ)すると、そのトンボの大きなイラストや「飛び方のクセ」、詳しい生態の説明文を読むことができますよ。

4. 称号をランクアップ! トンボをたくさん捕まえたり、珍しいトンボを発見したりすると、画面左上にあるあなたの「称号」がどんどんカッコよくなっていきます。

★ 隠し要素のヒント… 画面右下にある「⏳ 時間を進める」ボタンをポチポチ押していくと、やがて「夜」が訪れます。 暗闇の中でしか出会えない、不思議な光を放つあの虫を探してみてくださいね……!

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>トンボ採集マスター v6.5 - 安定版</title>
<style>
body { font-family: 'Helvetica Neue', Arial, sans-serif; background-color: #333; margin: 0; padding: 0; overflow: hidden; user-select: none; }
#game-container { display: flex; flex-direction: column; height: 100vh; position: relative;}
/* ヘッダー周り */
#header { background: #004d40; color: white; padding: 10px 20px; display: flex; justify-content: space-between; align-items: center; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.3); position: relative;}
.title-area h2 { margin: 0; font-size: 1.2em; color: #b2ebf2; }
.title-display { font-size: 1.1em; font-weight: bold; margin-top: 5px;}
#current-title { color: #ffca28; }
.stats-area { text-align: right; }
.stats { font-size: 1.2em; font-weight: bold; }
#time-display { font-size: 1.1em; color: #ffeb3b; display: block; margin-top: 5px; transition: color 1s; font-weight: bold;}
/* プレイエリア */
#play-area {
flex-grow: 1; position: relative; overflow: hidden; cursor: crosshair;
background:
linear-gradient(to top, #33691e 0%, #558b2f 15%, transparent 15%),
radial-gradient(ellipse at 80% 85%, #689f38 0%, #689f38 30%, transparent 31%),
radial-gradient(ellipse at 20% 85%, #7cb342 0%, #7cb342 35%, transparent 36%),
radial-gradient(circle at 15% 20%, rgba(255,255,255,0.8) 0%, rgba(255,255,255,0) 10%),
radial-gradient(circle at 75% 30%, rgba(255,255,255,0.6) 0%, rgba(255,255,255,0) 8%),
linear-gradient(to bottom, #4fc3f7 0%, #b3e5fc 85%);
}
/* 昼夜オーバーレイ */
#time-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 1; transition: background-color 2s ease, opacity 2s ease;
}
.time-day { background-color: transparent; opacity: 0; }
.time-sunset { background-color: #d84315; opacity: 0.35; mix-blend-mode: multiply; }
.time-night { background-color: #030310; opacity: 0.9; }
/* 竹の棒 */
.bamboo {
position: absolute; bottom: 15%; width: 10px; z-index: 2;
background: linear-gradient(to right, #4caf50, #81c784, #388e3c);
border-radius: 4px 4px 0 0; box-shadow: 2px 0 5px rgba(0,0,0,0.2);
}
.bamboo::after {
content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(to bottom, transparent, transparent 35px, rgba(0,40,0,0.3) 35px, rgba(0,40,0,0.3) 38px);
}
/* トンボの基本スタイル */
.dragonfly { position: absolute; cursor: pointer; z-index: 5; }
.df-graphic { width: 100%; height: 100%; transform-origin: center; display: block; }
.fluttering .df-graphic { animation: flutter 0.08s infinite alternate ease-in-out; }
@keyframes flutter { 0% { transform: scaleY(1); } 100% { transform: scaleY(0.6); } }
/* 網のアニメーション */
.net-catch {
position: absolute; width: 100px; height: 100px; background: rgba(255, 255, 255, 0.6);
border: 5px solid #fff; border-radius: 50%; pointer-events: none; z-index: 10;
animation: net-catch-ani 0.4s ease-out forwards;
}
@keyframes net-catch-ani { 0% { transform: translate(-50%, -50%) scale(0); opacity: 1; } 80% { transform: translate(-50%, -50%) scale(1.5); opacity: 0.8; } 100% { transform: translate(-50%, -50%) scale(1.8); opacity: 0; } }
/* --- トンボの色設定 --- */
.common .df-wing { fill: rgba(200, 200, 200, 0.6); } .common .df-body { fill: #d32f2f; }
.common2 .df-wing { fill: rgba(200, 200, 200, 0.6); } .common2 .df-body { fill: #4fc3f7; }
.uncommon .df-wing { fill: rgba(50, 50, 50, 0.6); } .uncommon .df-body { fill: #2e7d32; }
.rare .df-wing { fill: rgba(200, 200, 200, 0.5); } .rare .df-body { fill: #4caf50; }
.epic .df-wing { fill: rgba(200, 200, 200, 0.6); } .epic .df-body { fill: #212121; }
.legendary .df-wing { fill: rgba(100, 50, 150, 0.7); } .legendary .df-body { fill: #311b92; } .legendary .df-graphic { filter: drop-shadow(0 0 5px #ffca28); }
.damselfly .df-wing { fill: rgba(200, 255, 255, 0.5); } .damselfly .df-body { fill: #00e5ff; } .damselfly .df-abdomen { transform: scaleY(0.4); }
.red-damselfly .df-wing { fill: rgba(255, 200, 200, 0.5); } .red-damselfly .df-body { fill: #ff1744; } .red-damselfly .df-abdomen { transform: scaleY(0.4); }
.natsuakane .df-wing { fill: rgba(255, 200, 200, 0.3); } .natsuakane .df-body { fill: #b71c1c; }
.miyama .df-wing { fill: rgba(101, 67, 33, 0.7); } .miyama .df-body { fill: #5d4037; }
.uchiwa .df-wing { fill: rgba(200, 200, 200, 0.5); } .uchiwa .df-body { fill: #fbc02d; }
/* 隠しキャラ:ホタルの発光エフェクト */
.firefly .df-wing { fill: rgba(220, 255, 200, 0.1); }
.firefly .df-head, .firefly .df-thorax { fill: #111; }
.firefly .df-abdomen { fill: #ccff00; }
.firefly::before {
content: ''; position: absolute; top: -40%; left: -40%; width: 180%; height: 180%;
background: radial-gradient(circle, rgba(170, 255, 0, 0.6) 0%, rgba(170, 255, 0, 0.2) 40%, transparent 70%);
border-radius: 50%; z-index: -1; pointer-events: none;
animation: firefly-glow 1.8s infinite alternate ease-in-out;
}
@keyframes firefly-glow { 0% { opacity: 0.3; transform: scale(0.85); } 100% { opacity: 1; transform: scale(1.2); } }
/* 黄金のヤンマ */
.golden .df-wing { fill: rgba(255, 215, 0, 0.6); } .golden .df-body { fill: #ffea00; }
.golden .df-graphic { filter: drop-shadow(0 0 15px #ffd600); }
/* フッター */
#footer { background: #00332a; padding: 15px; display: flex; justify-content: center; gap: 15px; z-index: 10; position: relative;}
button { padding: 10px 20px; font-size: 16px; cursor: pointer; border: none; border-radius: 8px; background: #ffca28; color: #3e2723; font-weight: bold; box-shadow: 0 3px 0 #ff8f00; transition: transform 0.1s, box-shadow 0.1s;}
button:active { transform: translateY(3px); box-shadow: none; }
.btn-time { background: #b2ebf2; box-shadow: 0 3px 0 #80deea; color: #006064; }
/* 図鑑モーダル(backdrop-filterを削除して描画バグを防止) */
#modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 100; justify-content: center; align-items: center; }
#modal-content { background: #f5f5f5; width: 90%; max-width: 600px; max-height: 85vh; overflow-y: auto; padding: 25px; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); position: relative;}
.book-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 10px; margin-top: 15px; }
/* Hover時の文字ブレ防止のために translateZ(0) と backface-visibility を追加 */
.book-item { border: 2px dashed #bdbdbd; padding: 10px 5px; text-align: center; border-radius: 8px; background: #e0e0e0; color: #9e9e9e; transition: all 0.2s; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; transform: translateZ(0); backface-visibility: hidden;}
.book-item.caught { background: #fff; border: 2px solid #4caf50; color: #333; box-shadow: 0 2px 5px rgba(0,0,0,0.1); cursor: pointer; }
.book-item.caught:hover { background: #e8f5e9; transform: scale(1.05) translateZ(0); }
.book-item.secret { border-color: #ffca28; background: #263238; color: #fff; }
.book-item .bug-graphic { margin-bottom: 5px; display: flex; align-items: center; justify-content: center; height: 40px; position: relative; width: 100%;}
/* 図鑑詳細ビュー */
#detail-view { display: none; text-align: center; padding: 20px 0; }
#detail-graphic { width: 150px; height: 100px; margin: 0 auto 20px; border-radius: 50%; background: #e0f2f1; display: flex; justify-content: center; align-items: center; box-shadow: inset 0 0 10px rgba(0,0,0,0.1); position: relative;}
#detail-name { font-size: 1.8em; color: #006064; margin: 10px 0; }
#detail-desc { font-size: 1.1em; line-height: 1.6; color: #333; text-align: left; background: #fff; padding: 15px; border-radius: 8px; border-left: 5px solid #4caf50; margin-bottom: 20px;}
#detail-stats-box { display: flex; justify-content: space-around; background: #eee; padding: 10px; border-radius: 8px; }
.detail-back-btn { background: #9e9e9e; color: white; box-shadow: 0 3px 0 #757575; margin-bottom: 15px;}
.close-btn { display: block; margin: 20px auto 0; background: #e0e0e0; color: #333; box-shadow: 0 3px 0 #bdbdbd;}
#notification { position: absolute; top: 20%; left: 50%; transform: translate(-50%, -50%); background: rgba(0, 0, 0, 0.7); color: #fff; padding: 15px 30px; border-radius: 30px; font-size: 1.5em; font-weight: bold; pointer-events: none; opacity: 0; transition: opacity 0.3s ease-out; z-index: 50;}
</style>
</head>
<body>
<div id="game-container">
<div id="header">
<div class="title-area">
<h2>トンボ採集マスター v6.5</h2>
<div class="title-display">称号: <span id="current-title">トンボ見習い</span></div>
</div>
<div class="stats-area">
<div class="stats">捕獲数: <span id="catch-count">0</span></div>
<div id="time-display">☀️ 昼</div>
</div>
</div>
<div id="play-area">
<div id="time-overlay" class="time-day"></div>
</div>
<div id="footer">
<button class="btn-time" onclick="changeTime(true)">⏳ 時間を進める</button>
<button onclick="openBook()">📖 図鑑を開く</button>
</div>
</div>
<div id="notification"></div>
<div id="modal">
<div id="modal-content">
<div id="list-view">
<h2 style="margin-top: 0; color: #006064; border-bottom: 2px solid #006064; padding-bottom: 10px;">
トンボ図鑑 (<span id="comp-count">0</span>/<span id="total-count">0</span>)
</h2>
<p style="font-size:0.9em; color:#666; margin:0;">捕まえた虫をタップすると詳細が見られます。</p>
<div class="book-grid" id="book-grid"></div>
<button class="close-btn" onclick="closeBook()">閉じる</button>
</div>
<div id="detail-view">
<button class="detail-back-btn" onclick="hideDetail()">◀ 一覧に戻る</button>
<div id="detail-graphic"></div>
<h3 id="detail-name">名前</h3>
<div id="detail-stats-box">
<div>捕獲数: <strong id="detail-catch">0</strong></div>
<div>飛び方: <strong id="detail-flight"></strong></div>
</div>
<p id="detail-desc">説明文</p>
</div>
</div>
</div>
<script>
// SVGタグに width="100%" height="100%" を追加してレイアウト崩れを完全に防ぐ
const dragonflySvg = `
<svg viewBox="0 0 60 40" class="df-graphic" width="100%" height="100%">
<ellipse class="df-wing df-wing-top-left" cx="20" cy="10" rx="15" ry="6" transform="rotate(-15 20 10)"/>
<ellipse class="df-wing df-wing-bottom-left" cx="20" cy="18" rx="12" ry="5" transform="rotate(-5 20 18)"/>
<ellipse class="df-wing df-wing-top-right" cx="40" cy="10" rx="15" ry="6" transform="rotate(15 40 10)"/>
<ellipse class="df-wing df-wing-bottom-right" cx="40" cy="18" rx="12" ry="5" transform="rotate(5 40 18)"/>
<ellipse class="df-body df-head" cx="30" cy="10" rx="3.5" ry="3.5" />
<ellipse class="df-body df-thorax" cx="30" cy="16" rx="3.5" ry="5.5" />
<rect class="df-body df-abdomen" x="28.5" y="21" width="3" height="17" rx="1.5" ry="1.5"/>
</svg>
`;
const bugsData = [
{ id: 1, name: 'アキアカネ', rarity: 'common', speed: 4.5, prob: 25, restChance: 0.015, scale: 0.8, flightType: 'erratic', allowedTimes: ['day', 'sunset'], caught: 0, desc: 'いわゆる「赤とんぼ」。昼間から夕方にかけてよく飛んでいる。' },
{ id: 2, name: 'ナツアカネ', rarity: 'natsuakane', speed: 4.8, prob: 15, restChance: 0.015, scale: 0.8, flightType: 'erratic', allowedTimes: ['day'], caught: 0, desc: '全身が真っ赤に染まるのが特徴。真昼の暑い時間に元気。' },
{ id: 3, name: 'シオカラトンボ', rarity: 'common2', speed: 6.0, prob: 20, restChance: 0.012, scale: 0.9, flightType: 'hover', allowedTimes: ['day', 'sunset'], caught: 0, desc: '水色っぽい体。空中でピタッと止まるホバリングが得意。' },
{ id: 4, name: 'イトトンボ', rarity: 'damselfly', speed: 2.5, prob: 18, restChance: 0.02, scale: 0.5, flightType: 'flutter', allowedTimes: ['day'], caught: 0, desc: '糸のように細い小さなトンボ。草むらを弱々しく飛ぶ。' },
{ id: 5, name: 'ベニイトトンボ', rarity: 'red-damselfly', speed: 2.5, prob: 8, restChance: 0.02, scale: 0.5, flightType: 'flutter', allowedTimes: ['day', 'sunset'], caught: 0, desc: '全身が鮮やかなオレンジ〜赤色。夕暮れ時にも見かける。' },
{ id: 6, name: 'ハグロトンボ', rarity: 'uncommon', speed: 5.0, prob: 12, restChance: 0.005, scale: 0.9, flightType: 'flutter', allowedTimes: ['day', 'sunset'], caught: 0, desc: '黒い羽でチョウのように舞う。「神様とんぼ」とも呼ばれる。' },
{ id: 7, name: 'ミヤマカワトンボ', rarity: 'miyama', speed: 6.5, prob: 7, restChance: 0.01, scale: 1.1, flightType: 'flutter', allowedTimes: ['day'], caught: 0, desc: '渓流の宝石。茶色い羽に白い紋がある。' },
{ id: 8, name: 'ウチワヤンマ', rarity: 'uchiwa', speed: 9.0, prob: 6, restChance: 0.015, scale: 1.3, flightType: 'erratic', allowedTimes: ['day'], caught: 0, desc: '尻尾の先に「うちわ」のような広がりがあるのが特徴。' },
{ id: 9, name: 'ギンヤンマ', rarity: 'rare', speed: 13.0, prob: 8, restChance: 0.000, scale: 1.2, flightType: 'patrol', allowedTimes: ['day', 'sunset'], caught: 0, desc: '高速で池の周りをパトロール飛行する青と緑のヤンマ。' },
{ id: 10, name: 'オニヤンマ', rarity: 'epic', speed: 17.0, prob: 4, restChance: 0.000, scale: 1.5, flightType: 'patrol', allowedTimes: ['day'], caught: 0, desc: '日本最大のトンボ。圧倒的な存在感で真っ直ぐ飛ぶ。' },
{ id: 11, name: 'チョウトンボ', rarity: 'legendary', speed: 7.0, prob: 2, restChance: 0.002, scale: 0.8, flightType: 'flutter', allowedTimes: ['day', 'sunset'], caught: 0, desc: 'メタリックな羽で蝶のように滑空する美しいトンボ。' },
// --- 隠しキャラ ---
{ id: 12, name: 'ヘイケボタル', rarity: 'firefly', speed: 1.5, prob: 150, restChance: 0.05, scale: 0.5, flightType: 'flutter', allowedTimes: ['night'], isSecret: true, caught: 0,
desc: '【隠しキャラ】夜の闇に浮かぶ幻想的な光。暗い体でお尻だけを強く光らせ、周囲をぼんやりと緑色に染めながらフワフワと舞う。' },
{ id: 13, name: '黄金のヤンマ', rarity: 'golden', speed: 20.0, prob: 1, restChance: 0.0, scale: 1.5, flightType: 'patrol', allowedTimes: ['day', 'sunset', 'night'], isSecret: true, caught: 0,
desc: '【幻の虫】全てが黄金に輝く伝説のヤンマ。その速度は捉えることすら困難。' }
];
const bamboosData = [
{ id: 1, left: '25%', height: '35%', el: null, targetX: 0, targetY: 0, occupied: false },
{ id: 2, left: '55%', height: '42%', el: null, targetX: 0, targetY: 0, occupied: false },
{ id: 3, left: '80%', height: '30%', el: null, targetX: 0, targetY: 0, occupied: false }
];
const baseTitles = [
{ req: 0, name: 'トンボ見習い' }, { req: 10, name: '駆け出しハンター' },
{ req: 30, name: '熟練の網さばき' }, { req: 50, name: 'トンボマニア' },
{ req: 100, name: '神の目を持つ者' }
];
let totalCaught = 0;
let activeBugs = [];
const playArea = document.getElementById('play-area');
const bookGrid = document.getElementById('book-grid');
const times = ['day', 'sunset', 'night'];
let timeIndex = 0;
let currentTime = times[timeIndex];
let timeIntervalId;
let isGamePaused = false; // 図鑑展開時のフリーズ用フラグ
function init() {
const regularCount = bugsData.filter(b => !b.isSecret).length;
document.getElementById('total-count').textContent = regularCount;
updateBookUI();
initBamboos();
setInterval(spawnBug, 1000);
setInterval(moveBugs, 30);
timeIntervalId = setInterval(changeTime, 30000);
}
function changeTime(isManual = false) {
if (isGamePaused && !isManual) return; // 図鑑を開いている間は自動で時間が進まない
if (isManual) {
clearInterval(timeIntervalId);
timeIntervalId = setInterval(changeTime, 30000);
}
timeIndex = (timeIndex + 1) % times.length;
currentTime = times[timeIndex];
const overlay = document.getElementById('time-overlay');
overlay.className = `time-${currentTime}`;
const timeDisplay = document.getElementById('time-display');
const timeText = { 'day': '☀️ 昼', 'sunset': '🌇 夕方', 'night': '🌙 夜' };
const timeColor = { 'day': '#ffeb3b', 'sunset': '#ffcc80', 'night': '#b39ddb' };
timeDisplay.textContent = timeText[currentTime];
timeDisplay.style.color = timeColor[currentTime];
}
function initBamboos() {
bamboosData.forEach(b => {
const el = document.createElement('div');
el.className = 'bamboo';
el.style.left = b.left; el.style.height = b.height;
playArea.appendChild(el); b.el = el;
});
setTimeout(updateBambooPositions, 100);
window.addEventListener('resize', updateBambooPositions);
}
function updateBambooPositions() {
const playRect = playArea.getBoundingClientRect();
bamboosData.forEach(b => {
const rect = b.el.getBoundingClientRect();
b.targetX = (rect.left - playRect.left) + (rect.width / 2);
b.targetY = (rect.top - playRect.top);
});
}
function spawnBug() {
if (isGamePaused || activeBugs.length >= 12) return; // 図鑑を開いている時は湧かない
const availableBugs = bugsData.filter(b => b.allowedTimes.includes(currentTime));
if(availableBugs.length === 0) return;
let totalProb = availableBugs.reduce((sum, bug) => sum + bug.prob, 0);
let rand = Math.random() * totalProb;
let sum = 0;
let selectedBug = availableBugs[0];
for (let bug of availableBugs) {
sum += bug.prob;
if (rand <= sum) { selectedBug = bug; break; }
}
createBugElement(selectedBug);
}
function createBugElement(bugData) {
const el = document.createElement('div');
el.className = `dragonfly fluttering ${bugData.rarity}`;
el.innerHTML = dragonflySvg;
const baseW = 60; const baseH = 40;
el.style.width = (baseW * bugData.scale) + 'px';
el.style.height = (baseH * bugData.scale) + 'px';
let x = Math.random() * (playArea.clientWidth - (baseW * bugData.scale));
let y = Math.random() * (playArea.clientHeight - (baseH * bugData.scale));
el.style.left = x + 'px'; el.style.top = y + 'px';
let angle = Math.random() * Math.PI * 2;
let actualSpeed = bugData.speed + (Math.random() * 2 - 1);
const bugObj = {
el, data: bugData, x, y, angle, speed: actualSpeed,
state: 'flying', targetRest: null, restTimeout: null, lifeTimer: null, isHovering: false
};
el.onmousedown = (e) => catchBug(bugObj, e);
el.ontouchstart = (e) => { e.preventDefault(); catchBug(bugObj, e.touches[0]); };
playArea.appendChild(el);
activeBugs.push(bugObj);
setBugLife(bugObj);
}
function setBugLife(bugObj) {
let lifeBase = bugObj.data.rarity === 'firefly' ? 8000 : 4000;
bugObj.lifeTimer = setTimeout(() => {
// 一時停止中は消さないなどの厳密な管理も可能だが、今回は簡易的に
if (!isGamePaused && activeBugs.includes(bugObj)) removeBug(bugObj);
}, lifeBase + Math.random() * 4000);
}
function moveBugs() {
if (isGamePaused) return; // 図鑑を開いている時は動かない(画面ブレを完全に防ぐ)
activeBugs.forEach(bug => {
if (bug.state === 'flying') {
if (bug.data.restChance > 0 && Math.random() < bug.data.restChance && !bug.isHovering) {
let freeBamboos = bamboosData.filter(b => !b.occupied);
if (freeBamboos.length > 0) {
let target = freeBamboos[Math.floor(Math.random() * freeBamboos.length)];
bug.state = 'seeking_rest';
bug.targetRest = target; target.occupied = true;
clearTimeout(bug.lifeTimer);
return;
}
}
let speedMult = 1; let angleWiggle = 0.5;
if (bug.data.flightType === 'patrol') {
angleWiggle = 0.1;
} else if (bug.data.flightType === 'flutter') {
angleWiggle = 1.0;
} else if (bug.data.flightType === 'hover') {
if (Math.random() < 0.015 && !bug.isHovering) {
bug.isHovering = true;
setTimeout(() => {
if(activeBugs.includes(bug)) {
bug.isHovering = false;
bug.angle += Math.PI * (Math.random() - 0.5);
}
}, 600 + Math.random() * 1000);
}
if (bug.isHovering) speedMult = 0; else angleWiggle = 0.1;
}
bug.angle += (Math.random() - 0.5) * angleWiggle;
bug.x += Math.cos(bug.angle) * bug.speed * speedMult;
bug.y += Math.sin(bug.angle) * bug.speed * speedMult;
const elWidth = bug.el.offsetWidth; const elHeight = bug.el.offsetHeight;
if (bug.x <= 0 || bug.x >= playArea.clientWidth - elWidth) {
bug.angle = Math.PI - bug.angle;
bug.x = Math.max(0, Math.min(bug.x, playArea.clientWidth - elWidth));
}
if (bug.y <= 0 || bug.y >= playArea.clientHeight - elHeight) {
bug.angle = -bug.angle;
bug.y = Math.max(0, Math.min(bug.y, playArea.clientHeight - elHeight));
}
bug.el.style.left = bug.x + 'px'; bug.el.style.top = bug.y + 'px';
bug.el.style.transform = `rotate(${bug.angle + Math.PI / 2}rad)`;
} else if (bug.state === 'seeking_rest') {
let tx = bug.targetRest.targetX - (bug.el.offsetWidth / 2);
let ty = bug.targetRest.targetY - (bug.el.offsetHeight / 2);
let dx = tx - bug.x; let dy = ty - bug.y;
let dist = Math.sqrt(dx*dx + dy*dy);
if (dist < 5) {
bug.state = 'resting';
bug.x = tx; bug.y = ty;
bug.el.style.left = bug.x + 'px'; bug.el.style.top = bug.y + 'px';
bug.el.classList.remove('fluttering');
bug.el.style.transform = `rotate(-0.3rad)`;
bug.restTimeout = setTimeout(() => {
if (activeBugs.includes(bug)) {
bug.state = 'flying';
bug.targetRest.occupied = false; bug.targetRest = null;
bug.el.classList.add('fluttering');
setBugLife(bug);
}
}, 2000 + Math.random() * 3000);
} else {
bug.angle = Math.atan2(dy, dx);
bug.x += Math.cos(bug.angle) * bug.speed;
bug.y += Math.sin(bug.angle) * bug.speed;
bug.el.style.left = bug.x + 'px'; bug.el.style.top = bug.y + 'px';
bug.el.style.transform = `rotate(${bug.angle + Math.PI / 2}rad)`;
}
}
});
}
function catchBug(bug, e) {
if (isGamePaused) return; // 図鑑展開中は捕まえられない
createNetCatch(e.clientX, e.clientY);
removeBug(bug);
bug.data.caught++;
totalCaught++;
document.getElementById('catch-count').textContent = totalCaught;
if(bug.data.isSecret) {
showNotification(`✨ ${bug.data.name} を発見した!! ✨`);
} else {
showNotification(`${bug.data.name} を捕まえた!`);
}
checkTitle();
updateBookUI();
}
function createNetCatch(x, y) {
const net = document.createElement('div');
net.className = 'net-catch';
net.style.left = x + 'px'; net.style.top = (y - 50) + 'px';
playArea.appendChild(net);
net.addEventListener('animationend', () => {
if (playArea.contains(net)) playArea.removeChild(net);
});
}
function removeBug(bug) {
if (playArea.contains(bug.el)) playArea.removeChild(bug.el);
if (bug.targetRest) bug.targetRest.occupied = false;
if (bug.restTimeout) clearTimeout(bug.restTimeout);
if (bug.lifeTimer) clearTimeout(bug.lifeTimer);
activeBugs = activeBugs.filter(b => b !== bug);
}
function checkTitle() {
let currentTitle = baseTitles[0].name;
for (let i = baseTitles.length - 1; i >= 0; i--) {
if (totalCaught >= baseTitles[i].req) { currentTitle = baseTitles[i].name; break; }
}
const regularCaught = bugsData.filter(d => !d.isSecret && d.caught > 0).length;
const golden = bugsData.find(d => d.name === '黄金のヤンマ');
if (golden && golden.caught > 0) currentTitle = '👑 黄金を掴みし者';
else if (regularCaught >= 11) currentTitle = '🌟 生態系を識る者';
document.getElementById('current-title').textContent = currentTitle;
}
function updateBookUI() {
bookGrid.innerHTML = '';
let comp = 0;
bugsData.forEach(bug => {
if (bug.isSecret && bug.caught === 0) return;
const div = document.createElement('div');
div.className = `book-item ${bug.caught > 0 ? 'caught' : ''} ${bug.isSecret ? 'secret' : ''}`;
const graphicWrap = document.createElement('div');
graphicWrap.className = 'bug-graphic';
if(bug.isSecret) graphicWrap.classList.add(bug.rarity);
if (bug.caught > 0) {
if(!bug.isSecret) comp++;
const graphic = document.createElement('div');
graphic.className = `df-graphic ${bug.rarity}`;
const bookScale = bug.scale * 0.7;
graphic.style.width = (60 * bookScale) + 'px';
graphic.style.height = (40 * bookScale) + 'px';
graphic.style.position = 'relative';
graphic.innerHTML = dragonflySvg;
graphicWrap.appendChild(graphic);
div.appendChild(graphicWrap);
div.innerHTML += `<strong class="${bug.rarity}" style="font-size:0.9em;">${bug.name}</strong><br><small style="color:${bug.isSecret ? '#bbb' : '#666'};">捕獲: ${bug.caught}</small>`;
div.onclick = () => showDetail(bug.id);
} else {
graphicWrap.innerHTML = `<div class="df-graphic" style="background: rgba(0,0,0,0.1); border-radius: 50%; width: 40px; height: 40px;"></div>`;
div.appendChild(graphicWrap);
div.innerHTML += `<strong style="font-size:0.9em;">???</strong><br><small>未発見</small>`;
}
bookGrid.appendChild(div);
});
document.getElementById('comp-count').textContent = comp;
}
const flightTypeLabels = {
'erratic': '不規則・止まる', 'hover': 'ホバリング・急発進',
'flutter': 'ヒラヒラ飛ぶ', 'patrol': '直線パトロール'
};
function showDetail(id) {
const bug = bugsData.find(d => d.id === id);
if(!bug) return;
const graphicContainer = document.getElementById('detail-graphic');
graphicContainer.style.background = bug.isSecret ? '#1a1a2e' : '#e0f2f1';
graphicContainer.innerHTML = `<div class="${bug.rarity}" style="position:relative; width:${60 * bug.scale * 2}px; height:${40 * bug.scale * 2}px;">${dragonflySvg}</div>`;
document.getElementById('detail-name').textContent = bug.name;
document.getElementById('detail-catch').textContent = bug.caught + ' 匹';
document.getElementById('detail-flight').textContent = flightTypeLabels[bug.flightType] || '不明';
document.getElementById('detail-desc').textContent = bug.desc;
document.getElementById('list-view').style.display = 'none';
document.getElementById('detail-view').style.display = 'block';
}
function hideDetail() {
document.getElementById('list-view').style.display = 'block';
document.getElementById('detail-view').style.display = 'none';
}
let notifTimeout;
function showNotification(text) {
const notif = document.getElementById('notification');
notif.textContent = text; notif.style.opacity = 1;
clearTimeout(notifTimeout);
notifTimeout = setTimeout(() => { notif.style.opacity = 0; }, 1800);
}
function openBook() {
isGamePaused = true; // ゲームをフリーズさせて画面のブレを防ぐ
document.getElementById('modal').style.display = 'flex';
hideDetail();
}
function closeBook() {
isGamePaused = false; // 図鑑を閉じたらゲーム再開
document.getElementById('modal').style.display = 'none';
}
window.onload = init;
</script>
</body>
</html>


■ 今日の学び

思えば、トンボには不思議な思い出がたくさんあります。
目の前を飛んでいるトンボを、思わず素手でパシッと捕まえて友達を驚かせたり。
指をぐるぐる回して目を回させてから、
そっと捕まえる……なんていう、
懐かしい遊びをしたこともありました。d(^^)

夕暮れの空をトンボが飛んでいる風景を見るだけで、
なんだか優しい気持ちになれる。
そんな「懐かしさ」を形にしたくて、今回のゲームを作りました。

今回の開発では、AIと一緒に「リアリティの追求」に挑戦しました。(=v=*)

特に苦労したのは、ホタルの光り方です。
最初は全体が光ってしまって「なんだか電球みたい?」
なんて思ったりもしたのです。(つ_=*)

試行錯誤の末に、お尻からフワァ…とオーラが広がる幻想的な明滅を再現することができました。

AIに「このトンボはもっと一直線に!」「イトトンボはもっと弱々しく…」と細かくお願いして、図鑑を開くたびに学びがあるような、本格的なシミュレーションに近づけることができました。

■ 次回は、お掃除ゲームです。

次回は、しょうが君が登場します。
落ち葉でいっぱいの庭を、できるだけ荒らさずに、
葉っぱだけをそっと画面の外へ吹き飛ばしていくゲームです。

ふわっと舞う落ち葉と、
しょうが君の軽やかな動きが楽しい作品になっています。

次回もぜひ遊びに来てくださいね。(*^v^)ノシ

次の実験はこちら(吹き飛ばしと重さ)「しょうが君のエアダスター」



 ✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)

~このブログについて~

AIと一緒に遊び、
100の実験でゲームを育てるブログ

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー
🐣未記事ゲーム展示室


AIと作る100のゲーム実験室(*^v^*)をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む