■118個目 「LUNAR ASCENSION」
こんにちは、AIと作る100のゲーム実験室の みー です。(*^_^*)/
今回は、海抜0mから“月面”を目指してひたすら上っていく、
絶景クリッカーゲームを作ってみました。
透明なチューブの中に続く螺旋階段を、
宇宙へ向かってコツコツと駆け上がっていく旅です。(^_^)
今回こだわったのは、旅をそっと彩ってくれる「マスターの雑学」と、
広大な宇宙のスケール感です。(=v=*)
「果てしない道のりでも、誰かが面白い話をしてくれたらロマンがあるんじゃないかな?」 そんな遊び心から生まれた、
やりこみ要素たっぷりのゲームになっています。
螺旋階段を上っていくのは、小さなドット絵のロボットくん。
高度が上がるにつれて、足元の青い地球はゆっくりと遠ざかり、
頭上からは巨大な月がじわじわと迫ってきます。
さらに、特定の高度に到達すると、
ちょっと渋いマスターが「月にまつわる不思議な雑学」を語ってくれるんです。
静かな宇宙の旅路に、ふっと灯る語りがとても心地よいんですよ。
果たしてロボットくんは、
38万4400キロ先のゴールへ無事に辿り着けるのでしょうか。
月面に着いた時の達成感は、きっと特別なものになるはずです。
詳しい操作方法や遊び方は、この後ご紹介しますね。
■ AIとのやりとりで面白かった部分
● ゲーム仕様の相談からスタート
みー:(^v^*) )* 今日もゲームを作ります。
今回は HTML で動く、
螺旋階段をひたすら上っていくクリッカー絶景ゲームです。
透明なチューブの中をコツコツと上り続けて、
海抜0mから月までの距離を一段20cmで積み重ねていきます。
視覚的にも楽しくて、やりこみ中毒要素のあるゲームにしたいです。
AI: みーさん、今日も新作の開発ですね。
海抜0mから月(約38万4400km)まで
階段で上るというスケールの大きさは、
クリッカーゲームとの相性が抜群です。
今回は、透明チューブの螺旋階段を HTML5 Canvas で疑似3D表現し、
背景が大気圏から宇宙へと変化する絶景演出を追加しました。
効果音には Web Audio API の矩形波を使って、
レトロなピコピコ音も再現しています。
みー:(^_^) 画面左に世界観と月の雑学を表示して、
右側には高度に合わせて遠ざかる地表を描きましょう。
出発点は東京沖100kmの海上にして、
月までの道のりを21地点に分けて雑学を変えていきたいです。
AI: 画面を3分割し、左側に「世界観+月の雑学」コーナー、
右側には高度に応じて遠ざかる地球をリアルタイム描画するようにしました。
雑学は全21地点分、重複なしで用意してあります。旅のお供にどうぞ。
みー:(=v=) 右パネルで地球が遠ざかるだけでなく、
上から月面が近づいてくる演出も入れましょう。
AI: いいですね。 画面下では青い地球がゆっくり遠ざかり、
上からは巨大な月面がじわじわと迫ってくるようにしました。
終盤になるほど視界いっぱいに月が広がり、ゴールへの期待感が高まります。
みー:( ̄v ̄) ランク10ごとにボーナスが欲しいです。
エネルギーが枯渇したら落下するペナルティも追加しましょう。
螺旋階段の中央にドット絵ロボを表示して、
歩き → 飛行の変化でパワーアップ感を出したいです。
クリック時にはクリックポイントの数字がふわっと出る演出も入れましょう。
AI: 素敵なアイデアです。以下の機能を追加しました。
- 落下演出:
エネルギーがゼロになると、ロボが目を回して真っ逆さまに落下 - ランクボーナス(10Lvごとに効率2倍):
UIに倍率表示も追加 - クリック時のポイントポップアップ:
「+100」などがふわっと浮かんで消える演出 - エネルギー枯渇時の高度減少:
自動昇降が止まり、ズリズリと段数が減るスリル - ドット絵ロボの表示:
歩き → 飛行(ジェット噴射)で見た目が変化
みー:ヽ(*^_^*)ノ ロボが飛行を始めたらオーラをまとわせて、
1秒あたりの段数が1M増えるごとに色が変わるようにしましょう。
ゴール時には月面にロボの足跡を“ポン”とスタンプして、
祝福のパーティクルを出しましょう。
そのまま成長を引き継ぐ「強くてニューゲーム」と、
初期化して遊ぶ「最初から」の2つのボタンも出したいです。
AI: ついに完成形が見えてきましたね。以下を実装しました。
クリア時のスタンプ&パーティクル:
月面到着と同時に足跡スタンプ+紙吹雪の祝福演出
周回プレイ:
「強くてニューゲーム」と「最初から」の2ボタンを追加
オーラ演出:
SPSが500を超えるとオーラ展開、1Mごとに色が変わる8段階仕様
クリア時のスタンプ&パーティクル:
月面到着と同時に足跡スタンプ+紙吹雪の祝福演出
■ 実際の動作
まずは、AI が最初に出してくれた試作版のゲーム画面をご紹介します。
階段を上って月を目指すクリッカーゲームの、いちばん最初のイメージです。

(画像:AIが最初に出してくれたゲーム画面)
こちらが、仕上がりのスタート画面です。
クリックしてためた距離ポイントを使って、
ショップでロボをパワーアップできます。
ポイントを使うと、その分だけ距離が戻る仕組みなので、
育成と前進のバランスを楽しんでいただけます。

(画像:仕上がりのゲーム画面)
距離に応じて、左側に表示される「月の雑学」が変わっていきます。
クリックしながら、
ゆっくりと宇宙の豆知識を楽しんでいただけると嬉しいです。

(画像:ゲーム画面⓵)
ロボはスピードが上がるほど、まとうオーラの色が変化します。
右側のパネルでは、地球が遠ざかり、
月が近づいてくる様子が美しく描かれます。
旅の進み具合が視覚的に分かるので、眺めているだけでも楽しいですよ。

(画像:ゲーム画面⓶)
月まで到着すると、ロボの記念の足跡が“ポン”とスタンプされて、
クリアになります。
そのままロボをもっと成長させたい方のために、やりこみ要素として
「強くてニューゲーム」もご用意しました。

(画像:月到着クリア画面)
■ コードのポイント
今回の記事では、
AI と相談しながら作った「LUNAR ASCENSION」 の HTML コードを公開します。
このコードをそのまま保存するだけで、
あなたの PC でもすぐに遊べます。
ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。
▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く
Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)
② 下のコードをコピーして貼り付けます。
③ 「tukiniiku.html」など、好きな名前で保存します。
(※拡張子は.html にしてください)
④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。
■ 遊び方・操作方法
基本的な操作は「クリック(またはタップ)」だけです!(^^*)
でも今回は、ちょっとした“やりこみ要素”とスリルが隠れています。
1. とにかくタップで登る!&エネルギーチャージ
画面中央の「TAP TO CLIMB」をタップすると、
ロボットくんが階段を上っていきます。
このゲームの特徴は、時間とともに減っていく
自動昇降エネルギー です。(・v・)
エネルギーがゼロになると、
重力に負けてロボがゆっくり落下してしまいます。
タップすると少し回復するので、
ロボに元気を送るようにポチポチしてあげてくださいね。
2. アイテムでパワーアップ(アップグレード)
画面下には、
これまでに登った段数(コスト)を使って購入できるアイテムが並んでいます。
- クリックの威力を上げる「軽量スニーカー」
- 自動で登ってくれる「星間バザールの熱気」
など、好きなものを強化して効率を上げていきましょう。(=v=*)
ワンポイント:
各アイテムは
レベルが10の倍数になるたびに効率が2倍 に跳ね上がるボーナスがあります!
3. 限界突破!オーラ演出
自動で登るスピードが一定(100万段/秒)を超えると、
ロボットくんがオーラをまとい始めます。
さらに、スピードが100万段増えるごとにオーラの色が変化していきます。
宇宙を駆け上がるロボの成長を、色の変化で楽しんでみてくださいね。(^▽^)
4. 月面到達と「強くてニューゲーム」
38万4400キロ先の月面に到達すると、
記念の足跡が“ポン”とスタンプされて、感動のゴールです。
クリア後は、成長したステータスをそのまま引き継いで再スタートできる
「強くてニューゲーム」 が選べるようになります。
周回を重ねて、どこまで速く登れるか挑戦してみてくださいね。(^_^)/
ヘッドホンやイヤホンを使うと、
状態に合わせて変化する BGM もより楽しめます。
ぜひ音を出しながらプレイしてみてください。
“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。
▼ここからコード▼(クリックで開く)
<html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>LUNAR ASCENSION - ゼロから月への軌跡 -</title> <style> body { margin: 0; padding: 0; background-color: #0a0a0a; color: #fff; font-family: 'Courier New', monospace; display: flex; height: 100vh; overflow: hidden; user-select: none; } #left-panel { width: 25%; background: #111; border-right: 2px solid #333; padding: 25px 20px; box-sizing: border-box; display: flex; flex-direction: column; } #center-panel { width: 50%; position: relative; background: #000; overflow: hidden; } #right-panel { width: 25%; background: #111; border-left: 2px solid #333; position: relative; overflow: hidden; } /* 左パネル:視認性アップ */ .worldview-title { color: #0ff; font-size: 20px; margin-bottom: 12px; border-bottom: 2px solid #0ff; padding-bottom: 5px; font-weight: bold; } .worldview-desc { font-size: 15px; color: #ccc; line-height: 1.6; margin-bottom: 25px; } .master-container { background: #1a1a1a; border: 1px solid #444; border-radius: 8px; padding: 20px; flex-grow: 1; display: flex; flex-direction: column; position: relative; overflow: hidden; } .master-title { color: #ffb74d; font-weight: bold; margin-bottom: 15px; font-size: 16px; } #trivia-text { font-size: 16px; line-height: 1.8; color: #e0e0e0; transition: opacity 0.3s ease; } .flash-bg { animation: flashEffect 1.5s ease-out; } @keyframes flashEffect { 0% { background-color: #004444; box-shadow: inset 0 0 20px #0ff; } 100% { background-color: #1a1a1a; box-shadow: inset 0 0 0px #0ff; } } #notice-badge { position: absolute; top: 15px; right: 15px; background: #f00; color: #fff; font-size: 12px; font-weight: bold; padding: 4px 10px; border-radius: 12px; opacity: 0; transition: opacity 0.3s; } canvas { display: block; width: 100%; height: 100%; } #ui-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: flex; flex-direction: column; justify-content: space-between; } /* 中央ヘッダー */ .header { background: rgba(0, 0, 0, 0.85); padding: 15px; text-align: center; border-bottom: 2px solid #555; } .header h1 { margin: 0 0 10px 0; font-size: 22px; color: #0ff; letter-spacing: 2px; } .header .sub-title { font-size: 12px; color: #aaa; letter-spacing: 1px; display: block; margin-top: 2px; } .alert-text { color: #f44336; font-weight: bold; animation: blink 1s infinite; display: none; font-size: 14px; margin-bottom: 5px; } @keyframes blink { 50% { opacity: 0; } } .progress-box { width: 90%; margin: 5px auto; position: relative; } .progress-label { font-size: 12px; text-align: left; color: #ddd; margin-bottom: 3px; } .bar-container { height: 10px; background: #333; border-radius: 5px; overflow: hidden; } #progress-bar { height: 100%; background: #0ff; width: 0%; transition: width 0.2s; } #energy-bar { height: 100%; background: linear-gradient(90deg, #f44336, #ffeb3b, #4caf50); width: 100%; } .stats { font-size: 15px; margin-top: 12px; display: flex; justify-content: space-around; font-weight: bold; } #click-zone { flex-grow: 1; pointer-events: auto; cursor: crosshair; display: flex; justify-content: center; align-items: center; flex-direction: column; position: relative; } #click-text { font-size: 32px; font-weight: bold; color: rgba(255, 255, 255, 0.1); text-shadow: 0 0 10px rgba(255,255,255,0.3); animation: pulse 2s infinite; pointer-events: none; } @keyframes pulse { 0% { opacity: 0.1; } 50% { opacity: 0.3; } 100% { opacity: 0.1; } } #upgrades { background: rgba(0, 0, 0, 0.9); height: 260px; border-top: 2px solid #555; pointer-events: auto; overflow-y: auto; padding: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .upgrade-btn { background: #222; border: 1px solid #444; color: #fff; padding: 10px; cursor: pointer; text-align: left; display: flex; justify-content: space-between; font-family: inherit; border-radius: 6px; position: relative; transition: background 0.2s; } .upgrade-btn:hover { background: #333; border-color: #666; } .upgrade-btn:active { transform: scale(0.98); } .upgrade-btn:disabled { background: #111; color: #555; border-color: #222; cursor: not-allowed; } .upg-left { display: flex; flex-direction: column; justify-content: center; z-index: 2; } .upg-right { text-align: right; display: flex; flex-direction: column; justify-content: space-between; z-index: 2; } .upgrade-name { font-weight: bold; color: #ffeb3b; font-size: 13px; } .rank-bonus { color: #e040fb; font-weight: bold; font-size: 12px; margin-left: 5px; } .upgrade-desc { font-size: 11px; color: #aaa; margin-top: 3px; } .upgrade-cost { color: #f44336; font-size: 12px; font-weight: bold; } .upgrade-count { color: #0f0; font-size: 12px; font-weight: bold; } ::-webkit-scrollbar { width: 8px; } ::-webkit-scrollbar-track { background: #111; } ::-webkit-scrollbar-thumb { background: #555; border-radius: 4px; } /* クリア時のメニュー */ #clear-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 100; display: none; flex-direction: column; justify-content: center; align-items: center; pointer-events: auto; } #clear-overlay h2 { color: #ffeb3b; font-size: 40px; text-shadow: 0 0 20px #ffeb3b; margin-bottom: 40px; } .btn-restart { background: #222; border: 2px solid #0ff; color: #0ff; padding: 15px 30px; font-size: 18px; margin: 10px; cursor: pointer; border-radius: 8px; font-family: 'Courier New', monospace; font-weight: bold; transition: all 0.2s; width: 300px; text-align: center; } .btn-restart:hover { background: #0ff; color: #000; box-shadow: 0 0 15px #0ff; } .btn-restart.strong { border-color: #ffeb3b; color: #ffeb3b; } .btn-restart.strong:hover { background: #ffeb3b; color: #000; box-shadow: 0 0 15px #ffeb3b; } </style></head><body><div id="left-panel"> <div class="worldview-title">MISSION OVERVIEW</div> <div class="worldview-desc"> 目標地点:月面(高度384,400km)<br> 【警告】エネルギー枯渇時、高度は急降下します。<br> 【オーラ】飛行時、1M段/秒上昇ごとに色が変化! </div> <div id="master-box" class="master-container"> <div id="notice-badge">NEW!</div> <div class="master-title">◆ マスターの雑学録</div> <div id="trivia-text">タップしてシステムを起動してくれ。</div> </div></div><div id="center-panel"> <canvas id="viewCanvas"></canvas> <div id="ui-layer"> <div class="header"> <h1>LUNAR ASCENSION<span class="sub-title">ゼロから月への軌跡</span></h1> <div id="fall-alert" class="alert-text">⚠ エネルギー枯渇!落下中 ⚠</div> <div class="progress-box"> <div class="progress-label">月への到達度</div> <div class="bar-container"><div id="progress-bar"></div></div> </div> <div class="progress-box" style="margin-top: 8px;"> <div class="progress-label">自動昇降エネルギー(クリックで回復)</div> <div class="bar-container"><div id="energy-bar"></div></div> </div> <div class="stats"> <span id="alt-display">高度: 0 m</span> <span id="sps-display" style="color:#88ff88">0 段/秒</span> </div> </div> <div id="click-zone"><div id="click-text">TAP TO START</div></div> <div id="upgrades"></div> <div id="clear-overlay"> <h2>MISSION CLEAR!</h2> <button class="btn-restart strong" onclick="restartGame(true)">強くてニューゲーム (成長維持)</button> <button class="btn-restart" onclick="restartGame(false)">最初から (完全初期化)</button> </div> </div></div><div id="right-panel"> <canvas id="earthCanvas"></canvas></div><script> const STEP_HEIGHT = 0.2; const TARGET_ALTITUDE = 384400000; const MAX_STEPS = TARGET_ALTITUDE / STEP_HEIGHT; // 1,922,000,000 const COST_CAP = 1900000000; // 月到達直前で確実に購入できるキャップ値 let state = { steps: 0, altitude: 0, clickPower: 1, baseStepsPerSecond: 0, currentSps: 0, energy: 100, cleared: false, isFalling: false, loops: 0 }; let floatingTexts = []; let particles = []; let stampAnim = { active: false, scale: 5, alpha: 0 }; const upgrades = [ { id: 'shoes', name: '軽量スニーカー', desc: 'クリック段数+1', cost: 10, type: 'click', power: 1, count: 0 }, { id: 'turtle', name: 'イシガメの歩み', desc: '自動: 5段/秒', cost: 50, type: 'auto', power: 5, count: 0 }, { id: 'watermelon', name: 'スイカエナジー', desc: 'クリック段数+10', cost: 300, type: 'click', power: 10, count: 0 }, { id: 'stirrer', name: 'オーバル型スターラー', desc: '自動: 100段/秒', cost: 1500, type: 'auto', power: 100, count: 0 }, { id: 'jitter', name: 'ジターリング', desc: 'クリック段数+100', cost: 8000, type: 'click', power: 100, count: 0 }, { id: 'bazaar', name: '星間バザールの熱気', desc: '自動: 1,000段/秒', cost: 40000, type: 'auto', power: 1000, count: 0 }, { id: 'elevator', name: '軌道エレベーター', desc: '自動: 8,000段/秒', cost: 250000, type: 'auto', power: 8000, count: 0 }, { id: 'base', name: '月面基地の支援', desc: 'クリック段数+5,000', cost: 1500000, type: 'click', power: 5000, count: 0 }, { id: 'antigrav', name: '反重力エンジン', desc: '自動: 50,000段/秒', cost: 10000000, type: 'auto', power: 50000, count: 0 }, { id: 'warp', name: '空間跳躍ゲート', desc: '自動: 300,000段/秒', cost: 80000000, type: 'auto', power: 300000, count: 0 } ]; const waypoints = [ { alt: 0, text: "いらっしゃい。月までの38万キロだ。エネルギーが切れると落ちるぞ。アイテムは10Lvで倍率ボーナスがかかる。" }, { alt: 10000, text: "高度1万メートル。月の重力は地球の6分の1。ジャンプ力が6倍になると思いきや、空気抵抗がないからもっと高く飛べるらしいぜ。" }, { alt: 100000, text: "高度100キロ、カーマン・ラインだ。ここから先は宇宙。月には大気がないから、空は昼間でも真っ暗なんだ。" }, { alt: 400000, text: "高度400キロ、ISSの軌道だ。月の砂『レゴリス』はガラスみたいに尖ってて、宇宙服の関節をボロボロにしちまうんだ。" }, { alt: 1000000, text: "高度1,000キロ突破。月は毎年3.8センチずつ地球から遠ざかってる。いつかは日食も見られなくなるらしいぜ。" }, { alt: 5000000, text: "月は常に同じ面を地球に向けてる。初めて裏側を見たのは旧ソ連のルナ3号だ。見えないものを見るってのはロマンだろう?" }, { alt: 10000000, text: "高度1万キロ。月の『海』と呼ばれる黒い部分には水はない。昔の火山活動で流れ出た玄武岩の平原なんだ。" }, { alt: 20000000, text: "月の温度差は強烈だ。昼は110度、夜はマイナス170度。大好物のスイカを持っていっても、一瞬で破裂するかカチコチだろうな。" }, { alt: 36000000, text: "高度3万6千キロ。『ムーンクエイク』って言って、月でも地震が起きるんだ。数時間も揺れ続けるらしいぞ。" }, { alt: 60000000, text: "アポロ計画で置いてきた鏡、今でも現役だ。地球からレーザーを当てて、月までの距離を測り続けてるんだ。" }, { alt: 100000000, text: "大台の10万キロだ。月の匂いって知ってるか?宇宙飛行士いわく『使用済みの火薬の匂い』らしい。嗅いでみたいか?" }, { alt: 130000000, text: "月のクレーターには、日本の科学者の名前がついたものがいくつかある。平山とか長岡とかな。" }, { alt: 160000000, text: "満月の光ってのは、太陽の光のたった7パーセントしか反射してないんだ。あんなに明るく見えるのにな。" }, { alt: 192200000, text: "高度19万2200キロ、ちょうど半分だ。月の質量は地球の約81分の1。これでも衛星としては異常なほどデカいんだ。" }, { alt: 230000000, text: "月が誕生した理由は『ジャイアント・インパクト説』が有力だ。火星くらいの星が地球にぶつかってできたって話さ。" }, { alt: 270000000, text: "月の裏側は表と違って、クレーターだらけだ。地球という盾がないから、隕石をモロに食らってきたんだな。" }, { alt: 310000000, text: "月には『永遠の光の峰』って場所がある。ほぼ常に太陽の光が当たってる。基地の候補地として最高らしいぜ。" }, { alt: 340000000, text: "逆に『永久影』には、氷の形で水が大量に眠ってる。未来の月面基地の水源として期待されてるんだ。" }, { alt: 360000000, text: "月面に残されたアポロ飛行士の足跡、風がないから今でもくっきり残ってるはずさ。何百万年も消えないらしいぜ。" }, { alt: 375000000, text: "もうすぐ到着だ。地球と月の間には、太陽系の他の全惑星を並べてすっぽり収められる距離がある。ラストスパートだ!" }, { alt: 384400000, text: "よく登り切ったな!見事な執念だ。月面到着だ!限界を超えたあんたの力に乾杯しよう。実験大成功、おめでとう!" } ]; let currentWaypoint = -1; // --- サウンド&動的BGMシステム --- const AudioContext = window.AudioContext || window.webkitAudioContext; let audioCtx; let isBgmPlaying = false; let nextNoteTime = 0; let currentNote = 0; function playSound(type) { if (!audioCtx) audioCtx = new AudioContext(); if (audioCtx.state === 'suspended') audioCtx.resume(); const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.connect(gain); gain.connect(audioCtx.destination); const now = audioCtx.currentTime; if (type === 'step') { osc.type = 'square'; osc.frequency.setValueAtTime(300 + Math.random() * 100, now); gain.gain.setValueAtTime(0.015, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.05); osc.start(now); osc.stop(now + 0.05); } else if (type === 'upgrade') { osc.type = 'square'; osc.frequency.setValueAtTime(440, now); osc.frequency.setValueAtTime(880, now + 0.1); gain.gain.setValueAtTime(0.05, now); gain.gain.linearRampToValueAtTime(0, now + 0.3); osc.start(now); osc.stop(now + 0.3); } else if (type === 'bonus') { osc.type = 'square'; osc.frequency.setValueAtTime(523, now); osc.frequency.setValueAtTime(1046, now + 0.1); osc.frequency.setValueAtTime(2093, now + 0.2); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.4); osc.start(now); osc.stop(now + 0.4); } else if (type === 'notice') { osc.type = 'sine'; osc.frequency.setValueAtTime(880, now); osc.frequency.setValueAtTime(1318, now + 0.2); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 0.4); osc.start(now); osc.stop(now + 0.4); } else if (type === 'fall') { osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, now); osc.frequency.linearRampToValueAtTime(40, now + 0.5); gain.gain.setValueAtTime(0.05, now); gain.gain.linearRampToValueAtTime(0, now + 0.5); osc.start(now); osc.stop(now + 0.5); } else if (type === 'stamp') { osc.type = 'square'; osc.frequency.setValueAtTime(150, now); osc.frequency.exponentialRampToValueAtTime(20, now + 0.4); gain.gain.setValueAtTime(0.3, now); gain.gain.linearRampToValueAtTime(0, now + 0.4); osc.start(now); osc.stop(now + 0.4); } else if (type === 'clearFanfare') { osc.type = 'square'; osc.frequency.setValueAtTime(523.25, now); osc.frequency.setValueAtTime(659.25, now + 0.2); osc.frequency.setValueAtTime(783.99, now + 0.4); osc.frequency.setValueAtTime(1046.50, now + 0.6); gain.gain.setValueAtTime(0.1, now); gain.gain.linearRampToValueAtTime(0, now + 2.0); osc.start(now); osc.stop(now + 2.0); } } // 状態に応じて変化するBGMスケジューラー function scheduleBGM() { if(state.cleared) return; // クリア時はBGM停止 let tempo = 0.25; let chord = [261.63, 329.63, 392.00, 523.25]; // 通常時: Cメジャー if (state.isFalling) { tempo = 0.4; chord = [261.63, 293.66, 311.13, 349.23]; // 落下時: 不穏なCマイナー } else if (state.currentSps > 1000000) { tempo = 0.1; chord = [523.25, 659.25, 783.99, 1046.50]; // オーラMAX: 高速オクターブ } else if (state.currentSps > 500) { tempo = 0.15; // ブースト時: やや高速 } while(nextNoteTime < audioCtx.currentTime + 0.1) { let osc = audioCtx.createOscillator(); let gain = audioCtx.createGain(); osc.type = 'triangle'; osc.frequency.value = chord[currentNote % chord.length]; gain.gain.setValueAtTime(0.015, nextNoteTime); gain.gain.exponentialRampToValueAtTime(0.001, nextNoteTime + tempo - 0.02); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(nextNoteTime); osc.stop(nextNoteTime + tempo); nextNoteTime += tempo; currentNote++; } requestAnimationFrame(scheduleBGM); } function initBGM() { if (isBgmPlaying) return; if (!audioCtx) audioCtx = new AudioContext(); if (audioCtx.state === 'suspended') audioCtx.resume(); isBgmPlaying = true; nextNoteTime = audioCtx.currentTime + 0.1; scheduleBGM(); document.getElementById('click-text').innerText = "TAP TO CLIMB"; // 最初の雑学を表示 updateUI(); } // --- 計算・UI更新ロジック --- function formatNumber(num) { if (num >= 1000000000) return (num / 1000000000).toFixed(2) + "B"; if (num >= 1000000) return (num / 1000000).toFixed(2) + "M"; if (num >= 1000) return (num / 1000).toFixed(1) + "k"; return Math.floor(num).toLocaleString(); } function getUpgradeCost(baseCost, count) { return Math.min(COST_CAP, Math.floor(baseCost * Math.pow(1.2, count))); } function triggerTriviaNotice(text) { playSound('notice'); document.getElementById('trivia-text').style.opacity = 0; setTimeout(() => { let prefix = state.loops > 0 ? `[周回Lv.${state.loops}] ` : ""; document.getElementById('trivia-text').innerText = prefix + text; document.getElementById('trivia-text').style.opacity = 1; }, 300); let mb = document.getElementById('master-box'); mb.classList.remove('flash-bg'); void mb.offsetWidth; mb.classList.add('flash-bg'); document.getElementById('notice-badge').style.opacity = 1; setTimeout(() => { document.getElementById('notice-badge').style.opacity = 0; }, 3000); } function initClearEffects() { for(let i=0; i<150; i++) { particles.push({ x: centerCanvas.width / 2, y: centerCanvas.height / 2, vx: (Math.random() - 0.5) * 15, vy: (Math.random() - 0.5) * 15 - 5, life: 1.0, color: `hsl(${Math.random()*360}, 100%, 60%)` }); } stampAnim = { active: true, scale: 5, alpha: 0 }; } function restartGame(isStrong) { state.steps = 0; state.altitude = 0; state.energy = 100; state.cleared = false; state.isFalling = false; currentWaypoint = -1; particles = []; stampAnim.active = false; if (isStrong) { state.loops++; } else { state.loops = 0; upgrades.forEach(u => u.count = 0); } recalculatePower(); updateUI(); document.getElementById('clear-overlay').style.display = 'none'; document.getElementById('click-text').innerText = "TAP TO CLIMB"; document.getElementById('click-text').style.color = "rgba(255, 255, 255, 0.1)"; document.getElementById('trivia-text').innerText = "さあ、果てしない旅の再開だ。"; // BGM再開 nextNoteTime = audioCtx.currentTime + 0.1; scheduleBGM(); } function updateUI() { document.getElementById('alt-display').innerText = `高度: ${Math.floor(state.altitude).toLocaleString()} m`; let spsElem = document.getElementById('sps-display'); if (state.isFalling) { spsElem.innerText = `▼ ${formatNumber(Math.abs(state.currentSps))} 段/秒 (落下)`; spsElem.style.color = "#f44336"; document.getElementById('fall-alert').style.display = "block"; } else { spsElem.innerText = `▲ ${formatNumber(state.currentSps)} 段/秒`; spsElem.style.color = "#88ff88"; document.getElementById('fall-alert').style.display = "none"; } let progress = Math.min(100, (state.altitude / TARGET_ALTITUDE) * 100); document.getElementById('progress-bar').style.width = `${progress}%`; let eBar = document.getElementById('energy-bar'); eBar.style.width = `${state.energy}%`; if(state.energy > 60) eBar.style.background = "linear-gradient(90deg, #4caf50, #8bc34a)"; else if(state.energy > 20) eBar.style.background = "linear-gradient(90deg, #ff9800, #ffeb3b)"; else eBar.style.background = "linear-gradient(90deg, #f44336, #ff5722)"; upgrades.forEach(u => { const btn = document.getElementById(`upg-${u.id}`); if (btn) { const cost = getUpgradeCost(u.cost, u.count); btn.querySelector('.upgrade-cost').innerText = `コスト: ${formatNumber(cost)}`; btn.querySelector('.upgrade-count').innerText = `Lv.${u.count}`; btn.disabled = state.steps < cost; let rankMult = Math.pow(2, Math.floor(u.count / 10)); let bonusElem = btn.querySelector('.rank-bonus'); if (rankMult > 1) bonusElem.innerText = `(x${formatNumber(rankMult)})`; else bonusElem.innerText = ""; } }); if (isBgmPlaying) { for (let i = waypoints.length - 1; i >= 0; i--) { if (state.altitude >= waypoints[i].alt) { if (currentWaypoint !== i) { currentWaypoint = i; triggerTriviaNotice(waypoints[i].text); } break; } } } if (state.altitude >= TARGET_ALTITUDE && !state.cleared) { state.cleared = true; playSound('clearFanfare'); document.getElementById('click-text').innerText = ""; initClearEffects(); setTimeout(() => { document.getElementById('clear-overlay').style.display = 'flex'; }, 3000); } } function renderUpgrades() { let div = document.getElementById('upgrades'); div.innerHTML = ''; upgrades.forEach(u => { const btn = document.createElement('button'); btn.className = 'upgrade-btn'; btn.id = `upg-${u.id}`; btn.innerHTML = ` <div class="upg-left"> <span class="upgrade-name">${u.name} <span class="rank-bonus"></span></span> <span class="upgrade-desc">${u.desc}</span> </div> <div class="upg-right"> <span class="upgrade-count">Lv.${u.count}</span> <span class="upgrade-cost">コスト: 0</span> </div> `; btn.onclick = () => buyUpgrade(u); div.appendChild(btn); }); } function buyUpgrade(upgrade) { const cost = getUpgradeCost(upgrade.cost, upgrade.count); if (state.steps >= cost) { state.steps -= cost; state.altitude = state.steps * STEP_HEIGHT; upgrade.count++; if (upgrade.count % 10 === 0) playSound('bonus'); else playSound('upgrade'); recalculatePower(); updateUI(); } } function recalculatePower() { state.clickPower = 1; state.baseStepsPerSecond = 0; upgrades.forEach(u => { let mult = Math.pow(2, Math.floor(u.count / 10)); if (u.type === 'click') state.clickPower += u.power * u.count * mult; if (u.type === 'auto') state.baseStepsPerSecond += u.power * u.count * mult; }); } function doClick(e) { initBGM(); if(state.cleared) return; state.steps += state.clickPower; state.altitude = Math.max(0, state.steps * STEP_HEIGHT); state.energy = Math.min(100, state.energy + 5); state.isFalling = false; playSound('step'); let x = centerCanvas.width / 2; let y = centerCanvas.height / 2; if (e) { const rect = centerCanvas.getBoundingClientRect(); if (e.clientX !== undefined) { x = e.clientX - rect.left; y = e.clientY - rect.top; } else if (e.touches && e.touches.length > 0) { x = e.touches[0].clientX - rect.left; y = e.touches[0].clientY - rect.top; } } floatingTexts.push({ x: x + (Math.random()*20 - 10), y: y, text: "+" + formatNumber(state.clickPower), life: 1.0 }); updateUI(); } let cz = document.getElementById('click-zone'); cz.addEventListener('mousedown', doClick); cz.addEventListener('touchstart', (e) => { e.preventDefault(); doClick(e); }, {passive: false}); // --- 描画系 --- const centerCanvas = document.getElementById('viewCanvas'); const centerCtx = centerCanvas.getContext('2d'); const rightCanvas = document.getElementById('earthCanvas'); const rightCtx = rightCanvas.getContext('2d'); function resizeCanvas() { centerCanvas.width = document.getElementById('center-panel').clientWidth; centerCanvas.height = document.getElementById('center-panel').clientHeight; rightCanvas.width = document.getElementById('right-panel').clientWidth; rightCanvas.height = document.getElementById('right-panel').clientHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); function drawRobot(ctx, time, baseY) { const cx = centerCanvas.width / 2; let yOffset = 0; let sprite = []; const pSize = 6; // オーラ描画(1Mごとに色変化) if (state.currentSps > 500 && !state.isFalling) { let auraIndex = Math.floor(state.currentSps / 1000000); const auraColors = ["#0ff", "#08f", "#80f", "#f0f", "#f00", "#f80", "#ff0", "#fff"]; let aColor = auraColors[Math.min(auraIndex, auraColors.length - 1)]; ctx.save(); ctx.shadowBlur = 15; ctx.shadowColor = aColor; ctx.fillStyle = aColor; ctx.globalAlpha = 0.3 + 0.2 * Math.sin(time * 15); let aw = pSize * 6 + 12; let ah = pSize * 5 + 15 + Math.random() * 8; let ax = cx - aw/2; let ay = baseY - 25 - 40 - ah/2 + 5; ctx.fillRect(ax, ay, aw, ah); ctx.fillRect(ax + 5, ay - 10, aw - 10, ah + 20); ctx.restore(); } if (state.isFalling) { sprite = [ " XX ", " X@@X ", " XXXXX", " X X ", " XX " ]; yOffset = Math.sin(time * 15) * 8; } else if (state.currentSps > 500) { let f = time % 0.2 < 0.1 ? "W" : "w"; sprite = [ " XX ", " X--X ", " XXXXX", " XX ", " "+f+" "+f+" " ]; yOffset = -40 + Math.sin(time * 5) * 5; } else if (state.currentSps > 0 || state.energy > 0) { let walkFrame = Math.floor(time * 6) % 2; sprite = walkFrame === 0 ? [" XX ", " X..X ", " XXXXX", " XX ", " X "] : [" XX ", " X..X ", " XXXXX", " XX ", " X "]; yOffset = Math.sin(time * 12) * 3; } else { sprite = [" XX ", " X..X ", " XXXXX", " XX ", " X X "]; } let startX = cx - (sprite[0].length * pSize) / 2; let startY = baseY - 25 + yOffset; for (let r = 0; r < sprite.length; r++) { for (let c = 0; c < sprite[r].length; c++) { let char = sprite[r][c]; if (char === ' ') continue; if (char === 'X') ctx.fillStyle = "#fff"; if (char === '.' || char === '@') ctx.fillStyle = "#f00"; if (char === '-') ctx.fillStyle = "#0ff"; if (char === 'W' || char === 'w') ctx.fillStyle = "#f80"; ctx.fillRect(startX + c*pSize, startY + r*pSize, pSize, pSize); } } } const footprintSprite = [ " XX XX ", " XXX XXX ", " XXX XXX ", " X X ", " XXXXXXX ", " XXXXXXX ", " XXXXXXX ", " XXXXX ", " XXXXX ", " XXX " ]; function drawCenterPanel(time) { centerCtx.clearRect(0, 0, centerCanvas.width, centerCanvas.height); let skyRatio = Math.max(0, 1 - (state.altitude / 30000)); let spaceRatio = Math.min(1, state.altitude / 100000); let grad = centerCtx.createLinearGradient(0, 0, 0, centerCanvas.height); let fallTint = state.isFalling ? 50 : 0; if (state.altitude < 50000) { grad.addColorStop(0, `rgba(${20 * spaceRatio + fallTint}, ${50 * spaceRatio}, ${150 - 100*spaceRatio}, 1)`); grad.addColorStop(1, `rgba(${100 * skyRatio + fallTint}, ${200 * skyRatio}, 255, 1)`); } else { grad.addColorStop(0, `rgb(${fallTint},0,0)`); grad.addColorStop(1, `rgb(${fallTint},5,16)`); } centerCtx.fillStyle = grad; centerCtx.fillRect(0, 0, centerCanvas.width, centerCanvas.height); const cx = centerCanvas.width / 2; const radius = 120; centerCtx.strokeStyle = "rgba(255, 255, 255, 0.15)"; centerCtx.lineWidth = 2; centerCtx.beginPath(); centerCtx.moveTo(cx - radius, 0); centerCtx.lineTo(cx - radius, centerCanvas.height); centerCtx.moveTo(cx + radius, 0); centerCtx.lineTo(cx + radius, centerCanvas.height); centerCtx.stroke(); const visibleSteps = 40; const visualStepGap = 15; const baseY = centerCanvas.height - 270; let offsetStep = state.steps % 1; if(offsetStep < 0) offsetStep = 1 + offsetStep; let baseAngle = (state.steps * 0.3) % (Math.PI * 2); if (!state.cleared) { for (let i = visibleSteps; i >= 0; i--) { let angle = baseAngle + (i * 0.3); let scale = 1 - (i / visibleSteps) * 0.5; let y = baseY - ((i - offsetStep) * visualStepGap); let x = cx + Math.cos(angle) * radius * scale; let depth = Math.sin(angle); centerCtx.fillStyle = depth > 0 ? "#66ccff" : "#3388cc"; centerCtx.globalAlpha = 1 - (i / visibleSteps); centerCtx.beginPath(); centerCtx.arc(x, y, 15 * scale, 0, Math.PI * 2); centerCtx.fill(); if (depth > -0.5) { centerCtx.strokeStyle = "rgba(255, 255, 255, 0.3)"; centerCtx.beginPath(); centerCtx.moveTo(x, y); centerCtx.lineTo(x, y - 20 * scale); centerCtx.stroke(); } } centerCtx.globalAlpha = 1.0; drawRobot(centerCtx, time, baseY); } for (let i = floatingTexts.length - 1; i >= 0; i--) { let ft = floatingTexts[i]; ft.y -= 2; ft.life -= 0.03; if (ft.life <= 0) floatingTexts.splice(i, 1); else { centerCtx.fillStyle = `rgba(255, 235, 59, ${ft.life})`; centerCtx.font = "bold 24px 'Courier New', monospace"; centerCtx.textAlign = "center"; centerCtx.lineWidth = 3; centerCtx.strokeStyle = `rgba(0, 0, 0, ${ft.life})`; centerCtx.strokeText(ft.text, ft.x, ft.y); centerCtx.fillText(ft.text, ft.x, ft.y); } } if (state.cleared) { for(let i=particles.length-1; i>=0; i--) { let p = particles[i]; p.x += p.vx; p.y += p.vy; p.vy += 0.3; p.life -= 0.01; if (p.life <= 0) particles.splice(i, 1); else { centerCtx.fillStyle = p.color; centerCtx.globalAlpha = p.life; centerCtx.fillRect(p.x, p.y, 6, 6); } } centerCtx.globalAlpha = 1.0; if (stampAnim.active) { if (stampAnim.scale > 1) { stampAnim.scale -= 0.3; if (stampAnim.scale <= 1) { stampAnim.scale = 1; playSound('stamp'); } } stampAnim.alpha = Math.min(1, stampAnim.alpha + 0.1); centerCtx.save(); centerCtx.translate(centerCanvas.width/2, centerCanvas.height/2 - 50); centerCtx.scale(stampAnim.scale, stampAnim.scale); centerCtx.globalAlpha = stampAnim.alpha; centerCtx.shadowColor = "#ffeb3b"; centerCtx.shadowBlur = 20; centerCtx.fillStyle = "#ffeb3b"; const pSize = 10; let sx = -(footprintSprite[0].length * pSize) / 2; let sy = -(footprintSprite.length * pSize) / 2; for (let r = 0; r < footprintSprite.length; r++) { for (let c = 0; c < footprintSprite[r].length; c++) { if (footprintSprite[r][c] === 'X') centerCtx.fillRect(sx + c*pSize, sy + r*pSize, pSize, pSize); } } centerCtx.restore(); } } } function drawRightPanel() { rightCtx.clearRect(0, 0, rightCanvas.width, rightCanvas.height); let alt = state.altitude; let spaceRatio = Math.min(1, alt / 100000); rightCtx.fillStyle = `rgb(0, 0, ${30 * (1 - spaceRatio)})`; rightCtx.fillRect(0, 0, rightCanvas.width, rightCanvas.height); if (alt > 10000) { rightCtx.fillStyle = "#fff"; for(let i=0; i<40; i++) { let x = (Math.sin(i * 123) * rightCanvas.width) % rightCanvas.width; if(x < 0) x += rightCanvas.width; let y = (Math.cos(i * 321) * rightCanvas.height + alt * 0.0005) % rightCanvas.height; if(y < 0) y += rightCanvas.height; rightCtx.globalAlpha = Math.min(1, (alt - 10000) / 40000); rightCtx.fillRect(x, y, 1.5, 1.5); } rightCtx.globalAlpha = 1.0; } let progress = Math.min(1, Math.max(0, alt / TARGET_ALTITUDE)); let moonGrowFactor = Math.pow(progress, 5); let maxMoonRadius = rightCanvas.width * 1.5; let minMoonRadius = 1; let currentMoonRadius = minMoonRadius + (maxMoonRadius - minMoonRadius) * moonGrowFactor; let moonYPos = -50 + (rightCanvas.height * 0.2 - (-50)) * progress; if (currentMoonRadius > 0.5) { rightCtx.beginPath(); rightCtx.arc(rightCanvas.width/2, moonYPos, currentMoonRadius, 0, Math.PI*2); let moonGrad = rightCtx.createRadialGradient( rightCanvas.width/2 - currentMoonRadius*0.2, moonYPos - currentMoonRadius*0.2, 0, rightCanvas.width/2, moonYPos, currentMoonRadius); moonGrad.addColorStop(0, "#f0f0f0"); moonGrad.addColorStop(1, "#555555"); rightCtx.fillStyle = moonGrad; rightCtx.fill(); if (currentMoonRadius > 10) { rightCtx.fillStyle = "rgba(0, 0, 0, 0.15)"; const craters = [ {x: 0.15, y: -0.2, r: 0.18}, {x: -0.25, y: 0.15, r: 0.12}, {x: 0.3, y: 0.25, r: 0.1}, {x: -0.1, y: -0.35, r: 0.15} ]; craters.forEach(c => { rightCtx.beginPath(); rightCtx.arc(rightCanvas.width/2 + c.x * currentMoonRadius, moonYPos + c.y * currentMoonRadius, c.r * currentMoonRadius, 0, Math.PI*2); rightCtx.fill(); }); } } let baseEarthRadius = rightCanvas.width * 1.5; let shrinkFactor = Math.pow(1 - progress, 4); let currentEarthRadius = 5 + (baseEarthRadius - 5) * shrinkFactor; let earthYPos = rightCanvas.height - (rightCanvas.height * 0.1) + (progress * rightCanvas.height * 0.7); rightCtx.beginPath(); rightCtx.arc(rightCanvas.width/2, earthYPos, currentEarthRadius, 0, Math.PI*2); let earthGrad = rightCtx.createRadialGradient(rightCanvas.width/2, earthYPos, 0, rightCanvas.width/2, earthYPos, currentEarthRadius); earthGrad.addColorStop(0, "#2266cc"); earthGrad.addColorStop(1, "#001144"); rightCtx.fillStyle = earthGrad; rightCtx.fill(); } let lastTime = performance.now(); let frameCount = 0; function gameLoop(currentTime) { const deltaTime = (currentTime - lastTime) / 1000; lastTime = currentTime; let animTime = currentTime / 1000; if (!state.cleared && isBgmPlaying) { state.energy = Math.max(0, state.energy - (12 * deltaTime)); if (state.energy === 0) { if (!state.isFalling) playSound('fall'); state.isFalling = true; let fallSpeed = -50 - (state.altitude * 0.0001); state.currentSps = fallSpeed; state.steps += state.currentSps * deltaTime; if (state.steps < 0) state.steps = 0; state.altitude = state.steps * STEP_HEIGHT; } else { state.isFalling = false; let efficiency = state.energy / 100; state.currentSps = state.baseStepsPerSecond * efficiency; if (state.currentSps > 0) { state.steps += state.currentSps * deltaTime; state.altitude = state.steps * STEP_HEIGHT; } } } drawCenterPanel(animTime); drawRightPanel(); frameCount++; if(frameCount % 4 === 0) updateUI(); requestAnimationFrame(gameLoop); } renderUpgrades(); updateUI(); requestAnimationFrame(gameLoop);</script></body></html>
■ 今日の学び
前回の「月相ルーレット」に続き、
今回も“月”をテーマにしたゲーム実験になりました。
実はこのゲーム、Xで交流させていただいている
ながひらさん の素敵なロボットゲームに触れたことがきっかけなんです。
「私もロボットを宇宙に飛ばしてみたい!」という気持ちがふっと芽生えて、
気づけば制作に取りかかっていました。ᕦ( 🤖_🤖)ᕤ🚀
地球から月までの距離は約38万4400キロ。
とても遠い道のりに感じますが、光のスピードで進むと、
なんと 1.28秒 で到着してしまうそうです。
最初は一歩ずつ、ゆっくりと階段を上っていくロボットくんですが……
今回実装した「強くてニューゲーム」で成長を重ねていけば、
きっとこの“光速の1.28秒”に近いスピード感を
体験できるようになるはずです。( ✧Д✧) カッ!!
遠くにあるはずの月が、連打とやりこみを通じて、
少しだけ身近に感じられるゲームになったのではないかなと思います。
皆さんのロボットくんは、一体どれくらいの速さで月へ駆け上がるでしょうか。
ぜひ、オーラを極めて“光の速さ”に挑戦してみてくださいね!
[▓▓] ε:) ピューン 🌔
■ 次回は、イノシシが全力で突進する超インパクト実験ゲームです。
前回のやさしい雰囲気から一転して、
次回は「画面を突き破るような勢いと迫力」をテーマにした、
衝撃たっぷりのアクションゲームを作ってみました。
巨大なマンガ文字が画面外へ飛び出したり、
衝突の瞬間に画面が激しく揺れたり、
障害物を粉々に吹き飛ばす“無敵フィーバーモード”が発動したり……。
レトロな 8bit サウンドに合わせて突っ走る、
疾走感と爽快感たっぷりのゲームになっています。
次回もぜひ楽しみにしていてくださいね。(*^v^)ノシ
▶ 次の実験はこちら
究極の猪突猛進を目指す!
「猪突猛進ダッシュ!」
✨今日も最後まで読んでくださって、ありがとうございます。
みーの実験室は、みなさんの応援で元気に動いています。(*^v^*)






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