実験#68:お悩み解決カリスマ店主(木漏れ日の幸運雑貨店)

■68個目 「木漏れ日の幸運雑貨店」

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

今回は、ちょっとほっこりする雑貨屋さん運営ゲームを作ってみました。

その名も…… 「木漏れ日の幸運雑貨店」 です!

キャッチコピーは、 「公園で出会う、あなただけのラッキー雑貨」。

緑いっぱいの公園で、木漏れ日が差し込むあたたかい午後。
そこには、小さな手作りの屋台があります。

そこは、誰かの心をあたたかくしたり、背中をそっと押してくれたりする、
「幸運」を運ぶ雑貨が集まる場所。

🌿 こだわりのラッキー雑貨たち

今回こだわったのは、この「ラッキー雑貨」たちです。

ひとつひとつが店主の手作りで、特別な想いが込められています。

屋台には、こんな素敵な雑貨が並んでいます。

  1. パワーストーンのブレスレット  
    (ローズクォーツ/アメジスト/クリスタル)
  2. 光が透ける シーグラス小瓶
  3. 海の恵み シーグラスと貝殻のリース

どの雑貨も、見ているだけで幸せな気持ちになれそうですね。

🌼 訪れる人々との出会い

屋台には、親子、友人、カップル…… さまざまな人が訪れます。

店主であるあなたと、雑貨を求める人々との あたたかい出会いが待っています。

あなたの作った雑貨が、誰かの毎日に小さな「幸運」を運んでくれる。

そんな優しい世界を体験してみてください。

どんな出会いが待っているか、お楽しみに!

 

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

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

みー:(^v^) 今日もゲームを作ります。html形式で動かせるゲーム。

今回は、晴れてさわやかな緑あふれる公園がイベント会場。

そこで、手作りの「幸運雑貨」を販売するお店を運営するゲームを作りたいんです。

扱う雑貨は、ローズクォーツやアメジスト、
クリスタルなどのパワーストーンを使ったブレスレット。

さらに、シーグラスや貝殻を使った小物など、
見ているだけで心がほぐれるようなアイテムたち。

お店のレイアウトは自分で決められて、最大5つまで商品を並べられます。
お客さんは次々と訪れて、置いた商品の中からランダムでひとつ手に取ります。

そこからが店主の腕の見せどころ。
商品説明は 3パターン、お客さんの雰囲気も 3タイプ 用意してあって、
その人に合った説明を選ぶと購入してくれます。

買ってもらえると、商品はふわっとフェードアウトして、
お客さんは笑顔で帰っていきます。

買ってもらえなかった場合は商品はそのまま残ります。

ゲームの流れとしては、

  • 商品レイアウト(5品)
  • お客さん来店
  • 商品を手に取る
  • 説明を選ぶ
  • 購入 or 見送り
  • レイアウト変更
  • 次のお客さんへ

というサイクルを10人分繰り返し、販売数がスコアになります。

また、お客さんの雰囲気は絵ではなく、
店主視点での「観察した印象」を文字で描写する形にしたいです。

商品は絵文字や図形の組み合わせで、
視覚的に魅力を出したいと思っています。

レイアウトのセンスによって、
来るお客さんの傾向が変わるような仕掛けも入れたいです。

AI: 今回は、初夏のさわやかな風が吹き抜けるような、
緑豊かな公園での雑貨販売ゲームですね。

店主としての「観察眼」を活かすシステム、
そして金額ではなく“想いのマッチング”に焦点を当てる構成、
とても温かくて素敵です。

みー:(^_^) レイアウトのテーブルを広くして、
2段置けるようにしたいです。
置ける数は変わらず5つですが、自由度を広げたいです。
あと商品を置く白枠は、少し控えめにできますか?

AI: なるほど! 前列・後列の2段構成になると、
「自分のお店を作っている感」が一気に増しますね。

みー:(*^v^*)背景を、木漏れ日があふれてそよ風を感じるような、
心地よい空間にしたいです。

AI: はい、もちろんです!
みーさんの雑貨屋さんがさらに心地よくなるよう、
背景に“光と風”の演出を加えましょう。

  • 柔らかい空と草の色
  • 木漏れ日の光の粒
  • ゆっくり揺れるそよ風アニメーション
  • 光が透けるUIのぼかし効果

これらを組み合わせて、 「午後の公園にいるような心地よさ」を表現します。

では、以下のコードを上書き保存して、
新しくなった公園の雑貨屋さんを訪れてみてください

■ 実際の動作

こちらが、仕上がったスタート画面です。
テーブルの上へ、商品を並べていきましょう。
(画像:スタート画面)


商品を並べたら、早速お店を開きます。
お客さんが立ち寄ってくださいます。(様子をよく観察しましょう)

(画像:ゲーム画面)

お客さんの想いにぴったり当てはまると、
とても喜んで購入してくださいます。
購入していただいた商品は棚からふわっと消えます。

(画像:ゲーム画面)


お客さんのニーズに合わない場合は、購入には至りません。
10人のお客さんへの販売が終わるとゲーム終了です。

(画像:ゲーム画面)


売り上げた数が、そのままスコアになります。

(画像:営業終了)


■ コードのポイント

今回の記事では、AI と相談しながら作った
「木漏れ日の幸運雑貨店」 の HTML コードを公開します。

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

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

▼ 遊ぶまでの手順

①PC の「メモ帳」(※Windowsの場合)や  
 Mac の「テキストエディット」など、テキストエディタを開きます。

②下のコードをコピーして貼り付けます。

③「zakkaya .html」など、好きな名前で保存します。
 (※拡張子は .html にしてください)

④保存したファイルをブラウザで開けば、すぐに遊べます。

 

■ 遊び方・操作説明

このゲームは、すべての操作をマウスだけで簡単に楽しめます。(^_^) 難しい操作は一切ありません。

あなたの観察眼とレイアウトのセンスで、 公園に訪れる人々に小さな幸運を届けてみましょう♪

🌿 ステップ1:お店のディスプレイを決めよう!

まずは、あなたの屋台(木のテーブル)に商品を並べましょう。
商品は 前列と後列あわせて10箇所 に置けますが、
実際に並べられるのは 5つまで です。(=v=*)

  • 空いている場所(薄い白枠)をクリックすると在庫リストが開きます。
  • 好きな雑貨(パワーストーンやシーグラスなど)を選んで並べてね。
  • 配置を変えたいときは、一度クリックして取り除いてから新しい場所を選びます。

🌿 ステップ2:お店を開けて、お客様を観察!

商品を5つ並べたら、いよいよ営業開始!
「お店を開ける」ボタンをクリック。(^v^*)

  • お客様がやってきます。 まずは画面上部に出る 「お客様の様子(思考テキスト)」 をよく観察してね。
  • 「ため息をついている」「背筋をピンと伸ばしている」 「誰かを想って微笑んでいる」…… 今のお客様が何を求めているのか、雰囲気を感じ取ることがポイントです。(^^*)

🌿 ステップ3:素敵なポイントとラッキー効果を説明!

お客様がランダムで商品をひとつ手に取ります。
ここからが店主であるあなたの出番!(^^)

  • 商品にぴったりの「商品説明」を3つの選択肢から選んでね。
  • 説明には、その雑貨の素敵なポイントと、
    癒やし・活力・愛情などの ラッキー効果 が書かれています。
  • お客様の雰囲気に合った説明を選ぶと、
    表情がパッと明るくなり、購入してくれます。(^v^)
  • 買ってもらった商品は、ふわりと消えていきます。

🌿 ステップ4:次のお客様へ、そして営業終了

お客様が帰ったら、「品出し・配置を整える」ボタンをクリック。(^.^)

  • 空いたスロットに新しい商品を補充したり、
    レイアウトを変えたりして次のお客様を迎えましょう。
  • 並べた商品の属性(癒やし・活力・愛情)のバランスによって、
    来るお客様の傾向が少し変わることもあります。(=v=*)
  • 10人のお客様が来たら本日の営業終了!
    何人のお客様にラッキーを届けられたか、スコアが表示されます。

さあ、あなたの観察眼とレイアウトセンスで、
たくさんのお客様を笑顔にしてあげてくださいね。(^^*)

“コードは長いので折りたたんでいます。必要な方は開いてご覧ください。”

▼ここからコード▼(クリックで開く)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>マッスル・シンギュラリティ</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #1a1a2e; color: #ecf0f1;
margin: 0; padding: 0;
height: 100vh; width: 100vw;
overflow: hidden;
user-select: none;
display: flex;
justify-content: center;
transition: background-color 2s ease;
}
/* --- 神々しい演出用 --- */
#god-rays {
position: fixed; top: -50%; left: -50%; width: 200%; height: 200%;
background: repeating-conic-gradient(from 0deg, transparent 0deg, rgba(241, 196, 15, 0.1) 15deg, transparent 30deg);
z-index: -1; opacity: 0; pointer-events: none;
transition: opacity 3s ease;
}
body.divine-mode {
background-color: #2c2510;
box-shadow: inset 0 0 100px rgba(241, 196, 15, 0.5);
}
body.divine-mode #god-rays {
opacity: 1;
animation: rotateGodRays 40s linear infinite;
}
@keyframes rotateGodRays { 100% { transform: rotate(360deg); } }
.holy-particle {
position: fixed; bottom: -20px;
background: #f1c40f; border-radius: 50%;
pointer-events: none; z-index: 0;
box-shadow: 0 0 10px #fff, 0 0 20px #f1c40f;
animation: floatUp linear forwards;
}
@keyframes floatUp {
0% { transform: translateY(0) scale(0); opacity: 0; }
20% { transform: translateY(-20vh) scale(1); opacity: 1; }
80% { transform: translateY(-80vh) scale(1); opacity: 0.8; }
100% { transform: translateY(-100vh) scale(0); opacity: 0; }
}
body.divine-mode .play-area {
border-color: #f1c40f;
box-shadow: 0 0 40px rgba(241, 196, 15, 0.8);
background: rgba(26, 30, 62, 0.8);
}
body.divine-mode h1 {
color: #fff; text-shadow: 0 0 10px #f1c40f, 0 0 20px #e67e22, 0 0 30px #f1c40f;
}
/* --- 基本レイアウト --- */
.game-wrapper {
width: 100%; max-width: 800px; height: 100%;
display: flex; flex-direction: column; padding: 10px; box-sizing: border-box;
z-index: 10;
}
.header { text-align: center; flex-shrink: 0; }
h1 {
color: #e94560; text-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
font-size: 1.8rem; margin: 5px 0 0 0; transition: color 1s, text-shadow 1s;
}
.subtitle {
color: #f1c40f; font-size: 0.9rem; font-weight: bold;
margin-bottom: 5px; letter-spacing: 3px;
}
.play-area {
flex: 1; background: #16213e; border-radius: 15px;
display: flex; flex-direction: column; justify-content: center; align-items: center;
box-shadow: 0 5px 15px rgba(0,0,0,0.5); border: 1px solid #0f3460;
margin-bottom: 10px; position: relative; overflow: hidden;
transition: all 2s ease;
}
/* --- 詳細データ表示用ボード --- */
.stats-board {
background: rgba(15, 52, 96, 0.6);
border: 1px solid #1a1a2e;
border-radius: 12px;
padding: 10px 20px;
width: 85%; max-width: 380px;
z-index: 20; margin-top: 10px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.3);
}
.stat-title { font-size: 0.9rem; color: #a2a2bd; text-align: center; margin-bottom: 2px; }
.mp-text { color: #f1c40f; font-weight: bold; font-size: 2.5rem; text-shadow: 0 0 10px rgba(241, 196, 15, 0.3); display: block; line-height: 1.1; text-align: center; }
.stat-details {
display: flex; justify-content: space-between;
border-top: 1px dashed #43aa8b;
margin-top: 8px; padding-top: 8px;
font-size: 0.8rem; color: #ecf0f1;
}
.stat-val { color: #f1c40f; font-weight: bold; font-size: 0.9rem; }
#muscle-stage {
position: relative; width: 140px; height: 140px; margin: 10px 0;
display: flex; justify-content: center; align-items: center; flex: 1;
}
.sunburst {
position: absolute; top: 50%; left: 50%; width: 400px; height: 400px; transform: translate(-50%, -50%);
background: repeating-conic-gradient(rgba(233, 69, 96, 0.2) 0 15deg, transparent 15deg 30deg);
border-radius: 50%; pointer-events: none; opacity: 0; z-index: 1;
animation: rotateBg 10s linear infinite;
transition: background 2s ease;
}
/* 後光も黄金に輝くように変更 */
body.divine-mode .sunburst {
background: repeating-conic-gradient(rgba(241, 196, 15, 0.5) 0 15deg, transparent 15deg 30deg);
animation: rotateBg 5s linear infinite;
filter: drop-shadow(0 0 10px rgba(241, 196, 15, 0.8));
}
@keyframes rotateBg { from { transform: translate(-50%, -50%) rotate(0deg); } to { transform: translate(-50%, -50%) rotate(360deg); } }
#muscle-display { font-size: 5.5rem; position: absolute; z-index: 10; transition: transform 0.08s ease-out; }
#muscle-name {
font-size: 1.3rem; font-weight: bold; color: #43aa8b;
text-shadow: 0 2px 4px rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.5s; z-index: 20; margin-bottom: 10px;
}
.btn-train {
font-size: 1.6rem; padding: 12px 40px; background-color: #e94560; color: #fff;
border: none; border-radius: 50px; cursor: pointer; box-shadow: 0 6px #8f2135;
transition: background-color 0.2s, transform 0.1s; font-weight: bold; letter-spacing: 2px; z-index: 20; margin-bottom: 15px;
}
.btn-train:active { background-color: #8f2135; box-shadow: 0 2px #8f2135; transform: translateY(4px); }
.shop-container { flex-shrink: 0; }
.shop-title { color: #e94560; font-size: 1.1rem; margin-bottom: 5px; border-bottom: 2px solid #e94560; display: inline-block; padding-bottom: 2px; }
.shop-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }
@media (min-width: 600px) { .shop-grid { grid-template-columns: repeat(4, 1fr); } }
.item {
background: #0f3460; padding: 8px; border-radius: 8px; cursor: pointer;
box-shadow: 0 3px 6px rgba(0,0,0,0.4); border: 2px solid transparent;
transition: transform 0.1s, border-color 0.2s, background-color 0.2s; text-align: center;
}
.item:hover { border-color: #e94560; background: #16213e; transform: translateY(-2px); }
.item:active { transform: translateY(2px); }
.item h3 { margin: 0 0 3px 0; color: #43aa8b; font-size: 0.9rem; }
.item.rare h3 { color: #f1c40f; }
.item p { margin: 2px 0; font-size: 0.75rem; color: #ecf0f1; }
.cost-box { margin-top: 4px; background: rgba(0,0,0,0.3); padding: 3px; border-radius: 4px; }
.cost-text { color: #f1c40f; font-weight: bold; font-size: 0.95rem; }
.disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(80%); }
.disabled:hover { border-color: transparent; transform: none; background: #0f3460; }
@keyframes shake {
0% { transform: translate(1px, 1px) rotate(0deg); }
20% { transform: translate(-2px, 0px) rotate(1deg); }
40% { transform: translate(1px, -1px) rotate(1deg); }
60% { transform: translate(-2px, 1px) rotate(0deg); }
80% { transform: translate(-1px, -1px) rotate(1deg); }
100% { transform: translate(1px, -1px) rotate(0deg); }
}
.shake-screen { animation: shake 0.4s; }
#flash-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: white; z-index: 9999; opacity: 0; pointer-events: none; transition: opacity 0.5s ease-out; }
#start-screen { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(26, 26, 46, 0.98); z-index: 10000; display: flex; flex-direction: column; justify-content: center; align-items: center; }
#start-btn { font-size: 1.5rem; padding: 15px 30px; background: #e94560; color: #fff; border: none; border-radius: 50px; cursor: pointer; font-weight: bold; box-shadow: 0 6px #8f2135; }
.smoke-effect { position: absolute; top: 50%; left: 50%; width: 100px; height: 100px; background: radial-gradient(circle, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 70%); border-radius: 50%; pointer-events: none; z-index: 15; animation: smokeAnim 0.7s ease-out forwards; }
@keyframes smokeAnim { 0% { transform: translate(-50%, -50%) scale(0.1); opacity: 1; filter: blur(0px); } 100% { transform: translate(-50%, -50%) scale(6); opacity: 0; filter: blur(15px); } }
.particle { position: absolute; top: 50%; left: 50%; pointer-events: none; font-size: 1.5rem; z-index: 20; animation: particleAnim 1s cubic-bezier(0.1, 0.8, 0.3, 1) forwards; }
@keyframes particleAnim { 0% { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; } 100% { transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(0) rotate(360deg); opacity: 0; } }
#coach-shout { position: absolute; top: 25%; left: 50%; transform: translateX(-50%); font-size: 2.2rem; color: #f1c40f; font-weight: bold; white-space: nowrap; text-shadow: 3px 3px 0 #8f2135, -2px -2px 0 #8f2135, 2px -2px 0 #8f2135, -2px 2px 0 #8f2135; pointer-events: none; opacity: 0; transition: opacity 0.2s, transform 0.2s; z-index: 10000; }
#milestone-alert { position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, #e94560, #8f2135); color: #fff; padding: 10px 30px; border-radius: 50px; font-size: 1.2rem; font-weight: bold; box-shadow: 0 10px 20px rgba(0,0,0,0.8); pointer-events: none; opacity: 0; transition: opacity 0.3s; z-index: 10000; border: 2px solid #f1c40f; white-space: nowrap; }
</style>
</head>
<body>
<div id="god-rays"></div>
<div id="flash-overlay"></div>
<div id="start-screen">
<h1 style="font-size: 2.2rem; margin-bottom: 10px; text-align: center;">マッスル・シンギュラリティ</h1>
<p style="font-size: 0.9rem; margin-bottom: 30px;">※激しい音と光の演出があります</p>
<button id="start-btn" onclick="startGame()">▶ SYNC START</button>
</div>
<div id="milestone-alert">進化完了!</div>
<div id="coach-shout">まだまだぁ!</div>
<div class="game-wrapper">
<div class="header">
<h1 id="main-title">MUSCLE SINGULARITY</h1>
<div class="subtitle">筋肉の特異点</div>
</div>
<div class="play-area">
<!-- 詳細データボード -->
<div class="stats-board">
<div class="stat-title">CURRENT MUSCLE ENERGY</div>
<span id="mp" class="mp-text">0</span>
<div class="stat-details">
<span>累計: <span id="disp-total" class="stat-val">0</span></span>
<span>タップ: <span id="disp-click" class="stat-val">1</span> /回</span>
<span>自動: <span id="disp-auto" class="stat-val">0</span> /秒</span>
</div>
</div>
<div id="muscle-stage">
<div id="sunburst" class="sunburst"></div>
<div id="muscle-display">💪</div>
</div>
<div id="muscle-name">ノーマルマッスル</div>
<button class="btn-train" onclick="train()">PUMP UP !!</button>
</div>
<div class="shop-container">
<div class="shop-title">EVOLUTION SHOP</div>
<div class="shop-grid">
<div id="item-dumbbell" class="item" onclick="buyUpgrade('dumbbell')">
<h3>🏋️‍♂️ ヘビーダンベル</h3>
<p>タップ力 <b>+1</b></p>
<div class="cost-box">COST: <span id="cost-dumbbell" class="cost-text">10</span></div>
</div>
<div id="item-protein" class="item" onclick="buyUpgrade('protein')">
<h3>🥤 プロテイン</h3>
<p>自動MP <b>+1/秒</b></p>
<div class="cost-box">COST: <span id="cost-protein" class="cost-text">50</span></div>
</div>
<div id="item-trainer" class="item" onclick="buyUpgrade('trainer')">
<h3>🗣️ 鬼コーチ</h3>
<p>自動MP <b>+10/秒</b></p>
<div class="cost-box">COST: <span id="cost-trainer" class="cost-text">500</span></div>
</div>
<div id="item-reactor" class="item rare" onclick="buyUpgrade('reactor')">
<h3>🔋 マッスルリアクター</h3>
<p>自動MP <b>+100/秒</b></p>
<div class="cost-box">COST: <span id="cost-reactor" class="cost-text">5000</span></div>
</div>
</div>
</div>
</div>
<script>
let audioCtx; let bgmStep = 0;
let divineModeActive = false;
function initAudio() { if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); startBGM(); } }
function playSE(type) {
if (!audioCtx || audioCtx.state === 'suspended') return;
const t = audioCtx.currentTime; const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.connect(gain); gain.connect(audioCtx.destination);
if (type === 'click') {
osc.type = 'triangle'; osc.frequency.setValueAtTime(300, t); osc.frequency.exponentialRampToValueAtTime(50, t + 0.1);
gain.gain.setValueAtTime(0.3, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1); osc.start(t); osc.stop(t + 0.1);
}
else if (type === 'buy') {
osc.type = 'square'; osc.frequency.setValueAtTime(800, t); osc.frequency.setValueAtTime(1200, t + 0.1);
gain.gain.setValueAtTime(0.1, t); gain.gain.linearRampToValueAtTime(0, t + 0.2); osc.start(t); osc.stop(t + 0.2);
}
else if (type === 'coach') {
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(1500, t); osc.frequency.linearRampToValueAtTime(1000, t + 0.2);
gain.gain.setValueAtTime(0.15, t); gain.gain.linearRampToValueAtTime(0, t + 0.2); osc.start(t); osc.stop(t + 0.2);
}
else if (type === 'evolve') {
osc.type = 'square'; osc.frequency.setValueAtTime(100, t); osc.frequency.exponentialRampToValueAtTime(10, t + 1.5);
gain.gain.setValueAtTime(0.8, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 1.5); osc.start(t); osc.stop(t + 1.5);
const osc2 = audioCtx.createOscillator(); const gain2 = audioCtx.createGain(); osc2.connect(gain2); gain2.connect(audioCtx.destination);
osc2.type = 'sine'; osc2.frequency.setValueAtTime(2000, t); osc2.frequency.linearRampToValueAtTime(3000, t + 0.5);
gain2.gain.setValueAtTime(0.3, t); gain2.gain.linearRampToValueAtTime(0, t + 0.5); osc2.start(t); osc2.stop(t + 0.5);
}
else if (type === 'divine') {
const freqs = [523.25, 659.25, 783.99, 1046.50];
freqs.forEach(f => {
const o = audioCtx.createOscillator(); const g = audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
o.type = 'sine'; o.frequency.setValueAtTime(f, t);
g.gain.setValueAtTime(0, t); g.gain.linearRampToValueAtTime(0.2, t + 1); g.gain.exponentialRampToValueAtTime(0.01, t + 5);
o.start(t); o.stop(t + 5);
});
}
}
function startBGM() {
setInterval(() => {
if (!audioCtx || audioCtx.state === 'suspended') return;
const t = audioCtx.currentTime;
if (!divineModeActive && bgmStep % 2 === 0) {
const kOsc = audioCtx.createOscillator(); const kGain = audioCtx.createGain();
kOsc.connect(kGain); kGain.connect(audioCtx.destination);
kOsc.frequency.setValueAtTime(150, t); kOsc.frequency.exponentialRampToValueAtTime(0.01, t + 0.3);
kGain.gain.setValueAtTime(0.4, t); kGain.gain.exponentialRampToValueAtTime(0.01, t + 0.3); kOsc.start(t); kOsc.stop(t + 0.3);
}
const bOsc = audioCtx.createOscillator(); const bGain = audioCtx.createGain();
bOsc.connect(bGain); bGain.connect(audioCtx.destination); bOsc.type = divineModeActive ? 'sine' : 'sawtooth';
const notes = [65.41, 65.41, 73.42, 65.41, 58.27, 65.41, 82.41, 73.42];
const freq = divineModeActive ? notes[bgmStep % 8] * 4 : notes[bgmStep % 8];
bOsc.frequency.setValueAtTime(freq, t);
bGain.gain.setValueAtTime(0.1, t); bGain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
bOsc.start(t); bOsc.stop(t + 0.2); bgmStep++;
}, 250);
}
function startGame() {
document.getElementById('start-screen').style.display = 'none';
initAudio(); if(audioCtx.state === 'suspended') audioCtx.resume();
setTimeout(randomCoachShout, 3000);
document.getElementById('muscle-name').style.opacity = 1;
}
let mp = 0; let totalMp = 0; let clickPower = 1; let autoPower = 0;
let currentBaseScale = 1.0; let muscleAnimTimeout;
let costs = { dumbbell: 10, protein: 50, trainer: 500, reactor: 5000 };
const randomEvolutions = {
typeA: [{icon: "👽", name: "エイリアン"}, {icon: "👻", name: "ファントム"}, {icon: "🤖", name: "メカニカル"}],
typeB: [{icon: "🐲", name: "ドラゴン"}, {icon: "🦄", name: "ユニコーン"}, {icon: "🧟", name: "アンデッド"}],
typeC: [{icon: "🌋", name: "ヴォルカニック"}, {icon: "🌪️", name: "テンペスト"}, {icon: "🌞", name: "ソーラー"}]
};
let milestones = [
{ threshold: 100, reward: 50, message: "覚醒:マッスルゴリラ!", isRandom: false, icon: "🦍", name: "マッスルゴリラ" },
{ threshold: 1000, reward: 500, message: "改造:サイボーグ化!", isRandom: false, icon: "🦾", name: "マッスルサイボーグ" },
{ threshold: 5000, reward: 2500, message: "超越:スーパーマッスル!", isRandom: false, icon: "🦸‍♂️", name: "スーパーマッスル" },
{ threshold: 20000, reward: 10000, message: "到達:伝説の筋肉像!", isRandom: false, icon: "🗽", name: "伝説の筋肉像" },
{ threshold: 50000, reward: 25000, message: "特異点:第一の混沌へ進化!", isRandom: true },
{ threshold: 150000, reward: 75000, message: "特異点:さらなる混沌へ!", isRandom: true },
{ threshold: 500000, reward: 250000, message: "終焉:神の領域へ到達…!", isRandom: true, isFinal: true }
];
function updateUI() {
document.getElementById('mp').innerText = Math.floor(mp);
document.getElementById('disp-total').innerText = Math.floor(totalMp);
document.getElementById('disp-click').innerText = clickPower;
document.getElementById('disp-auto').innerText = autoPower;
document.getElementById('cost-dumbbell').innerText = costs.dumbbell;
document.getElementById('cost-protein').innerText = costs.protein;
document.getElementById('cost-trainer').innerText = costs.trainer;
document.getElementById('cost-reactor').innerText = costs.reactor;
['dumbbell', 'protein', 'trainer', 'reactor'].forEach(type => {
const el = document.getElementById('item-' + type);
mp >= costs[type] ? el.classList.remove('disabled') : el.classList.add('disabled');
});
}
function getRandomEvolution() {
const parts = [
randomEvolutions.typeA[Math.floor(Math.random() * randomEvolutions.typeA.length)],
randomEvolutions.typeB[Math.floor(Math.random() * randomEvolutions.typeB.length)],
randomEvolutions.typeC[Math.floor(Math.random() * randomEvolutions.typeC.length)]
];
return parts[Math.floor(Math.random() * parts.length)];
}
function triggerDivineAwakening() {
divineModeActive = true;
document.body.classList.add('divine-mode');
document.getElementById('main-title').innerText = "DIVINE MUSCLE";
playSE('divine');
setInterval(() => {
const p = document.createElement('div');
p.className = 'holy-particle';
const size = 5 + Math.random() * 15;
p.style.width = size + 'px'; p.style.height = size + 'px';
p.style.left = Math.random() * 100 + 'vw';
p.style.animationDuration = (3 + Math.random() * 4) + 's';
document.body.appendChild(p);
setTimeout(() => p.remove(), 7000);
}, 300);
}
function triggerEvolutionEffect(newIcon, newName) {
playSE('evolve');
document.body.classList.add('shake-screen'); setTimeout(() => document.body.classList.remove('shake-screen'), 400);
const flash = document.getElementById('flash-overlay'); flash.style.opacity = 1; setTimeout(() => flash.style.opacity = 0, 100);
const container = document.getElementById('muscle-stage');
const sunburst = document.getElementById('sunburst'); sunburst.style.opacity = 1;
const smoke = document.createElement('div'); smoke.className = 'smoke-effect'; container.appendChild(smoke);
const colors = ['#f1c40f', '#e94560', '#43aa8b', '#ffffff'];
for(let i = 0; i < 30; i++) {
const p = document.createElement('div'); p.className = 'particle';
p.style.color = colors[Math.floor(Math.random() * colors.length)];
p.innerText = ["✨", "💥", "🔥", "⚡"][Math.floor(Math.random() * 4)];
const angle = Math.random() * Math.PI * 2; const distance = 60 + Math.random() * 150;
p.style.setProperty('--tx', `${Math.cos(angle) * distance}px`);
p.style.setProperty('--ty', `${Math.sin(angle) * distance}px`);
container.appendChild(p); setTimeout(() => p.remove(), 1000);
}
setTimeout(() => {
document.getElementById('muscle-display').innerText = newIcon;
document.getElementById('muscle-name').innerText = newName;
}, 50);
setTimeout(() => smoke.remove(), 700);
}
function updateSizeAndCheckMilestones() {
let prev = 0; let next = milestones[0].threshold;
for (let i = 0; i < milestones.length; i++) {
if (!milestones[i].achieved) { next = milestones[i].threshold; break; }
else { prev = milestones[i].threshold; next = (i + 1 < milestones.length) ? milestones[i + 1].threshold : prev; }
}
for (let i = 0; i < milestones.length; i++) {
let m = milestones[i];
if (!m.achieved && totalMp >= m.threshold) {
m.achieved = true; mp += m.reward;
let evolveIcon = m.icon; let evolveName = m.name;
if (m.isRandom) {
const randomEv = getRandomEvolution();
evolveIcon = randomEv.icon;
evolveName = m.isFinal ? "神格化・" + randomEv.name : randomEv.name + "・マッスル";
m.message = m.isFinal ? m.message : `融合:${evolveName}!`;
}
triggerEvolutionEffect(evolveIcon, evolveName);
showMilestoneAlert(m.message);
if (m.isFinal && !divineModeActive) {
setTimeout(triggerDivineAwakening, 500);
}
}
}
if (next > prev) {
let progress = Math.max(0, Math.min(1, (totalMp - prev) / (next - prev)));
let step = Math.min(4, Math.floor(progress / 0.2));
currentBaseScale = divineModeActive ? 2.5 : 1.0 + (step * 0.2);
} else {
currentBaseScale = divineModeActive ? 2.5 : 2.0;
}
}
function showMilestoneAlert(msg) {
const alertBox = document.getElementById('milestone-alert');
alertBox.innerText = msg; alertBox.style.opacity = 1;
setTimeout(() => alertBox.style.opacity = 0, 3000);
}
const normalCoachLines = ["限界を超えろ!", "ナイスパンプ!!", "特異点を突破しろ!", "マッスルコントロール!"];
const divineCoachLines = ["神の領域だ…!", "尊い筋肉…!", "黄金のパンプアップ!", "これが究極か…!"];
function randomCoachShout() {
if (totalMp > 50 && Math.random() < 0.4) {
playSE('coach');
const shoutBox = document.getElementById('coach-shout');
const lines = divineModeActive ? divineCoachLines : normalCoachLines;
shoutBox.innerText = lines[Math.floor(Math.random() * lines.length)];
const randomX = Math.floor(Math.random() * 20) - 10;
const randomY = Math.floor(Math.random() * 20) - 10;
shoutBox.style.transform = `translate(calc(-50% + ${randomX}vw), ${randomY}vh) scale(1.2) rotate(${Math.random()*10 - 5}deg)`;
shoutBox.style.opacity = 1;
setTimeout(() => {
shoutBox.style.transform = `translate(calc(-50% + ${randomX}vw), ${randomY}vh) scale(1) rotate(0deg)`;
shoutBox.style.opacity = 0;
}, 1200);
}
setTimeout(randomCoachShout, Math.floor(Math.random() * 4000) + 3000);
}
function train() {
playSE('click'); mp += clickPower; totalMp += clickPower;
updateSizeAndCheckMilestones(); updateUI();
const muscle = document.getElementById('muscle-display');
muscle.style.transform = `scale(${currentBaseScale + 0.2}) rotate(8deg)`;
if(muscleAnimTimeout) clearTimeout(muscleAnimTimeout);
muscleAnimTimeout = setTimeout(() => {
muscle.style.transform = `scale(${currentBaseScale}) rotate(0deg)`;
}, 80);
}
function buyUpgrade(type) {
if (mp >= costs[type]) {
playSE('buy'); mp -= costs[type];
if (type === 'dumbbell') { clickPower++; costs.dumbbell = Math.floor(costs.dumbbell * 1.5); }
else if (type === 'protein') { autoPower++; costs.protein = Math.floor(costs.protein * 1.4); }
else if (type === 'trainer') { autoPower += 10; costs.trainer = Math.floor(costs.trainer * 1.4); }
else if (type === 'reactor') { autoPower += 100; costs.reactor = Math.floor(costs.reactor * 1.4); }
updateUI();
}
}
setInterval(() => {
if (autoPower > 0) {
mp += autoPower; totalMp += autoPower;
updateSizeAndCheckMilestones(); updateUI();
document.getElementById('muscle-display').style.transform = `scale(${currentBaseScale}) rotate(0deg)`;
}
}, 1000);
updateUI();
</script>
</body>
</html>


■ 今日の学び

今回もAIがゲームに合わせた歌を作ってくれたので、紹介しますね。

みー:(*^v^)
このゲームに効果音とBGMを付けたいね。
テーマは「木漏れ日の幸運雑貨店」

AI: 「木漏れ日の幸運雑貨店」のテーマ、早速BGMを作っておいたよ。
再生ボタンを押して、お店の空気をさらに感じてみてほしい。

さて、ゲームの方にも音を仕込んでおこう。 ブラウザだけで完結するように、今回は Web Audio API を使って、 JavaScript内で直接「効果音(SE)」を生成するプログラムを組み込んだよ。

今回作った音はこんな感じ。

  • シャイラン♪:商品を手に取った時の音
  • ピロンッ!:商品が売れた(正解した)時の嬉しい音
  • ポコッ:買わずに戻した時の少し残念な音

さらに、画面の右上に「BGM・SEのオン/オフ」ボタンも追加したよ。
最初は音が鳴らない設定になっているので、
お店を開ける前にオンにしてね。

以下のコードを上書きして、音付きのお店を楽しんでおくれ。

最近、公園や広場で手作り雑貨のイベント、よく見かけますよね。
私も「素敵だなぁ」と思いながら、
そのあたたかい空間を眺めるのが好きです。

別にリアルで雑貨屋さんをやりたいと思ったわけではないのですが、
あの独特の「優しい雰囲気」をゲームで表現できないかなと思って作ってみました。

シーグラスが大好きで、たくさん集めています。
(以前のブログ記事「#14『シーグラス集め』」でも、好きなこととして紹介しましたね。)

集めたシーグラスを加工しようとチャレンジしたこともありますが、
ひもを通す穴をあけるだけでも一苦労。

絵を描くのと同じで、「作ることは好きだけど、上手じゃない」というのを実感します。 自分用を作るので精一杯でした。

でも、その技術的に足りない部分を補って、
私の感じた「憧れの雰囲気」を形にしてくれたのがAIでした。

素敵なアイテムを絵文字や図形で表現したり、
さわさわという風の音を付けたりして、
私の思い描く「優しい世界観」をゲームの中に作り出すことができました。

「上手じゃなくても、好きな雰囲気は形にできる」。
それがAIと一緒にゲームを作る、大きな喜びであり、今回の学びでした。

(画像:自作のシーグラス作品)

■ 次回は、ふわふわに特化したシミュレーターゲームです。

いろいろな “ふわふわ” がありますが、
それらをひとまとめにしたらどんな感じになるんだろう……?

そんな思いつきから生まれた、
ふわふわ成分だけをぎゅっと詰め込んだシミュレーターです。

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

▶ 次の実験はこちら
 (ふわふわとは)「究極のふわふわ」



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