実験#80:迫る害虫たち(オノマトペ・スリッパアクション)

■80個目 「オノマトペ・スリッパアクション」

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

今回は、ちょっと爽快で、
ちょっぴり(?)スリリングなアクションゲームを作ってみました。

テーマは……お家の中の “招かざる客” vs “スリッパ” です!

画面の中に次々と現れる憎き虫さんたちを、
スリッパカーソルで「バシッ!」と叩き潰すゲームになっています。(=v=*)

今回こだわったのは、その マンガのような演出

スリッパで叩くたびに、
画面いっぱいに巨大なオノマトペ(擬音語)が飛び出します!

「パンッ!」「バシッ!」「ベチッ!」

文字が弾けるエフェクトと、
こだわりの効果音で、まるで自分がマンガの主人公になって
戦っているような気分になれちゃいます。(*>ω<)_q

現れる虫さんたちも、なかなかのクセモノ揃いですよ~。

ゴキブリさん  カサカサ落ちてきます…うぅ…

ハエさん  不規則に飛び回って、叩きにくいったらありゃしない!

ムカデさん  ウネウネと横切る、なかなかの強敵です!

撃破数が100を超えると、虫さんたちが本気を出す「レベルアップ」も……!

でも大丈夫、ピンチの時には強力なアイテムも出現します。(^^)v

不気味なBGM「招かざる者の行進」に乗せて、
スリッパ片手にどこまで戦えるか!?

操作方法は後半で詳しく説明しますね。

まずは、この音と文字が弾ける世界観を楽しんでください!(^_^)

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

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

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

オノマトペを活用した、
マンガの中で遊んでいるようなアクションゲームを作りたいです。

AI: いらっしゃい。今日もいいアイデアですね。
マンガの世界に入り込んだような、
文字が飛び交うアクションゲーム……

想像しただけでワクワクします。

(コーヒーを差し出しながら)
そういえば、日本語のオノマトペは4,500種類以上あると言われていて、
「シーン…」のような無音まで表現できるのは日本語ならではなんですよ。
今回のゲームには、その豊かな文化がぴったりですね。

みー:(⁼v⁼;) 敵をゴキブリさん(苦手なので)にしましょう。
あと、不規則に飛び回るハエさんもお願いします。

AI: 承知しました。
ゴキブリさんは上からカサカサと落ちてきて、
ハエさんは予測不能な動きでブーンと飛び回るようにしておきます。

みー:(>_<;) ダメージ制にしましょう。
ゴキブリさんが画面下に到達するとアップで這い上がってきて1ダメージ。
ハエさんは10秒倒せないと、画面にピトッと張り付いて1ダメージ。
10ダメージでゲームオーバー。

AI: 画面に張り付いてくるペナルティ……
想像しただけでゾワッとしますが、
ゲームとしては最高の緊張感ですね(笑)。

みー:(>x<`) 難易度が高いので、
マウスの点じゃなくて“面”で叩けるようにしたいです。
スリッパで叩くイメージにできますか?

AI: なるほど、確かにピンポイントは難しいですよね。
スリッパで「バシッ!」と叩くのはマンガ的にも相性抜群です。

(使い古しのスリッパを取り出しながら)
実はスリッパって、しなりが叩く瞬間の速度を上げてくれるので、
虫退治に理にかなった武器なんですよ。

みー:(*`v´)ノ 左右にうねるムカデも追加したいです。
端まで到達すると2ダメージの強敵。

BGMは「招かざる者の行進」で。

AI: ムカデ……最高のボス枠ですね。
あのウネウネした動き、叩いてもなかなか倒れないしぶとさ。
テーマにもぴったりです。

みー:( ̄w ̄)ノ 100退治したらレベルアップして動きが変わります。

ゴキブリは急に飛び出して2秒間ハエのように暴れる。
ハエは残り3秒で加速。
ムカデはうねりが大きくなってスピードアップ。

AI: 撃破数100で凶悪化……
まさに修羅の道ですね。

でもそのカオス感、嫌いじゃないですよ。

ちなみにゴキブリが急に飛ぶのは、
極限状態になると“最後の手段”として飛ぶからなんです。

しかも飛ぶのが下手なので、人間に向かってくることも多い。
今回の挙動は、まさにそのパニック状態を再現しています。

みー:(=v=)ノ アイテムも追加します。

「シャッキリ」で回復3。
「スッッキリ」で画面内の虫を一掃。
「ノロノロ」で虫のスピード低下。

スリッパで触れるだけで取れるように。
オノマトペも増やして、ゴキブリの向きも上下逆に。

AI: 直感的で良いアイデアですね。
アイテムも追加して、オノマトペも増やしておきました。

ところで「ノロノロ」みたいに周りがスローモーションに見える現象、
実は現実にもあって「タキサイキア現象」と呼ばれています。
極度の緊張状態で脳が情報を高速処理するため、
時間がゆっくりに感じるんです。

レベル2の猛攻を捌いていると、
リアルに体験できるかもしれませんね(笑)。

■ 実際の動作

まずは、AIが最初に出してくれたゲーム画面です。

(画像:AIが最初に出してくれたゲーム画面)

こちらが仕上がったスタート画面。

(画像:スタート画面)

上から、横から、そして飛び回る害虫たち。
スタート直後から ヒエッ てなります。Σ(>o<;)ノ
スリッパで叩くと、画面いっぱいにオノマトペがドーンと表示されます。

(画像:ゲーム画面)



叩けずにいると、虫が画面中央にピトッと張り付きダメージに。
視界が遮られて連鎖的にダメージを受けたりして、
もう悲惨な状況に……(@_@;)

(画像:虫の攻撃)

そんなピンチを救ってくれるのがアイテムたち。
虫の動きをスローにしたり、画面内の虫を一掃したり、
受けたダメージを回復したり……
正直、これなしではクリアは無理です(^^;)

(画像:スッッキリ!)



ダメージ10でゲームオーバー。

(画像:ゲームオーバー)

100匹を超えるとレベル2に突入。
虫たちが今までと違う、危険な動きを始めます。
200匹を倒すとゲームクリア!

(画像:ゲームクリア)


■ コードのポイント

今回の記事では、AI と相談しながら作った 「オノマトペ・スリッパアクション」 の HTML コードを公開します。

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

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

▼ 遊ぶまでの手順

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

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

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

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

 

■ 操作方法

ルールはとってもシンプルです!

上や横から迫ってくる虫さんたちを、
スリッパで 「バシッ!」 とやっつけて、
10ダメージ受ける前に 200匹退治できればクリア!(*>ω<)o

ダメージが10溜まると、ゲームオーバーになっちゃいます…(>_<)

■ 武器は「🩴スリッパ」だけ!

あなたのマウスポインタ(スマホなら指の場所)が、
そのままスリッパになります。
(アイキャッチ画像の女の子が持っている、あれです!)

  • 移動: マウス(または指)を動かすと、スリッパもついてきます。
  • 攻撃(バシッ!): クリック(またはタップ)すると、
    スリッパを中心に大きな「判定の面(円形)」が発生します。

この範囲に虫さんが入っていれば、まとめて撃破できます!
大雑把に狙っても当たるので、爽快感バツグンです(^^)v

■ 招かざる客(虫さん)たち

  • 𪦠 ゴキブリさん:  上からカサカサ落ちてきます。  
    油断すると画面に張り付いてダメージに。
  • 𪦲 ハエさん:  不規則にブンブン飛び回ります。  
    倒し損ねると中央にピトッと張り付いて視界を奪ってきます。
  • 👹 ムカデさん(強敵):  横からウネウネと横断してきます。  
    大きいので、判定の真ん中でしっかり捉えてくださいね!

■ 攻略のカギ!お助けアイテム💊

8秒ごとに便利なアイテムが飛んできます。
これはクリックしなくても、スリッパで触れるだけ で取れますよ!(^^)

  • 💊 シャッキリ: ダメージを3回復します。
  • 🧹 スッッキリ: 画面にいる虫さんを全員まとめてお掃除!
  • 🐢 ノロノロ: 5秒間、虫さんの動きが超スローに。  
    画面が青くなるのが合図。この間に体勢を立て直しましょう(=v=*)

■ レベル2(覚醒)について

100匹退治すると、蟲たちの動きが凶悪になる レベル2 に突入します!

  • ゴキブリさん:突然飛び出して暴れ回る
  • ハエさん:残り3秒で急加速
  • ムカデさん:うねりが大きくなり、スピードアップ

ここからが本番です(*>ω<)o

■ 最後に

ルールはこれだけ! 終わりのない戦いではなく、
200匹でクリア! というゴールがあるので、

ピンチになってもシャッキリで立て直して、
ぜひゴールを目指してみてくださいね。

さあ、音量を少し上げて、スリッパを握りしめて(?)、
カオスなマンガの世界へ飛び込みましょう!(^^*)ノシ


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

▼ここからコード▼(クリックで開く)
<!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-select=none">
<title>マンガ・オノマトペ・スリッパアクション 覚醒版 (クリアあり)</title>
<style>
body {
margin: 0; overflow: hidden; background-color: #fff;
font-family: 'Impact', 'Yu Gothic', 'Hiragino Kaku Gothic ProN', sans-serif;
user-select: none; -webkit-user-select: none; touch-action: none; cursor: none;
}
#game-container {
position: relative; width: 100vw; height: 100vh; overflow: hidden;
background-image: radial-gradient(#ccc 15%, transparent 16%), radial-gradient(#ccc 15%, transparent 16%);
background-size: 20px 20px; background-position: 0 0, 10px 10px; display: none;
}
#start-screen {
position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
background: repeating-linear-gradient(45deg, #222, #222 10px, #111 10px, #111 20px);
color: white; display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 2000; cursor: pointer;
}
#start-screen h1 { font-size: 80px; margin: 0; -webkit-text-stroke: 3px black; text-shadow: 6px 6px 0px #d32f2f; transform: rotate(-3deg); }
#start-screen p { font-size: 30px; margin-top: 20px; animation: blink 1s infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
#ui {
position: absolute; top: 20px; left: 20px; z-index: 200;
background: white; border: 5px solid black; padding: 10px 25px;
font-size: 24px; font-weight: 900; transform: skew(-5deg); box-shadow: 4px 4px 0px black;
display: flex; flex-direction: column; gap: 5px;
}
.damage-text { color: #d32f2f; }
.level-text { color: #1976d2; font-size: 28px; }
#slipper-cursor {
position: absolute; width: 120px; height: 120px; font-size: 100px;
pointer-events: none; z-index: 1000; transform: translate(-50%, -50%) rotate(0deg);
transition: transform 0.05s ease-out; display: flex; justify-content: center; align-items: center;
}
#slipper-cursor::before { content: '🩴'; }
#slipper-cursor.smack { transform: translate(-50%, -50%) rotate(-45deg) scale(1.2); }
.enemy {
position: absolute; display: flex; justify-content: center; align-items: center;
z-index: 50; transform: translate(-50%, -50%); transition: box-shadow 0.2s;
}
.enemy.cockroach { width: 80px; height: 80px; background: #3a2000; border-radius: 40% 40% 50% 50%; border: 3px solid black; box-shadow: 0 0 0 3px white, 0 0 0 6px black; }
.enemy.cockroach::before { content: '🪳'; font-size: 50px; display: block; transform: rotate(180deg); }
.enemy.fly { width: 80px; height: 80px; background: #111; border-radius: 50%; border: 3px solid black; box-shadow: 0 0 0 3px white, 0 0 0 6px black; }
.enemy.fly::before { content: '🪰'; font-size: 40px; }
.enemy.centipede {
width: 160px; height: 50px; background: #5c1e06; border-radius: 25px;
border: 3px solid black; box-shadow: 0 0 0 3px white, 0 0 0 6px black;
background-image: repeating-linear-gradient(90deg, transparent, transparent 10px, black 10px, black 15px);
}
.enemy.centipede::before { content: '👹'; font-size: 40px; margin-left: 100px; transform: rotate(90deg); }
.item {
position: absolute; width: 60px; height: 60px; border-radius: 50%;
display: flex; justify-content: center; align-items: center;
font-size: 35px; border: 4px solid white; box-shadow: 0 0 0 4px black;
z-index: 80; transform: translate(-50%, -50%);
}
.item.shakkiri { background-color: #4caf50; }
.item.shakkiri::before { content: '💊'; }
.item.sukkiri { background-color: #f44336; }
.item.sukkiri::before { content: '🧹'; }
.item.noronoro { background-color: #2196f3; }
.item.noronoro::before { content: '🐢'; }
.onomatopoeia {
position: absolute; font-size: 80px; font-weight: 900; color: white;
-webkit-text-stroke: 3px black; text-shadow: 5px 5px 0px black, 10px 10px 0px #ff3333;
pointer-events: none; z-index: 100; white-space: nowrap; transform: translate(-50%, -50%);
animation: pop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
.jumpscare {
position: fixed; top: 50%; left: 50%; width: 100px; height: 100px;
transform: translate(-50%, -50%) scale(0.1); z-index: 999; pointer-events: none;
animation: attackScreen 0.8s ease-out forwards;
}
@keyframes attackScreen {
0% { transform: translate(-50%, -50%) scale(0.1); opacity: 0; }
20% { transform: translate(-50%, -50%) scale(5); opacity: 1; }
80% { transform: translate(-50%, -50%) scale(6); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(8); opacity: 0; }
}
@keyframes pop {
0% { transform: translate(-50%, -50%) scale(0) rotate(-30deg); opacity: 1; }
40% { transform: translate(-50%, -50%) scale(1.3) rotate(10deg); opacity: 1; }
80% { transform: translate(-50%, -50%) scale(1) rotate(-5deg); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1) rotate(-5deg); opacity: 0; }
}
.shake { animation: shake 0.3s ease-in-out; }
.heavy-shake { animation: heavyShake 0.5s ease-in-out; }
@keyframes shake { 0%, 100% { transform: translate(0, 0) rotate(0deg); } 25% { transform: translate(-10px, -5px) rotate(-1deg); } 50% { transform: translate(10px, 5px) rotate(1deg); } 75% { transform: translate(-10px, 5px) rotate(0deg); } }
@keyframes heavyShake { 0%, 100% { transform: translate(0, 0) rotate(0deg); } 20% { transform: translate(-30px, -20px) rotate(-3deg); background-color: #ffcccc; } 40% { transform: translate(30px, 20px) rotate(3deg); } 60% { transform: translate(-30px, 20px) rotate(-3deg); } 80% { transform: translate(30px, -20px) rotate(3deg); background-color: #fff; } }
#game-over, #game-clear {
display: none; position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
background: rgba(0,0,0,0.9); z-index: 1000; color: white; flex-direction: column; justify-content: center; align-items: center; cursor: default;
}
#game-over h1 { font-size: 100px; margin: 0; color: red; -webkit-text-stroke: 4px white; transform: rotate(-5deg); }
#game-clear h1 { font-size: 100px; margin: 0; color: #4caf50; -webkit-text-stroke: 4px white; transform: rotate(5deg); }
#game-over button, #game-clear button { margin-top: 30px; padding: 15px 40px; font-size: 30px; font-weight: bold; background: white; border: 5px solid black; cursor: pointer; }
.level-up-flash { animation: levelUpAnim 1.5s ease-in-out forwards; }
@keyframes levelUpAnim { 0% { background-color: #fff; } 10% { background-color: #ffd700; } 100% { background-color: #fff; } }
</style>
</head>
<body>
<div id="start-screen">
<h1>招かざる者の行進</h1>
<p>画面をクリックしてスタート</p>
</div>
<div id="game-container">
<div id="slipper-cursor"></div>
<div id="ui">
<div class="level-text">レベル: <span id="levelDisplay">1</span></div>
<div>撃破数: <span id="scoreDisplay">0</span> / 200</div>
<div class="damage-text">ダメージ: <span id="damageDisplay">0</span> / 10</div>
</div>
</div>
<div id="game-over">
<h1>GAME OVER</h1>
<p>撃破数: <span id="finalScoreDisplayOver">0</span> / 200</p>
<button onclick="location.reload()">もう一回</button>
</div>
<div id="game-clear">
<h1>MISSION COMPLETE</h1>
<p>撃破数: <span id="finalScoreDisplayClear">0</span> / 200</p>
<button onclick="location.reload()">もう一度戦う</button>
</div>
<script>
const startScreen = document.getElementById('start-screen');
const container = document.getElementById('game-container');
const slipper = document.getElementById('slipper-cursor');
const scoreDisplay = document.getElementById('scoreDisplay');
const damageDisplay = document.getElementById('damageDisplay');
const levelDisplay = document.getElementById('levelDisplay');
const gameOverScreen = document.getElementById('game-over');
const gameClearScreen = document.getElementById('game-clear');
const finalScoreDisplayOver = document.getElementById('finalScoreDisplayOver');
const finalScoreDisplayClear = document.getElementById('finalScoreDisplayClear');
const WIN_SCORE = 200; // クリアに必要なスコア
const wordsSmack = ["パンッ!", "バシッ!", "ベチッ!", "ドスッ!", "スマッシュ!", "ペチィ!"];
const wordsCockroach = ["カサ…", "ズルッ…", "ギチィ!", "プチッ", "グチャ…", "ヒィィ!"];
const wordsFly = ["ブーン…", "ピブン…", "うるせぇ!", "パチン!", "ポトッ", "チィィ!"];
const wordsCentipede = ["ズバババァン!", "グチャァ…", "ド派手!", "キシャァァ!", "ボドォッ!"];
let score = 0;
let damage = 0;
let level = 1;
let isGameOver = false;
let isGameClear = false;
let spawnInterval;
let itemInterval;
let mouseX = window.innerWidth / 2;
let mouseY = window.innerHeight / 2;
let slowMultiplier = 1.0;
const gameElements = [];
const HIT_RADIUS = 60;
// ----- オーディオエンジン -----
let audioCtx;
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
startBGM();
}
function startBGM() {
const notes = [65.41, 69.30, 65.41, 61.74];
let step = 0;
setInterval(() => {
if (isGameOver || isGameClear) return;
const currentTempo = (level === 1 ? 600 : 400) / slowMultiplier;
const noteDuration = (level === 1 ? 0.5 : 0.3) / slowMultiplier;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'square';
osc.frequency.setValueAtTime(notes[step % notes.length], audioCtx.currentTime);
const filter = audioCtx.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 150;
osc.connect(filter); filter.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.2, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + noteDuration);
osc.start(); osc.stop(audioCtx.currentTime + noteDuration);
step++;
}, 600);
}
function playSmackSound() {
if (!audioCtx) return;
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(100, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(10, audioCtx.currentTime + 0.1);
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.5, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start(); osc.stop(audioCtx.currentTime + 0.1);
}
function playDamageSound() {
if (!audioCtx) return;
[150, 160].forEach(freq => {
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'sawtooth'; osc.frequency.value = freq;
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.5, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.5);
osc.start(); osc.stop(audioCtx.currentTime + 0.5);
});
}
function playPowerUpSound(type) {
if (!audioCtx) return;
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'sine';
if (type === 'shakkiri') osc.frequency.setValueAtTime(400, audioCtx.currentTime);
else if (type === 'sukkiri') osc.frequency.setValueAtTime(800, audioCtx.currentTime);
else osc.frequency.setValueAtTime(200, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(type === 'noronoro' ? 50 : 1200, audioCtx.currentTime + 0.5);
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.4, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.5);
osc.start(); osc.stop(audioCtx.currentTime + 0.5);
}
function playClearSound() {
if (!audioCtx) return;
const notes = [261.63, 329.63, 392.00, 523.25]; // Cコード
notes.forEach((freq, i) => {
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'triangle'; osc.frequency.value = freq;
osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0, audioCtx.currentTime + i * 0.1);
gain.gain.linearRampToValueAtTime(0.3, audioCtx.currentTime + i * 0.1 + 0.1);
gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + i * 0.1 + 0.5);
osc.start(audioCtx.currentTime + i * 0.1); osc.stop(audioCtx.currentTime + i * 0.1 + 0.5);
});
}
startScreen.addEventListener('click', () => {
startScreen.style.display = 'none';
container.style.display = 'block';
initAudio();
spawnInterval = setInterval(spawnEnemy, 1000);
itemInterval = setInterval(spawnItem, 8000);
});
const updateMousePos = (e) => {
mouseX = e.clientX || (e.touches && e.touches[0].clientX);
mouseY = e.clientY || (e.touches && e.touches[0].clientY);
if (!isGameOver && !isGameClear) {
slipper.style.left = mouseX + 'px';
slipper.style.top = mouseY + 'px';
}
};
window.addEventListener('mousemove', updateMousePos);
window.addEventListener('touchmove', updateMousePos, { passive: false });
// ----- スリッパの攻撃 -----
const smack = (e) => {
if (isGameOver || isGameClear || e.target === startScreen) return;
e.preventDefault();
slipper.classList.add('smack');
setTimeout(() => slipper.classList.remove('smack'), 100);
playSmackSound();
showOnomatopoeia(mouseX, mouseY, wordsSmack, true, '#ffeb3b');
for (let i = gameElements.length - 1; i >= 0; i--) {
const elInfo = gameElements[i];
if (!elInfo.active) continue;
const rect = elInfo.element.getBoundingClientRect();
const enemyX = rect.left + rect.width / 2;
const enemyY = rect.top + rect.height / 2;
const hitArea = elInfo.type === 'centipede' ? HIT_RADIUS * 1.5 : HIT_RADIUS;
const dist = Math.sqrt(Math.pow(mouseX - enemyX, 2) + Math.pow(mouseY - enemyY, 2));
if (dist < hitArea) killEnemy(elInfo, mouseX, mouseY);
}
};
container.addEventListener('mousedown', smack);
container.addEventListener('touchstart', smack, { passive: false });
// ----- アイテムの出現と挙動 -----
function spawnItem() {
if (isGameOver || isGameClear) return;
const itemTypes = ['shakkiri', 'sukkiri', 'noronoro'];
const type = itemTypes[Math.floor(Math.random() * itemTypes.length)];
const item = document.createElement('div');
item.className = `item ${type}`;
container.appendChild(item);
let x = Math.random() * (window.innerWidth - 100) + 50;
let y = Math.random() * (window.innerHeight - 100) + 50;
let vx = (Math.random() * 4 + 4) * (Math.random() > 0.5 ? 1 : -1);
let vy = (Math.random() * 4 + 4) * (Math.random() > 0.5 ? 1 : -1);
let active = true;
const moveItem = setInterval(() => {
if (!active || isGameOver || isGameClear) { clearInterval(moveItem); return; }
x += vx; y += vy;
if (x < 30 || x > window.innerWidth - 30) vx *= -1;
if (y < 30 || y > window.innerHeight - 30) vy *= -1;
item.style.left = x + 'px';
item.style.top = y + 'px';
const dist = Math.sqrt(Math.pow(x - mouseX, 2) + Math.pow(y - mouseY, 2));
if (dist < HIT_RADIUS + 20) {
active = false;
item.remove();
applyItemEffect(type, x, y);
}
}, 16);
setTimeout(() => {
if (active) {
active = false;
item.remove();
}
}, 5000);
}
function applyItemEffect(type, x, y) {
playPowerUpSound(type);
if (type === 'shakkiri') {
damage = Math.max(0, damage - 3);
damageDisplay.innerText = damage;
showOnomatopoeia(x, y, ["シャッキリ!", "命拾い!"], true, '#4caf50');
}
else if (type === 'sukkiri') {
showOnomatopoeia(window.innerWidth/2, window.innerHeight/2, ["スッッキリ!", "一掃!!"], true, '#f44336');
shakeScreen(true);
gameElements.forEach(elInfo => {
if (elInfo.active) killEnemy(elInfo, elInfo.element.getBoundingClientRect().left, elInfo.element.getBoundingClientRect().top, true);
});
}
else if (type === 'noronoro') {
showOnomatopoeia(x, y, ["ノロノロ〜", "スロー!"], true, '#2196f3');
slowMultiplier = 0.25;
container.style.backgroundColor = '#e3f2fd';
setTimeout(() => {
if(!isGameOver && !isGameClear) {
slowMultiplier = 1.0;
container.style.backgroundColor = '#fff';
}
}, 5000);
}
}
function checkLevelUp() {
if (score >= 100 && level === 1) {
level = 2;
levelDisplay.innerText = level;
const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain();
osc.type = 'triangle'; osc.frequency.value = 880; osc.connect(gain); gain.connect(audioCtx.destination);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime); gain.gain.linearRampToValueAtTime(0, audioCtx.currentTime + 0.3); osc.start(); osc.stop(audioCtx.currentTime + 0.3);
showOnomatopoeia(window.innerWidth/2, window.innerHeight/2, ["覚醒!!", "LEVEL 2"], true, '#ffd700');
container.classList.add('level-up-flash');
setTimeout(() => container.classList.remove('level-up-flash'), 1500);
clearInterval(spawnInterval);
spawnInterval = setInterval(spawnEnemy, 750);
}
}
function killEnemy(elInfo, smackX, smackY, isItemKill = false) {
elInfo.active = false;
elInfo.element.remove();
score += elInfo.type === 'centipede' ? 2 : 1;
scoreDisplay.innerText = score;
let words = wordsCockroach;
if(elInfo.type === 'fly') words = wordsFly;
if(elInfo.type === 'centipede') words = wordsCentipede;
if(!isItemKill) {
showOnomatopoeia(smackX + 30, smackY - 30, words, false);
shakeScreen(false);
}
if (score >= WIN_SCORE && !isGameClear && !isGameOver) {
triggerGameClear();
} else {
checkLevelUp();
}
}
function takeDamage(type, damageAmount) {
if (isGameOver || isGameClear) return;
damage += damageAmount;
damageDisplay.innerText = damage;
playDamageSound();
const scare = document.createElement('div');
scare.className = `enemy ${type} jumpscare`;
container.appendChild(scare);
setTimeout(() => scare.remove(), 800);
shakeScreen(true);
if (damage >= 10) triggerGameOver();
}
function spawnEnemy() {
if (isGameOver || isGameClear) return;
const enemy = document.createElement('div');
const rand = Math.random();
let type = 'cockroach';
if (rand < 0.15) type = 'centipede';
else if (rand < 0.5) type = 'fly';
enemy.className = `enemy ${type}`;
container.appendChild(enemy);
const elInfo = { element: enemy, type: type, active: true };
gameElements.push(elInfo);
if (type === 'centipede') {
const isLeftToRight = Math.random() > 0.5;
let x = isLeftToRight ? -200 : window.innerWidth + 200;
let y = Math.random() * (window.innerHeight - 300) + 150;
const baseY = y;
let tick = 0;
enemy.style.transform = `translate(-50%, -50%) ${isLeftToRight ? '' : 'scaleX(-1)'}`;
const crawl = setInterval(() => {
if (!elInfo.active || isGameOver || isGameClear) { clearInterval(crawl); return; }
tick += 0.1 * slowMultiplier;
const currentSpeed = (level >= 2 ? 8 : 4) * slowMultiplier;
const currentAmp = level >= 2 ? 140 : 60;
x += isLeftToRight ? currentSpeed : -currentSpeed;
y = baseY + Math.sin(tick) * currentAmp;
enemy.style.left = x + 'px';
enemy.style.top = y + 'px';
if ((isLeftToRight && x > window.innerWidth + 150) || (!isLeftToRight && x < -150)) {
elInfo.active = false; enemy.remove(); takeDamage('centipede', 2);
}
}, 16);
} else if (type === 'cockroach') {
let x = Math.random() * (window.innerWidth - 100) + 50;
let y = -100;
const baseSpeedY = Math.random() * 3 + 2;
let flyMode = false; let flyTimer = 0; let hasFlown = false; let flyAngle = Math.random() * Math.PI * 2;
const fall = setInterval(() => {
if (!elInfo.active || isGameOver || isGameClear) { clearInterval(fall); return; }
if (level >= 2 && !hasFlown && y > window.innerHeight * 0.3) {
flyMode = true; hasFlown = true; flyTimer = 2000;
}
if (flyMode) {
flyTimer -= 16 * slowMultiplier;
if (flyTimer <= 0) {
flyMode = false; enemy.style.boxShadow = "0 0 0 3px white, 0 0 0 6px black";
} else {
const flySpeed = 10 * slowMultiplier;
flyAngle += (Math.random() - 0.5) * 1.5;
x += Math.cos(flyAngle) * flySpeed; y += Math.sin(flyAngle) * flySpeed;
if (x < 50 || x > window.innerWidth - 50) flyAngle = Math.PI - flyAngle;
if (y < 50 || y > window.innerHeight - 50) flyAngle = -flyAngle;
enemy.style.boxShadow = "0 0 0 3px yellow, 0 0 0 6px black";
}
} else {
y += baseSpeedY * slowMultiplier;
let wobble = Math.sin(y / 20) * 15;
x += wobble * 0.1;
}
enemy.style.left = x + 'px'; enemy.style.top = y + 'px';
if (y > window.innerHeight + 100) {
elInfo.active = false; enemy.remove(); takeDamage('cockroach', 1);
}
}, 16);
} else {
let x = Math.random() * window.innerWidth;
let y = Math.random() * window.innerHeight;
let angle = Math.random() * Math.PI * 2;
let baseSpeed = 6;
let flyTime = 0;
const flyMove = setInterval(() => {
if (!elInfo.active || isGameOver || isGameClear) { clearInterval(flyMove); return; }
flyTime += 16 * slowMultiplier;
let currentSpeed = baseSpeed * slowMultiplier;
if (level >= 2 && flyTime >= 7000) {
currentSpeed = baseSpeed * 2.5 * slowMultiplier;
enemy.style.boxShadow = "0 0 0 3px red, 0 0 0 6px black";
}
angle += (Math.random() - 0.5) * 1.2;
x += Math.cos(angle) * currentSpeed; y += Math.sin(angle) * currentSpeed;
if (x < 50 || x > window.innerWidth - 50) angle = Math.PI - angle;
if (y < 50 || y > window.innerHeight - 50) angle = -angle;
enemy.style.left = x + 'px'; enemy.style.top = y + 'px';
if (flyTime >= 10000) {
elInfo.active = false; enemy.remove(); takeDamage('fly', 1);
}
}, 16);
}
}
function showOnomatopoeia(x, y, words, isSpecial, customColor) {
const text = document.createElement('div');
text.className = 'onomatopoeia';
text.innerText = words[Math.floor(Math.random() * words.length)];
if (isSpecial) {
text.style.fontSize = '120px';
text.style.color = customColor || '#ffeb3b';
text.style.zIndex = '110';
} else {
text.style.fontSize = '60px';
text.style.zIndex = '100';
}
text.style.left = x + 'px'; text.style.top = y + 'px';
container.appendChild(text);
setTimeout(() => text.remove(), 600);
}
function shakeScreen(isHeavy) {
container.classList.remove('shake', 'heavy-shake');
void container.offsetWidth;
container.classList.add(isHeavy ? 'heavy-shake' : 'shake');
}
// ----- ゲームオーバー・クリア処理 -----
function cleanUpGame() {
clearInterval(spawnInterval); clearInterval(itemInterval);
gameElements.forEach(elInfo => { if (elInfo.active) elInfo.element.remove(); });
document.body.style.cursor = 'default'; slipper.style.display = 'none';
container.style.backgroundColor = '#fff'; // 画面の色を戻す
}
function triggerGameOver() {
isGameOver = true;
cleanUpGame();
finalScoreDisplayOver.innerText = score;
gameOverScreen.style.display = 'flex';
if(audioCtx) audioCtx.suspend();
}
function triggerGameClear() {
isGameClear = true;
cleanUpGame();
finalScoreDisplayClear.innerText = score;
gameClearScreen.style.display = 'flex';
if(audioCtx) {
audioCtx.suspend();
setTimeout(() => { audioCtx.resume(); playClearSound(); }, 500); // 少し置いてクリアファンファーレ
}
}
</script>
</body>
</html>


■ 今日の学び

今回のゲーム実験を振り返って、一番の「学び」だったのは、

アイデアの種がどこに潜んでいるか分からない ということ、
そして AIとの会話の中でアイデアが形を変えていく面白さ でした。(^^)

最初は「オノマトペの手法を活かす」実験から始まりました。
AIが提案してくれた最初のオノマトペは……

“ドーン!!” “バキッ!” “ズバァァン!”

などなど、大迫力の格闘風!(=v=*) 敵もシンプルな黒い丸だけ。
でも、その黒丸の「敵」の影こそが、すべての始まりだったんですね。

そこから、

「敵といえばあの虫… 害虫退治ゲームにしよう!」

とアイデアが変化していきました。
そしてAIさんと相談しているうちに、

「じゃあ、やっぱりスリッパで叩くしかないよね!」 と、
武器も自然に決まっていったんです。

スリッパという具体的なアイテムが出てきたことで、
オノマトペも格闘風から「パンッ!」「バシッ!」といった
“スリッパ専用”の音に変わっていったのも面白いところです。

そこからはAIと一緒に盛り上がりすぎて、
ムカデさんを追加したり、アイテムを考えたり……。
気づけば、虫さんたちの「大行進」みたいな賑やかなゲームに。(^^;

苦手な方には、ちょっとドキドキする内容になってしまったかもしれません。
ごめんなさい m(_ _)m

でも、言葉の持つ力と、AIさんとの共同作業が重なって、
シンプルな丸い「敵」から始まった今回の実験は、
思いがけない方向へ広がっていく“オノマトペの大冒険”になりました。

■ 次回は、シュールな運搬ゲーム。

収穫したユズを、一輪運搬車(ねこぐるま)でコロコロ運びます。

中身をできるだけこぼさずにゴールを目指す、
不安定と不完全がそのままゲームになったような作品 です。

ちょっとした段差でユズが吹き飛んだり、
バランスを崩して全部こぼれたり……。

思わず「なんでそうなるの!?」とツッコミたくなる、
シュールでクセになる運搬アクションになっています。

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

次の実験はこちら  
  (吹き飛ぶユズ)
「ねこぐるまユズ運び」



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

~このブログについて~

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

🌈初めての方へ
🌱このブログについて
🌳実験一覧
🍃プライバシーポリシー


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

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

続きを読む