実験#124:育てて収穫!おいしく炒めよう(ゴーヤの緑のカーテン シミュレーション)

■124個目 「ゴーヤの緑のカーテン シミュレーション」

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

日差しが強くなる季節になると、
窓辺にそよぐ「緑のカーテン」が恋しくなりますよね。

以前のスナップエンドウ収穫ゲームに続いて、
今回は夏のお庭にぴったりの「ゴーヤ」をテーマにした、
3Dシミュレーションゲームを作ってみました。

プランターからツルが上へ上へと伸び、
葉っぱがわさわさと茂っていく中で、
その影に隠れた“ちょうどいいゴーヤ”を探しながら育てていきます。

今回こだわったのは、葉っぱに紛れたゴーヤを見つける
「宝探しのようなワクワク感」です。(=v=*)

ゴーヤも葉っぱも同じ緑色なので、うっかり見落としてしまうと……
黄色く熟れてボトッと落ち、
中から真っ赤な種が飛び出してしまうんです!(><)

一番おいしい「緑で大きい状態」を見極めてクリックすると、
スパパッ!と輪切りになって熱々のフライパンへダイブ!

ドット絵のお豆腐や卵と一緒に、
ジューッ!と音を立てながらゴーヤチャンプルーになっていきます。(´▽`*)

ちなみに、黄色く熟れてしまった失敗ゴーヤは黒いコンポスターへ。
飲み込んだあとにガタガタッと揺れる姿も、
ちょっとした見どころになっています。

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

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

みー:(^v^*)今日もゲームを作ります。

HTMLで動かせる「ゴーヤの緑のカーテン シミュレーションゲーム」です。

3D視点でゴーヤを苗から育て、
ネットを這わせながらツルが伸びていきます。

成長の仕方は毎回違いますが、
最終的には立派な緑のカーテンに。
黄色い花が咲き、緑のゴーヤが実ります。

程よい大きさで収穫すると高ポイント。
早すぎたり遅すぎると減点で、
放置しすぎると黄色く熟れてしまい大幅減点になります。

以前のスナップエンドウ収穫ゲームに似ていますが、
今回は収穫すると瞬間輪切りになってフライパンへダイブ!

豆腐やお肉、卵と一緒にチャンプルーになります。
黄色くなった実は料理に使えないので黒いコンポスターへ。
小さい実は材料が少なめです。

ちょうどよい大きさのゴーヤを見極めて収穫し、
目標人数分のゴーヤチャンプルーを作ったらゲーム終了です。

AI: 3D空間でツルがランダムに伸びながら緑のカーテンを作り、
タイミングを見計らって収穫するゲームとしてコードを作成しました。

みー:(^v^) まず苗の育ち方から調整したいです。
ツルはもっと上に向かってぬるぬる伸びて、葉っぱもたくさん。

ゴーヤの実を見分ける難易度を上げたいです。
植物らしいリアルな成長を再現したい。

AI: 了解です。
ツルは滑らかな曲線で上へ伸びるようにし、
葉っぱもたっぷり追加して自然な茂り方にしました。

実と葉の色を近づけて、見つけにくさも調整しています。

みー:(つ<) 苗や蔓、葉が見えなくて、
いきなり花と実だけが出てきてるよ。

AI: すみません!
葉の画像読み込みとツルの太さ調整でエラーが出ていました。
外部画像を使わず、プログラムで葉の形を生成するように修正し、
ツルも安全にぬるぬる成長するように見直しました。

葉っぱも大量に生い茂るようにして、実が隠れる自然な状態になっています。

みー:(=v=*)いい感じ。
実の発生をもっとランダムにして、
サイズは7割くらいに。葉の割合も増やしたいです。

AI: 葉っぱマシマシ、実は少し小ぶりに調整しました。
発生タイミングも偏らないようにランダム化しています。

みー:( ̄_ ̄*)ツルをネット全体に広がる感じに。
葉っぱももう少し増やして。

クリアが速いので、ゴーヤがなくなるまで収穫できるようにしたいです。

AI: ツルが横にも広がるクセを追加して、
ネット全体を覆う緑のカーテンにしました。

葉っぱもさらに増量。

クリア条件も変更し、
シーズンに実る全40個を収穫しきるまで遊べるようにしました。

みー:ヽ(^_^) 葉っぱの影のゴーヤも分かるように、
収穫最適になったら主張する演出できる?

発生はもっとランダムに。
緑のカーテンの隙間も気になるので、偏らない設定にしたいです。

AI: 収穫タイミングになると、
葉の裏でも分かるように「光るワイヤーフレームのオーラ」を追加しました。

ツルは縦レーンを意識して伸びるようにして、隙間ができにくいカーテンに。

実の発生もツルのあちこちからランダムに出るように調整しています。

みー:(^^) 黄色くなったのを放置しすぎると、
実がぼとっと落ちて赤い種が見えるよね。
あれは減点倍にしたいです。
調理のところもブラッシュアップできたらお願い。

AI: 黄色く熟れたゴーヤは自然と割れて赤い種が見えるので、
放置しすぎると「ボトッと落ちて-100点」のペナルティを追加しました。

調理場も強化して、コンロの火がメラメラ、
フライパンがジュージュー揺れるアニメーションを入れてあります。
料理している感がぐっと増したはずです。



■ 実際の動作

まずは、AI が最初に作ってくれた試作版のゲーム画面をご紹介します。
この段階では、つるの伸び方も控えめで、
実が大きく育ちすぎてしまうことが多い状態でした。

(画像:AIが最初に出してくれた試作画面)


こちらが、調整を重ねて仕上がったゲーム画面です。
一瞬でネットいっぱいに緑のカーテンが広がり、
つやつやした黄色い花が咲き始めます。

(画像:仕上がりのゲーム画面)


花が咲いたところへ実が付き、ゴーヤがどんどん大きく成長していきます。
タイミングよくクリックして収穫できると、
左側のフライパンへスパッと刻まれて投入され、
豆腐や卵と一緒にジューッと音を立てながらゴーヤチャンプルーになります。

(画像:成長するゴーヤ)



収穫のタイミングを逃してしまうと、実は黄色く熟れてしまい、
ぼてっと下に落下します。
黄色くなったゴーヤは右下のコンポスターへ投入されて肥料に。
ポイントは減点となります。

(画像:熟れ過ぎたゴーヤ)


すべての収穫が終わると、今回のスコアが表示されます。
どれだけ上手に育てられたか、結果を見るのも楽しみのひとつです。

(画像:ゲーム終了画面)



■ コードのポイント

今回の記事では、
AI と相談しながら作った「ゴーヤの緑のカーテン シミュレーション」 の HTML コードを公開します。

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

ブラウザだけで動く、とてもシンプルなゲームなので、
「コードは初めて…」という方でも気軽に試してみてくださいね。

▼ 遊ぶまでの手順(むずかしさ:やさしい)
① テキストエディタを開く

Windows の場合は「メモ帳」、
Mac の場合は「テキストエディット」を使います。
(※検索バーで「メモ帳」と入力するとすぐ出てきます)

② 下のコードをコピーして貼り付けます。
③ 「go-ya.html」など、好きな名前で保存します。

(※拡張子は.html にしてください)

④ 保存したファイルをダブルクリックすると、
ブラウザでゲームが起動します。

■ 遊び方・操作方法

操作はとっても簡単! 3Dの緑のカーテンの中からゴーヤを見つけて、
マウスでクリック(タップ)するだけです。(^_^*)

■ 収穫のタイミングがポイント!

ゴーヤは時間とともに、

  • 黄色いお花
  • 赤ちゃんゴーヤ
  • 食べ頃の緑ゴーヤ
  • ちょっと巨大化
  • 黄色く完熟

……という順番で成長していきます。

一番の狙い目は、もちろん 「緑で大きい食べ頃」 の状態。

最高のタイミングになると、
葉っぱの奥に隠れていても見逃さないように
ピカピカと光るサイン が出ます。(=v=*)

見つけたら、すかさずクリック!

■ お料理 or コンポスト?

美味しい緑のゴーヤをクリックすると、
スパッと輪切りになってフライパンへダイブ!
ドット絵の具材たちと一緒に「ジューッ!」と炒められて、
ポイントとチャンプルーの分量がアップします。

でも、収穫が遅れて黄色くなってしまったゴーヤはお料理には使えません。
クリックすると黒いコンポスターへ一直線。減点になります。(><)

さらに放置しすぎて、
ぼとっ と落ちて弾けてしまったゴーヤ(赤い種が見える状態)を
お掃除すると…… なんと マイナス100点 の大減点です!

■ ゲームクリア

ひと夏に実るゴーヤは全部で 40個。
すべてのゴーヤを収穫(またはお掃除)して、
画面からゴーヤがなくなったらゲームクリアです!

葉っぱをよーく観察して、
何人前の特大ゴーヤチャンプルーを作れるか、
ぜひチャレンジしてみてくださいね。(^^*)ノシ


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

▼ここからコード▼(クリックで開く)
<!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;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #87CEEB;
user-select: none;
}
#ui-layer {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none;
}
.panel {
position: absolute;
background: rgba(255, 255, 255, 0.85);
padding: 15px; border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
pointer-events: auto;
border: 2px solid #4CAF50;
}
#status-panel {
top: 15px; left: 15px;
font-size: 1.2rem; font-weight: bold; color: #333;
}
#target-info { color: #E65100; margin-bottom: 5px; }
/* 調理場エリア */
#kitchen-station {
position: absolute;
bottom: 20px; left: 20px;
width: 220px; height: 220px;
pointer-events: auto;
}
#stove {
position: absolute;
bottom: 5px; left: 40px;
width: 120px; height: 30px;
background: #444;
border-radius: 10px;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.8), 0 5px 10px rgba(0,0,0,0.5);
z-index: 1;
}
.fire {
position: absolute;
bottom: 30px; left: 55px;
width: 90px; height: 35px;
background: linear-gradient(to top, #ff5500, #ffaa00, transparent);
border-radius: 50% 50% 0 0;
filter: blur(2px);
animation: flicker 0.15s infinite alternate;
z-index: 2;
}
@keyframes flicker {
0% { transform: scale(1) translateY(0); opacity: 0.9; }
100% { transform: scale(1.1) translateY(-3px); opacity: 1; }
}
#pan-container {
position: absolute;
bottom: 25px; left: 10px;
z-index: 3;
transform-origin: center bottom;
}
#pan-area {
position: relative;
width: 180px; height: 180px;
background: #2b2b2b; border-radius: 50%;
border: 6px solid #111;
display: flex; flex-wrap: wrap;
align-content: center; justify-content: center;
box-shadow: inset 0 0 20px rgba(0,0,0,0.9), 5px 15px 15px rgba(0,0,0,0.4);
}
#pan-handle {
position: absolute; bottom: 80px; left: -90px;
width: 100px; height: 22px; background: #222;
border-radius: 10px; transform: rotate(15deg);
box-shadow: 2px 5px 5px rgba(0,0,0,0.3);
}
.ingredient { margin: 4px; }
.pixel-art {
width: 40px; height: 40px;
shape-rendering: crispEdges;
filter: drop-shadow(2px 2px 0px rgba(0,0,0,0.6));
}
/* フライパンを振るアニメーション */
@keyframes pan-shake {
0% { transform: rotate(0deg) translateY(0); }
25% { transform: rotate(6deg) translateY(-8px); }
50% { transform: rotate(-4deg) translateY(0); }
75% { transform: rotate(4deg) translateY(-4px); }
100% { transform: rotate(0deg) translateY(0); }
}
.shake-anim {
animation: pan-shake 0.3s ease-in-out;
}
/* コンポスターエリアとガタガタアニメーション */
#compost-area {
bottom: 20px; right: 20px;
width: 120px; height: 150px; background: #111;
border-radius: 5px; display: flex;
align-items: center; justify-content: center;
color: white; font-weight: bold; flex-direction: column;
border-top: 15px solid #333;
transform-origin: center bottom;
}
@keyframes composter-rattle {
0% { transform: translate(0, 0) rotate(0deg); }
20% { transform: translate(-3px, 2px) rotate(-3deg); }
40% { transform: translate(3px, -2px) rotate(3deg); }
60% { transform: translate(-3px, -2px) rotate(-3deg); }
80% { transform: translate(3px, 2px) rotate(3deg); }
100% { transform: translate(0, 0) rotate(0deg); }
}
.rattle-anim {
animation: composter-rattle 0.3s ease-in-out;
}
.slice {
position: absolute; width: 20px; height: 20px;
border-radius: 50%; border: 4px solid; background: transparent;
pointer-events: none; transition: all 0.8s cubic-bezier(0.25, 1, 0.5, 1);
z-index: 100; opacity: 1;
}
.slice.green { border-color: #4CAF50; }
.slice.yellow { border-color: #FFC107; }
.slice.red { border-color: #FF0000; }
/* エフェクト用のスタイル */
.onomatopoeia {
position: absolute;
color: #FF7043;
font-size: 1.8rem;
font-weight: 900;
-webkit-text-stroke: 1px #FFF;
text-shadow: 2px 2px 2px rgba(0,0,0,0.5);
pointer-events: none;
transition: all 0.8s cubic-bezier(0.25, 1, 0.5, 1);
z-index: 200;
}
.steam {
position: absolute;
width: 25px; height: 25px;
background: rgba(255, 255, 255, 0.8);
border-radius: 50%;
filter: blur(5px);
pointer-events: none;
transition: all 1s ease-out;
z-index: 150;
}
#floating-score {
position: absolute;
color: #FF0000;
font-size: 2rem;
font-weight: bold;
pointer-events: none;
transition: all 1s ease-out;
opacity: 1;
text-shadow: 2px 2px 4px white;
z-index: 200;
}
#game-over {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.95); padding: 40px; border-radius: 20px;
text-align: center; display: none; pointer-events: auto; border: 4px solid #4CAF50;
z-index: 1000;
}
#game-over h1 { margin-top: 0; color: #4CAF50; }
button {
padding: 10px 20px; font-size: 1.1rem; background: #4CAF50;
color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 15px;
}
button:hover { background: #45a049; }
</style>
</head>
<body>
<div id="game-container"></div>
<div id="ui-layer">
<div id="status-panel" class="panel">
<div id="target-info">目標: すべてのゴーヤを収穫せよ!</div>
<div>チャンプルー: <span id="portions-display">0.0</span> 人前</div>
<div>スコア: <span id="score-display">0</span> pts</div>
<div>残りのゴーヤ: <span id="remaining-display">40</span> 個 (未発生含む)</div>
<div style="font-size: 0.9rem; margin-top: 5px; font-weight: normal;">
収穫適期にはサインが光るぞ!<br>
落ちて弾けた実は掃除すると特大減点!
</div>
</div>
<div id="kitchen-station">
<div class="fire"></div>
<div id="stove"></div>
<div id="pan-container">
<div id="pan-handle"></div>
<div id="pan-area">
<div class="ingredient">
<svg class="pixel-art" viewBox="0 0 8 8">
<rect x="0" y="0" width="8" height="8" fill="#111"/>
<rect x="1" y="1" width="6" height="6" fill="#F5F5F5"/>
<rect x="2" y="2" width="4" height="4" fill="#FFF"/>
<rect x="1" y="6" width="6" height="1" fill="#DDD"/>
<rect x="6" y="1" width="1" height="6" fill="#DDD"/>
</svg>
</div>
<div class="ingredient">
<svg class="pixel-art" viewBox="0 0 8 8">
<rect x="0" y="2" width="8" height="5" fill="#111"/>
<rect x="1" y="3" width="6" height="3" fill="#D32F2F"/>
<rect x="1" y="3" width="2" height="1" fill="#FFF"/>
<rect x="4" y="4" width="2" height="1" fill="#EF9A9A"/>
</svg>
</div>
<div class="ingredient">
<svg class="pixel-art" viewBox="0 0 8 8">
<rect x="1" y="1" width="6" height="6" fill="#111"/>
<rect x="2" y="0" width="4" height="8" fill="#111"/>
<rect x="1" y="2" width="6" height="4" fill="#FFF"/>
<rect x="2" y="1" width="4" height="6" fill="#FFF"/>
<rect x="3" y="3" width="2" height="2" fill="#FFC107"/>
</svg>
</div>
<div class="ingredient">
<svg class="pixel-art" viewBox="0 0 8 8">
<rect x="0" y="0" width="8" height="8" fill="#111"/>
<rect x="1" y="1" width="6" height="6" fill="#F5F5F5"/>
<rect x="2" y="2" width="4" height="4" fill="#FFF"/>
<rect x="1" y="6" width="6" height="1" fill="#DDD"/>
<rect x="6" y="1" width="1" height="6" fill="#DDD"/>
</svg>
</div>
</div>
</div>
</div>
<div id="compost-area" class="panel">
<div style="font-size: 2rem;">♻️</div><div>コンポスト</div>
</div>
<div id="game-over">
<h1>収穫完了!お疲れ様!</h1>
<p><span id="final-portions" style="font-weight:bold; font-size:1.2rem;">0</span> 人前の特大チャンプルーが完成!</p>
<p>最終スコア: <span id="final-score" style="font-weight:bold; font-size:1.2rem;">0</span> pts</p>
<button onclick="location.reload()">もう一度育てる</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let score = 0, portions = 0, isGameOver = false;
const MAX_GOYAS = 40;
let totalSpawned = 0;
let harvestedCount = 0;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 3, 10);
camera.lookAt(0, 4, 0);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('game-container').appendChild(renderer.domElement);
scene.add(new THREE.AmbientLight(0xffffff, 0.7));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 10, 7);
scene.add(dirLight);
const netMat = new THREE.LineBasicMaterial({ color: 0xcccccc });
for(let i = -5; i <= 5; i++) {
scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(i, 0, 0), new THREE.Vector3(i, 10, 0)]), netMat));
if(i >= 0 && i <= 10) scene.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints([new THREE.Vector3(-5, i, 0), new THREE.Vector3(5, i, 0)]), netMat));
}
const planter = new THREE.Mesh(new THREE.BoxGeometry(11, 1, 2), new THREE.MeshLambertMaterial({ color: 0x8B4513 }));
planter.position.set(0, -0.5, 0);
scene.add(planter);
const leafShape = new THREE.Shape();
leafShape.moveTo(0, 0);
leafShape.bezierCurveTo(0.3, 0.2, 0.4, 0.6, 0, 1.0);
leafShape.bezierCurveTo(-0.4, 0.6, -0.3, 0.2, 0, 0);
const leafGeometry = new THREE.ShapeGeometry(leafShape);
const vineMat = new THREE.MeshLambertMaterial({ color: 0x6DAA46 });
const leafMat = new THREE.MeshLambertMaterial({ color: 0x3b7a24, side: THREE.DoubleSide });
const vines = [];
for(let i = -5; i <= 5; i += 1) {
vines.push({ points: [new THREE.Vector3(i, 0, 0.1)], mesh: null, startX: i, tendencyX: 0 });
}
const goyas = [];
const validSpawnPoints = [];
const goyaGeo = new THREE.IcosahedronGeometry(1, 2);
const flowerGeo = new THREE.ConeGeometry(0.3, 0.5, 5);
class GoyaItem {
constructor(x, y, z) {
this.age = 0; this.state = 0;
this.mesh = new THREE.Mesh(flowerGeo, new THREE.MeshLambertMaterial({ color: 0xFFD700 }));
this.mesh.position.set(x, y, z + (Math.random() * 0.4 - 0.2));
this.mesh.rotation.x = Math.PI;
this.mesh.scale.set(0.5 * 0.7, 0.5 * 0.7, 0.5 * 0.7);
scene.add(this.mesh);
this.aura = null;
this.vy = 0;
goyas.push(this);
}
update() {
this.age++;
if (this.state === 0 && this.age > 200) {
this.state = 1;
this.mesh.geometry = goyaGeo;
this.mesh.material = new THREE.MeshLambertMaterial({ color: 0x4CAF50 });
this.mesh.scale.set(0.1 * 0.7, 0.2 * 0.7, 0.1 * 0.7);
} else if (this.state >= 1 && this.state < 5) {
if (this.age < 1200) {
let s = this.age / 1200;
let scX = (0.1 + (s * 0.4)) * 0.7;
let scY = (0.2 + (s * 1.2)) * 0.7;
this.mesh.scale.set(scX, scY, scX);
if (this.age > 1000) this.state = 4;
else if (this.age > 600) this.state = 3;
else if (this.age > 300) this.state = 2;
} else if (this.state !== 5) {
this.state = 5;
this.mesh.material.color.setHex(0xFFC107);
}
} else if (this.state === 5 && this.age > 1700) {
this.state = 6;
const seedGeo = new THREE.SphereGeometry(0.2, 8, 8);
const seedMat = new THREE.MeshLambertMaterial({ color: 0xFF0000 });
for(let i=0; i<4; i++) {
let seed = new THREE.Mesh(seedGeo, seedMat);
seed.position.set((Math.random()-0.5)*0.8, (Math.random()-0.5)*1.5, (Math.random()-0.5)*0.8);
this.mesh.add(seed);
}
}
if (this.state === 6) {
if (this.mesh.position.y > 0) {
this.vy += 0.005;
this.mesh.position.y -= this.vy;
this.mesh.rotation.z += 0.05;
}
}
if (this.state === 3) {
if (!this.aura) {
const auraGeo = new THREE.IcosahedronGeometry(1, 1);
const auraMat = new THREE.MeshBasicMaterial({ color: 0x00FFaa, wireframe: true, transparent: true, opacity: 0.8, depthTest: false });
this.aura = new THREE.Mesh(auraGeo, auraMat);
scene.add(this.aura);
}
this.aura.position.copy(this.mesh.position);
this.aura.rotation.copy(this.mesh.rotation);
let pulse = 1.3 + Math.sin(this.age * 0.15) * 0.2;
this.aura.scale.set(this.mesh.scale.x * pulse, this.mesh.scale.y * pulse, this.mesh.scale.z * pulse);
} else {
if (this.aura) {
scene.remove(this.aura);
this.aura.geometry.dispose();
this.aura.material.dispose();
this.aura = null;
}
}
}
}
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('mousedown', (e) => {
if(isGameOver) return;
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(goyas.map(g => g.mesh));
if (intersects.length > 0) {
const clicked = intersects[0].object;
const idx = goyas.findIndex(g => g.mesh === clicked);
if(idx !== -1) {
harvest(goyas[idx], e.clientX, e.clientY);
scene.remove(clicked);
goyas.splice(idx, 1);
}
}
});
function harvest(goya, mX, mY) {
if (goya.state === 0) { score -= 10; updateUI(); return; }
harvestedCount++;
if (goya.aura) {
scene.remove(goya.aura);
goya.aura.geometry.dispose();
goya.aura.material.dispose();
}
let p = 0, s = 0, colorType = 'green';
if(goya.state === 1) { p = 0.1; s = 10; }
else if(goya.state === 2) { p = 0.4; s = 50; }
else if(goya.state === 3) { p = 1.0; s = 100; }
else if(goya.state === 4) { p = 0.7; s = 30; }
else if(goya.state === 5) { p = 0; s = -50; colorType = 'yellow'; }
else if(goya.state === 6) {
p = 0; s = -100; colorType = 'red';
showFloatingScore(mX, mY, "-100");
}
portions += p; score += s;
updateUI();
createSliceAnimation(mX, mY, colorType, goya.state);
checkGameClear();
}
function showFloatingScore(x, y, text) {
const floatText = document.createElement('div');
floatText.id = 'floating-score';
floatText.innerText = text;
floatText.style.left = x + 'px';
floatText.style.top = y + 'px';
document.body.appendChild(floatText);
requestAnimationFrame(() => {
floatText.style.transform = 'translateY(-50px)';
floatText.style.opacity = '0';
});
setTimeout(() => floatText.remove(), 1000);
}
// フライパンのエフェクト(ジュー&湯気)
function triggerPanEffect(x, y) {
const pan = document.getElementById('pan-container');
pan.classList.remove('shake-anim');
void pan.offsetWidth;
pan.classList.add('shake-anim');
// 「ジュー」文字
const text = document.createElement('div');
text.className = 'onomatopoeia';
text.innerText = 'ジュー';
text.style.left = (x + (Math.random() - 0.5) * 40 - 20) + 'px';
text.style.top = (y + (Math.random() - 0.5) * 40 - 20) + 'px';
document.body.appendChild(text);
// 湯気パーティクル
for(let j = 0; j < 3; j++) {
const steam = document.createElement('div');
steam.className = 'steam';
steam.style.left = (x + (Math.random() - 0.5) * 80) + 'px';
steam.style.top = (y + (Math.random() - 0.5) * 40) + 'px';
document.body.appendChild(steam);
requestAnimationFrame(() => {
steam.style.transform = `translateY(-${50 + Math.random() * 50}px) scale(${1 + Math.random()})`;
steam.style.opacity = '0';
});
setTimeout(() => steam.remove(), 1000);
}
requestAnimationFrame(() => {
text.style.transform = `translateY(-50px) scale(1.3) rotate(${(Math.random() - 0.5) * 30}deg)`;
text.style.opacity = '0';
});
setTimeout(() => text.remove(), 800);
}
// コンポスターのエフェクト(ガタガタ)
function triggerCompostEffect() {
const comp = document.getElementById('compost-area');
comp.classList.remove('rattle-anim');
void comp.offsetWidth;
comp.classList.add('rattle-anim');
}
function checkGameClear() {
if (totalSpawned >= MAX_GOYAS && goyas.length === 0) {
isGameOver = true;
document.getElementById('final-portions').innerText = portions.toFixed(1);
document.getElementById('final-score').innerText = score;
document.getElementById('game-over').style.display = 'block';
}
}
function createSliceAnimation(x, y, colorType, state) {
const count = state >= 3 ? 5 : (state === 2 ? 3 : 1);
const isCompost = (colorType === 'yellow' || colorType === 'red');
const target = document.getElementById(isCompost ? 'compost-area' : 'pan-container');
const rect = target.getBoundingClientRect();
const targetX = rect.left + rect.width / 2;
const targetY = rect.top + rect.height / 2;
for (let i = 0; i < count; i++) {
setTimeout(() => {
const slice = document.createElement('div');
slice.className = 'slice ' + colorType;
slice.style.left = x + 'px'; slice.style.top = y + 'px';
document.body.appendChild(slice);
requestAnimationFrame(() => requestAnimationFrame(() => {
slice.style.left = (targetX - 10 + (Math.random()-0.5)*60) + 'px';
slice.style.top = (targetY - 10 + (Math.random()-0.5)*60) + 'px';
// 着弾時の処理 (800ms後)
setTimeout(() => {
slice.remove();
if (isCompost) {
triggerCompostEffect();
} else {
triggerPanEffect(targetX, targetY);
}
}, 800);
}));
}, i * 50);
}
}
function updateUI() {
document.getElementById('portions-display').innerText = portions.toFixed(1);
document.getElementById('score-display').innerText = score;
document.getElementById('remaining-display').innerText = MAX_GOYAS - harvestedCount;
}
let frame = 0;
function animate() {
requestAnimationFrame(animate);
if(!isGameOver) {
frame++;
if (frame % 3 === 0) {
vines.forEach(vine => {
const pts = vine.points;
if(pts[pts.length-1].y < 10 && pts.length < 150) {
const prev = pts[pts.length - 1];
let distFromStart = prev.x - vine.startX;
vine.tendencyX -= distFromStart * 0.05;
let nextX = prev.x + vine.tendencyX + (Math.random() - 0.5) * 0.3;
nextX = Math.max(-5, Math.min(5, nextX));
const next = new THREE.Vector3(
nextX,
prev.y + (Math.random() * 0.25 + 0.1),
Math.max(-0.2, Math.min(0.2, prev.z + (Math.random() - 0.5) * 0.2))
);
pts.push(next);
if (next.y > 1.5) validSpawnPoints.push(next.clone());
const leafCount = Math.floor(Math.random() * 3) + 3;
for(let i=0; i<leafCount; i++) {
const leaf = new THREE.Mesh(leafGeometry, leafMat);
leaf.position.copy(next);
leaf.position.x += (Math.random() - 0.5) * 1.2;
leaf.position.y += (Math.random() - 0.5) * 1.0;
leaf.position.z += (Math.random() - 0.5) * 0.8;
leaf.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, Math.random()*Math.PI);
const sc = 0.5 + Math.random() * 0.7;
leaf.scale.set(sc, sc, sc);
scene.add(leaf);
}
if(pts.length > 1) {
const curve = new THREE.CatmullRomCurve3(pts);
const tube = new THREE.TubeGeometry(curve, pts.length * 2, 0.05, 5, false);
if(vine.mesh) {
scene.remove(vine.mesh);
vine.mesh.geometry.dispose();
}
vine.mesh = new THREE.Mesh(tube, vineMat);
scene.add(vine.mesh);
}
}
});
}
if (frame % 15 === 0 && totalSpawned < MAX_GOYAS && validSpawnPoints.length > 50) {
const randomPoint = validSpawnPoints[Math.floor(Math.random() * validSpawnPoints.length)];
new GoyaItem(randomPoint.x, randomPoint.y, randomPoint.z);
totalSpawned++;
}
goyas.forEach(g => g.update());
checkGameClear();
}
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();
</script>
</body>
</html>


■ 今日の学び

季節になると、窓辺にそよぐ「緑のカーテン」は本当に心地よい存在ですよね。

職場の窓際やお家のベランダで育てたことがある方も、
きっと多いのではないでしょうか。

日差しをやわらげてくれるだけでなく、
育てたあとには「ゴーヤ」という美味しいごほうびまでついてくる、
とても素敵な自然のカーテンです。(* ✧ ▽ ✧ *)

今回のゲームは、そんな緑のカーテンをテーマにしたものですが、
収穫という点では第64回の「スナップエンドウ収穫ゲーム」とよく似ています。
葉っぱも実も同じ緑色で見つけにくいところや、
「無駄なく収穫していく」というゲームの根っこは共通しているんですよね。

黄色くなって落ちてしまう前に、一番おいしいタイミングで収穫する。

ゲームの中のささやかな行動ですが、
自然の恵みを大切にすることや、
資源を無駄にしないことの大切さを
改めて感じるきっかけにもなりました。(* ˘ _ ˘ *)

■ 次回は、魔法で敵を吹き飛ばす爽快アクション実験ゲームです。

今回は「風を操る気持ちよさを表現してみたい」と思い、
マウスで狙って魔法を放つ防衛アクションを作ってみました。

雪山の林を舞台に、主人公の 大魔導士シマエナガ が、
10種類の風の魔法を使い分けながら「何か怖いの」達を吹き飛ばしていきます。

ストック制の魔法を駆使して立ち回ることで、
爽快感と少しの手ごたえが楽しめるゲームになっています。

次回もぜひ楽しみにしていてくださいね。(*^v^)ノシ


▶ 次の実験はこちら
風を操る爽快アクション
「大魔導士シマエナガ – 吹雪の防衛戦」




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