実験#30:物理と物語のシューティング(エクレアジター弾幕シューティング)

■30個目「エクレアジター弾幕シューティング

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

今回のゲームは、タイトルを見て「なんだろう?」と感じた人も多いと思います。

どんなゲームかというと…
以前 #23 で紹介したジターリングの動きと、AIが提案してくれた弾幕シューティングを組み合わせた作品です。

世界観は、これまで記事にしていないサウンドノベルゲーム「エクレア・スイーツ転生」から持ってきています。
(■今日の学び で少し紹介しますね)

操作は簡単で、マウス移動(スマホはスワイプ)で自機を操作します。

弾は自動で発射されます。
最大のコツは **「左右に素早く振る」こと!**

ジターリングの要領で自機を激しく動かすと回転スピードが上がり、
弾が大きく、そして強力に進化します。

SPゲージが100%になると「上矢印(↑)キー」でスペシャル攻撃が発動!
虹色の弾幕で画面を一掃できます。

タピオカ(回復)とバッテリー(SP満タン)を拾いながら、
全ステージクリアを目指してください。(^o^)o”

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

(^^*)いろいろゲーム作ってきたけど、誰も作ったことのないようなアイデアの組み合わせゲーム作りたいな。

(AI)これまでに50作品以上ものゲームを、HTML/JSという「限られた枠組み」の中で形にしてきたあなたの技術力と、「独自の視点」を掛け合わせれば、世界に一つだけの独創的なゲームが生まれる予感がします。

誰も見たことがない、けれどあなただからこそ作れる
**「異色の掛け合わせアイディア」**を提案します。

・「ジッターリング × 弾幕シューティング」
あの「ジッターリング」の独特な回転と慣性を、自機の攻撃手段にします。

(^_^)ジッターリング × 弾幕シューティングは楽しそうだね。何か具体的な案があるの?

(AI)お目が高い!ジッターリングの「制御が難しいけれど、勢いに乗ると手が付けられない」という独特の感覚は、シューティングゲームの「自機操作」と相性が抜群です。

・基本操作:リングの「回転」が攻撃力
・自機(中央のリング)の周りを、5つのビーズが回っています
・ショット:ビーズの回転が速いほど、弾の威力と連射速度が上がる

(^^*)カットインとかエクレア・スイーツ転生の小6ギャル風なのはどうかな?

(AI)いいですね!**「エクレア・スイーツ転生」風の小6ギャルマインド**に全振りするのは最高に尖っています。

ジッターリングのストイックな技術と、小6ギャルの「マじウける」「詰んだわw」という軽快なノリが合わさることで、一気にシュールで中毒性の高い世界観になりますね。

(*^^*)主人公は教頭校長をやっつけた後、エクレアを買いに行って転生します。ステージ4以降の世界観とボスはエクレア転生からの物語に沿って出しましょう。

・4:和風世界(蜘蛛の妖怪)
・5:洋風魔法の世界(魔女)
・6:中華の世界(仙人)
・7:SFの世界(AIロボ)
・8:お笑いピンクの世界(ピンク魔王)

ステージ8をクリアしたら、ステージ9として「エクレア物語の真のエンディング」が600字ほどの文章で背景スクロールします。文章が流れ終わったら END。

(AI)
エクレアへの執念から異世界転生しちゃうなんて、最高にぶっ飛んだ展開だねぇ。

ちなみに、エクレアってフランス語で「稲妻(Éclair)」って意味なんだけど、一口で稲妻のように素早く食べないとクリームが飛び出すから、なんて説があるんだよね。

ギャルが稲妻のように次々と次元を飛び越えていくスピード感に、驚くほどピッタリなチョイスだよ。

和風、洋風、中華、SF、そしてお笑いピンク……!
これだけ世界観が変わっていくと、もはや「超大作インディーゲーム」の風格だね。

そして、すべての戦いを終えた後の「600字のハッピーエンドスクロール」。
ゲームのエモさを極限まで高める素晴らしい演出だ。

■ 実際の動作

まずは、AIとの相談で作ってもらったゲームの初期状態です。
敵も小さくて迫力はいまいち。ボスもいません。
(画像:AIが最初に作った状態)

ここから調整を重ねて、最終的にはこんな感じになりました。 自機はジターリングです。

(画像:完成版のプレイ画面)

マウスに反応して左右に動きます。
小刻みに動かすことで強い攻撃が出ます。

(画像:1ステージボス)

SPが100%になると、↑ボタンでスペシャル攻撃が発動します。
“↑ボタン”は「アゲ」のイメージにも合っていて、ちょうどいい感じです。

(画像:スペシャル攻撃)


アイテムは2種類あります。

・タピオカドリンク(メンタル回復)
・バッテリー(SP満タン)
(画像:回復アイテム入手時)

 残機は3つで、メンタルが0%になると1つ減ります。
 残機が0になるとゲームオーバーです。

(画像:ゲームオーバーの状態)


 そして、各ステージには個性豊かなボスが待ち構えています。

(画像:セリフカットイン)





■ コードのポイント

今回の記事では、AI と相談しながら作った
「エクレアジター弾幕シューティング」の HTML コードを公開します。

ブラウザだけで動く、とてもシンプルなゲームです。
コードといっても難しい部分はありません。
貼り付けて保存するだけで動くので、気軽に試してみてください。

【遊ぶまでの手順】

1. PC の「メモ帳」(※Windows の場合)を開きます。
2. 下のコードをコピーして貼り付けます。
3. 「ekureajita.html」など、好きな名前で保存します。
4. 保存したファイルをブラウザで開くだけで遊べます。

【操作方法】

1. マウスの動きに合わせて左右に移動します。
2. SPゲージが100%のときに ↑ ボタンを押すとスペシャル攻撃が出ます。

▼ここからコード▼(クリックで開く)
<!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; background: #222; font-family: 'MS Gothic', sans-serif; }
canvas { display: block; background: #000; }
#ui {
position: absolute; top: 10px; left: 10px; color: #0ff;
font-weight: bold; text-shadow: 2px 2px #000; pointer-events: none;
font-size: 20px; line-height: 1.5; z-index: 10;
}
#cutin {
position: absolute; top: 25%; right: -600px;
background: linear-gradient(45deg, #ff69b4, #ff1493); color: white;
padding: 20px 40px; border-radius: 50px; font-size: 40px; font-weight: bold;
transition: right 0.2s cubic-bezier(0.18, 0.89, 0.32, 1.28);
border: 5px solid #fff; box-shadow: 0 0 40px #ff69b4; z-index: 100;
}
#warning {
position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%);
color: #ff0; font-size: 50px; font-weight: bold; text-shadow: 0 0 20px #f00;
display: none; text-align: center; z-index: 1000; letter-spacing: 5px; width: 100%;
}
#gameover {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
color: #ff0055; font-size: 80px; font-weight: bold; text-shadow: 0 0 20px #fff;
display: none; text-align: center; z-index: 1000; width: 100%;
}
#retryBtn {
font-size: 28px; color: #fff; text-shadow: none; margin-top: 30px;
cursor: pointer; padding: 15px 30px; background: #ff69b4; border-radius: 15px;
border: 3px solid #fff; display: inline-block; box-shadow: 0 5px 15px rgba(255,105,180,0.5);
transition: transform 0.1s; user-select: none;
}
#retryBtn:active { transform: scale(0.95); }
#bossHpContainer {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
width: 300px; height: 20px; background: #333; border: 3px solid #fff;
display: none; z-index: 10; border-radius: 10px; overflow: hidden;
}
#bossHpBar { width: 100%; height: 100%; background: #f00; transition: width 0.1s; }
</style>
</head>
<body>
<div id="ui">
LIVES: <span id="livesDisp">3</span> ❤️<br>
STAGE: <span id="stageDisp">1</span><br>
SCORE: <span id="score">0</span><br>
SPEED: <span id="speed">0</span><br>
MENTAL: <span id="hp">100</span>%<br>
SP GAUGE [↑]: <span id="gauge">0</span>%
</div>
<div id="bossHpContainer"><div id="bossHpBar"></div></div>
<div id="cutin">キタコレ!</div>
<div id="warning"></div>
<div id="gameover">
<div>詰んだわw</div>
<div id="retryBtn">👉 ワンチャンやり直す 👈</div>
</div>
<canvas id="gameCanvas"></canvas>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const scoreElement = document.getElementById('score');
const hpElement = document.getElementById('hp');
const gaugeElement = document.getElementById('gauge');
const stageElement = document.getElementById('stageDisp');
const livesElement = document.getElementById('livesDisp');
const cutin = document.getElementById('cutin');
const warning = document.getElementById('warning');
const gameoverScreen = document.getElementById('gameover');
const retryBtn = document.getElementById('retryBtn');
const bossHpContainer = document.getElementById('bossHpContainer');
const bossHpBar = document.getElementById('bossHpBar');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let lives = 3;
let currentStage = 1;
let score = 0;
let hp = 100;
let mouseX = canvas.width / 2;
let lastX = mouseX;
let ringRotation = 0;
let beadSpeed = 0.05;
let spGauge = 0;
let isSpecialMode = false;
let specialTimer = 0;
let isGameOver = false;
let shakeTime = 0;
let nextBossScore = 500;
let endTextY = 0;
const beads = [0, Math.PI*0.4, Math.PI*0.8, Math.PI*1.2, Math.PI*1.6];
const bullets = [];
const enemies = [];
const particles = [];
const enemyBullets = [];
const items = [];
let boss = null;
let bossPhase = false;
const endingText = [
"ピンク魔王をギャルバイブスで浄化した主人公。",
"彼女の目の前には、転生前からずっと食べたかった",
"幻の「アルティメット・エクレア」が輝いていた。",
"",
"「まじヤバい!これ絶対バズるやつじゃん!」",
"",
"一口かじると、口の中に広がるサクサクの生地と",
"濃厚なカスタードクリーム。",
"その瞬間、世界を覆っていた謎の次元の歪みが",
"エクレアの甘さに包まれて、みるみるうちに浄化されていく。",
"",
"和風世界の蜘蛛妖怪も、魔法使いのおばあちゃんも、",
"中華の仙人も、AIロボットも、みんなエクレアの虜に。",
"争いは消え、すべての次元が「エモい」平和な世界へと統合されたのだ。",
"",
"「てか、これ食べたら元の世界に戻れるんじゃね?」",
"",
"彼女の予感は的中し、次の瞬間、温かい光に包まれて……",
"気がつけば、いつもの放課後の教室に戻っていた。",
"手には、コンビニの袋に入った食べかけのエクレア。",
"",
"「……夢? ま、いっか。てかエクレアうま!」",
"",
"廊下では、担任の先生が「走るな!」と遠くで怒っている。",
"いつもと変わらない、少し退屈で平和な日常。",
"でも、彼女のジッターリングを回す手つきは、",
"以前よりもずっと、しなやかで力強かった。",
"",
"世界を救った小6ギャルの日常は、これからも続く。",
"マジで、チョベリグな毎日が!",
"",
"",
"THANK YOU FOR PLAYING!",
"",
"E N D"
];
// 復活時のセリフリスト
const reviveWords = [
"タイムタイム!今のノーカンで!",
"神回避ミスったわー、次は本気出す!",
"痛った!てか服汚れたし最悪!",
"残機消費とかマじウケるw",
"ギリ耐え!ウチらのバイブス舐めんな!"
];
function showCutin(text, duration = 2000) {
cutin.innerHTML = text;
cutin.style.right = "20px";
setTimeout(() => { if(!isGameOver) cutin.style.right = "-600px"; }, duration);
}
function shakeScreen(frames) { shakeTime = frames; }
function resetGame() {
lives = 3;
currentStage = 1;
nextBossScore = 500;
score = 0;
hp = 100;
spGauge = 0;
beadSpeed = 0.05;
isSpecialMode = false;
specialTimer = 0;
isGameOver = false;
bossPhase = false;
boss = null;
bullets.length = 0;
enemies.length = 0;
enemyBullets.length = 0;
particles.length = 0;
items.length = 0;
scoreElement.innerText = score;
hpElement.innerText = hp;
gaugeElement.innerText = spGauge;
stageElement.innerText = currentStage;
livesElement.innerText = lives;
document.getElementById('ui').style.display = "block";
gameoverScreen.style.display = "none";
bossHpContainer.style.display = "none";
warning.style.display = "none";
cutin.style.right = "-600px";
}
retryBtn.addEventListener('click', resetGame);
retryBtn.addEventListener('touchstart', (e) => { e.preventDefault(); resetGame(); }, {passive: false});
window.addEventListener('keydown', (e) => {
if (isGameOver && (e.key === "Enter" || e.key === " ")) {
resetGame();
} else if (e.key === "ArrowUp" && spGauge >= 100 && !isSpecialMode && !isGameOver && currentStage < 9) {
isSpecialMode = true; spGauge = 0; specialTimer = 180;
showCutin("バキバキ・アゲ!");
shakeScreen(10);
}
});
class Particle {
constructor(x, y, color, speed, size) {
this.x = x; this.y = y;
const angle = Math.random() * Math.PI * 2;
const vel = Math.random() * speed;
this.vx = Math.cos(angle) * vel;
this.vy = Math.sin(angle) * vel;
this.life = 1.0;
this.decay = Math.random() * 0.03 + 0.02;
this.color = color;
this.size = size;
}
update() { this.x += this.vx; this.y += this.vy; this.life -= this.decay; }
draw() {
ctx.globalAlpha = this.life;
ctx.fillStyle = this.color;
ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI*2); ctx.fill();
ctx.globalAlpha = 1.0;
}
}
function createExplosion(x, y, color, count, speed, size) {
if (particles.length > 200) return;
for(let i=0; i<count; i++) particles.push(new Particle(x, y, color, speed, size));
}
class Bullet {
constructor(x, y, angle, type) {
this.x = x; this.y = y;
this.type = type;
let baseSpeed = (type >= 2) ? 18 : 14;
this.vx = Math.cos(angle) * baseSpeed;
this.vy = Math.sin(angle) * baseSpeed;
if (type === 3) {
this.color = `hsl(${Math.random()*360}, 100%, 60%)`;
this.dmg = 5; this.size = 8;
} else if (type === 2) {
this.color = "#ffdd00";
this.dmg = 3; this.size = 6;
} else if (type === 1) {
this.color = "#ff69b4";
this.dmg = 2; this.size = 5;
} else {
this.color = "#fff";
this.dmg = 1; this.size = 4;
}
}
update() {
this.x += this.vx; this.y += this.vy;
if (this.type >= 2 && Math.random() < 0.15) {
particles.push(new Particle(this.x, this.y, this.color, 2, 2));
}
}
draw() {
ctx.fillStyle = this.color;
ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 0.4;
ctx.beginPath(); ctx.arc(this.x, this.y, this.size * 1.8, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1.0;
}
}
class Enemy {
constructor() {
this.x = Math.random() * (canvas.width - 60) + 30; this.y = -50;
// バランス調整:スピードの上昇率を緩和 (0.4 -> 0.25)
this.speed = 1.5 + (currentStage * 0.25) + Math.random();
this.maxHp = currentStage * 2;
this.hp = this.maxHp;
let chars = ["👾", "💀"];
if(currentStage === 4) chars = ["🏮", "👹"];
if(currentStage === 5) chars = ["🎃", "🧟"];
if(currentStage === 6) chars = ["🐼", "🏮"];
if(currentStage === 7) chars = ["🛸", "👾"];
if(currentStage === 8) chars = ["💖", "🦄"];
this.char = chars[Math.floor(Math.random()*chars.length)];
}
update() { this.y += this.speed; }
draw() {
ctx.font = "40px Arial";
ctx.fillText(this.char, this.x - 20, this.y + 15);
if(this.hp < this.maxHp) {
ctx.fillStyle = "#f00";
ctx.fillRect(this.x - 15, this.y - 30, 30 * (Math.max(0, this.hp) / this.maxHp), 4);
}
}
}
class EnemyBullet {
constructor(x, y, char, stage, vx = 0, vy = null) {
this.x = x; this.y = y;
this.char = char;
this.vx = vx;
// バランス調整:敵弾のスピード上昇率を緩和 (0.5 -> 0.4)
this.vy = vy !== null ? vy : (4 + (stage * 0.4) + Math.random() * 2);
}
update() { this.x += this.vx; this.y += this.vy; }
draw() { ctx.font = "30px Arial"; ctx.fillText(this.char, this.x - 15, this.y + 10); }
}
class Boss {
constructor(stage) {
this.stage = stage;
this.x = canvas.width / 2; this.y = 100;
this.vx = 4 + (stage * 0.4);
this.tick = 0;
const bossData = {
1: { char: "👨‍🏫", bullets: ["📝", "💯", "📐"], hp: 300 },
2: { char: "🦍", bullets: ["📢", "💢", "🫵"], hp: 600 },
3: { char: "👴👨‍🦲", bullets: ["🎤", "🍵", "📜"], hp: 1000 },
4: { char: "🕷️", bullets: ["🕸️", "🧶"], hp: 1500 },
5: { char: "🧙‍♀️", bullets: ["🦇", "🔮"], hp: 2000 },
6: { char: "🐉", bullets: ["🥟", "☯️"], hp: 2500 },
7: { char: "🤖", bullets: ["⚡", "💾"], hp: 3000 },
8: { char: "😈", bullets: ["💩", "💋"], hp: 4000 }
};
this.char = bossData[stage].char;
this.bulletChars = bossData[stage].bullets;
this.maxHp = bossData[stage].hp;
this.hp = this.maxHp;
bossHpContainer.style.display = "block";
}
update() {
this.x += this.vx;
if (this.x < 100 || this.x > canvas.width - 100) this.vx *= -1;
this.tick++;
if (this.stage === 3 || this.stage >= 5) {
if (this.tick % 45 === 0) {
let dx = mouseX - (this.x - 50);
let dy = (canvas.height - 100) - this.y;
let angle = Math.atan2(dy, dx);
enemyBullets.push(new EnemyBullet(this.x - 50, this.y, this.bulletChars[0], this.stage, Math.cos(angle)*7, Math.sin(angle)*7));
}
if (this.tick % 70 === 0) {
for(let i = -1; i <= 1; i++) {
enemyBullets.push(new EnemyBullet(this.x + 50, this.y, this.bulletChars[1], this.stage, i * 4, 6));
}
}
} else {
if (Math.random() < 0.05 + (this.stage * 0.01)) {
enemyBullets.push(new EnemyBullet(this.x, this.y, this.bulletChars[Math.floor(Math.random()*this.bulletChars.length)], this.stage));
}
}
}
draw() {
ctx.font = "80px Arial";
let drawX = this.x;
if (shakeTime > 0 && Math.random() < 0.5) drawX += (Math.random()*4-2);
if (this.stage === 3) {
ctx.fillText("👴", drawX - 70, this.y + 30);
ctx.fillText("👨‍🦲", drawX + 10, this.y + 30);
} else {
ctx.fillText(this.char, drawX - 40, this.y + 30);
}
bossHpBar.style.width = (Math.max(0, this.hp) / this.maxHp * 100) + "%";
}
}
class Item {
constructor(type) {
this.x = Math.random() * (canvas.width - 80) + 40;
this.y = -50;
this.speed = 2.5;
this.type = type;
this.char = type === 'heal' ? "🧋" : "🔋";
}
update() { this.y += this.speed; }
draw() {
ctx.font = "35px Arial";
let bounceY = this.y + Math.sin(Date.now() / 150) * 5;
ctx.fillText(this.char, this.x - 17, bounceY + 12);
ctx.fillStyle = this.type === 'heal' ? "rgba(255, 182, 193, 0.3)" : "rgba(0, 255, 0, 0.3)";
ctx.beginPath(); ctx.arc(this.x, bounceY, 25, 0, Math.PI*2); ctx.fill();
}
}
function triggerBossPhase() {
bossPhase = true;
let warningText = "";
let cutinText = "";
switch(currentStage) {
case 1: warningText = '⚠️担任襲来⚠️<br><span style="font-size:30px; color:#fff;">「宿題出せよ〜」</span>'; cutinText = "担任キタw"; break;
case 2: warningText = '⚠️学年主任⚠️<br><span style="font-size:30px; color:#fff;">「廊下を走るなァ!」</span>'; cutinText = "学年主任マじムリ!"; break;
case 3: warningText = '⚠️校長&教頭⚠️<br><span style="font-size:30px; color:#fff;">「静かになるまで…」</span>'; cutinText = "話長すぎバッド入る〜"; break;
case 4: warningText = '⚠️和風世界転生⚠️<br><span style="font-size:30px; color:#fff;">「シャアアア!」</span>'; cutinText = "エクレア買ったら<br>転生したんだけどw"; break;
case 5: warningText = '⚠️魔界の森⚠️<br><span style="font-size:30px; color:#fff;">「ヒッヒッヒ」</span>'; cutinText = "魔女とかビジュ古っw"; break;
case 6: warningText = '⚠️仙境の試練⚠️<br><span style="font-size:30px; color:#fff;">「我を越えてゆけ」</span>'; cutinText = "仙人?ウチらの方が上だし"; break;
case 7: warningText = '⚠️AI暴走中⚠️<br><span style="font-size:30px; color:#fff;">「ガガ…ピピッ」</span>'; cutinText = "ロボ?電源抜けばよくね?"; break;
case 8: warningText = '⚠️ピンク魔王降臨⚠️<br><span style="font-size:30px; color:#fff;">「ブハハハハ!」</span>'; cutinText = "魔王ダサw 浄化するわ!"; break;
}
warning.innerHTML = warningText;
warning.style.display = "block";
shakeScreen(40);
setTimeout(() => {
warning.style.display = "none";
boss = new Boss(currentStage);
showCutin(cutinText, 3000);
}, 3000);
}
function update() {
if (isGameOver) return;
if (currentStage === 9) {
endTextY -= 0.6;
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].life <= 0) particles.splice(i, 1);
}
return;
}
if (score >= nextBossScore && !bossPhase && !boss) {
triggerBossPhase();
}
let dx = Math.abs(mouseX - lastX);
let accel = isSpecialMode ? 0.015 : dx * 0.0006;
beadSpeed = Math.max(0.05, Math.min(isSpecialMode ? 0.9 : 0.45, beadSpeed + accel - 0.0015));
lastX = mouseX;
ringRotation += beadSpeed;
if (isSpecialMode) {
specialTimer--;
if (specialTimer <= 0) {
isSpecialMode = false;
showCutin(["秒で冷めたわ", "パケ死んだw", "ハイ、解散〜"][Math.floor(Math.random()*3)]);
}
} else if (spGauge < 100) spGauge += 0.2;
if (!bossPhase && Math.random() < 0.006) {
items.push(new Item(Math.random() < 0.5 ? 'heal' : 'charge'));
}
for (let i = items.length - 1; i >= 0; i--) {
let item = items[i];
item.update();
if (Math.hypot(item.x - mouseX, item.y - (canvas.height - 100)) < 45) {
if (item.type === 'heal') {
hp = Math.min(100, hp + 30);
showCutin("タピって復活🧋✨", 2000);
createExplosion(item.x, item.y, "#ffb6c1", 20, 5, 4);
} else if (item.type === 'charge') {
spGauge = 100;
showCutin("パケ大盛り!即アゲ🔋⚡", 2000);
createExplosion(item.x, item.y, "#0f0", 20, 5, 4);
}
items.splice(i, 1);
} else if (item.y > canvas.height) {
items.splice(i, 1);
}
}
gaugeElement.innerText = Math.floor(spGauge);
hpElement.innerText = Math.floor(hp);
document.getElementById('speed').innerText = Math.floor(beadSpeed * 1000);
let bulletType = 0;
if (isSpecialMode) bulletType = 3;
else if (beadSpeed > 0.35) bulletType = 2;
else if (beadSpeed > 0.2) bulletType = 1;
if (isSpecialMode && specialTimer % 2 === 0) {
for(let i=0; i<12; i++) bullets.push(new Bullet(mouseX, canvas.height-100, (i/12)*Math.PI*2, bulletType));
} else if (Math.random() < beadSpeed * 2.5) {
bullets.push(new Bullet(mouseX, canvas.height-100, -Math.PI/2 + (Math.random()*0.2-0.1), bulletType));
}
let spawnRate = 0.04 + (currentStage * 0.015);
if (!bossPhase && enemies.length < 15 && Math.random() < spawnRate) enemies.push(new Enemy());
for (let i = bullets.length - 1; i >= 0; i--) {
bullets[i].update();
if (bullets[i].y < -50 || bullets[i].x < -50 || bullets[i].x > canvas.width + 50) {
bullets.splice(i, 1);
}
}
for (let ebi = enemyBullets.length - 1; ebi >= 0; ebi--) {
let eb = enemyBullets[ebi];
eb.update();
if (Math.hypot(eb.x - mouseX, eb.y - (canvas.height - 100)) < 40) {
enemyBullets.splice(ebi, 1);
hp -= 10; shakeScreen(10); createExplosion(eb.x, eb.y, "#f00", 10, 5, 3);
} else if (eb.y > canvas.height || eb.x < -50 || eb.x > canvas.width + 50) {
enemyBullets.splice(ebi, 1);
}
}
for (let ei = enemies.length - 1; ei >= 0; ei--) {
let e = enemies[ei];
e.update();
let isHit = false;
for (let bi = bullets.length - 1; bi >= 0; bi--) {
let b = bullets[bi];
if (Math.hypot(e.x - b.x, e.y - b.y) < 30) {
e.hp -= b.dmg;
createExplosion(b.x, b.y, b.color, 5, 3, 2);
bullets.splice(bi, 1);
isHit = true;
break;
}
}
if (e.hp <= 0) {
createExplosion(e.x, e.y, "#ff69b4", 15, 6, 4);
enemies.splice(ei, 1);
score += 10; scoreElement.innerText = score;
} else if (e.y > canvas.height) {
enemies.splice(ei, 1);
hp -= 15; shakeScreen(10);
}
}
if (boss) {
boss.update();
for (let bi = bullets.length - 1; bi >= 0; bi--) {
let b = bullets[bi];
let hitRadius = boss.stage >= 3 ? 80 : 60;
if (Math.hypot(boss.x - b.x, boss.y - b.y) < hitRadius) {
boss.hp -= b.dmg;
bullets.splice(bi, 1);
createExplosion(b.x, b.y, b.color, 5, 4, 2);
if (b.dmg >= 3) shakeScreen(2);
}
}
if (boss.hp <= 0) {
createExplosion(boss.x, boss.y, "#fff", 150, 20, 5);
shakeScreen(40);
score += 1000 + (currentStage * 500); scoreElement.innerText = score;
bossHpContainer.style.display = "none";
boss = null; bossPhase = false;
if (currentStage === 8) {
currentStage = 9;
document.getElementById('ui').style.display = "none";
bullets.length = 0; enemies.length = 0; enemyBullets.length = 0; items.length = 0;
endTextY = canvas.height + 50;
} else {
currentStage++;
nextBossScore = score + 500 + (currentStage * 300);
stageElement.innerText = currentStage;
showCutin(`STAGE ${currentStage} 突入!マじ神!`, 3000);
}
}
}
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].life <= 0) particles.splice(i, 1);
}
// --- 残機システム(復活処理) ---
if (hp <= 0) {
if (lives > 0) {
lives--;
livesElement.innerText = lives;
hp = 100;
// 画面内の敵と弾を一掃して安全を確保
enemies.length = 0;
enemyBullets.length = 0;
bullets.length = 0;
shakeScreen(20);
// ランダムな復活セリフを表示
showCutin(reviveWords[Math.floor(Math.random() * reviveWords.length)], 3000);
} else {
hp = 0;
isGameOver = true;
gameoverScreen.style.display = "block";
bossHpContainer.style.display = "none";
}
}
}
function drawBackground() {
const bgColors = {
1: "#1a3320", 2: "#200020", 3: "#000b1a", 4: "#2a0000",
5: "#1a0033", 6: "#331a00", 7: "#002200", 8: "#ffb6c1", 9: "#000000"
};
ctx.fillStyle = bgColors[currentStage] || "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (currentStage === 7) {
ctx.fillStyle = "rgba(0, 255, 0, 0.1)";
for(let i=0; i<canvas.width; i+=40) ctx.fillRect(i, 0, 2, canvas.height);
} else if (currentStage === 8) {
ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
for(let i=0; i<20; i++) ctx.fillText("🤍", Math.random()*canvas.width, Math.random()*canvas.height);
} else if (currentStage === 9 || currentStage === 3 || currentStage === 5) {
ctx.fillStyle = "rgba(255, 255, 255, 0.5)";
for(let i=0; i<40; i++) ctx.fillRect(Math.random()*canvas.width, Math.random()*canvas.height, 2, 2);
}
}
function draw() {
drawBackground();
if (currentStage === 9) {
ctx.fillStyle = "#fff";
ctx.font = "24px 'MS Gothic', sans-serif";
ctx.textAlign = "center";
endingText.forEach((line, i) => {
ctx.fillText(line, canvas.width / 2, endTextY + i * 40);
});
ctx.textAlign = "left";
particles.forEach(p => p.draw());
requestAnimationFrame(() => { update(); draw(); });
return;
}
ctx.save();
if (shakeTime > 0) {
ctx.translate(Math.random()*16 - 8, Math.random()*16 - 8);
shakeTime--;
}
let ringColor = isSpecialMode ? `hsl(${Date.now()%360}, 100%, 70%)` :
beadSpeed > 0.35 ? "#ffdd00" :
beadSpeed > 0.2 ? "#ff1493" : "#ff69b4";
ctx.strokeStyle = ringColor;
ctx.shadowBlur = isSpecialMode ? 20 : 10;
ctx.shadowColor = ringColor;
ctx.lineWidth = isSpecialMode ? 12 : beadSpeed > 0.35 ? 8 : 5;
ctx.beginPath();
ctx.arc(mouseX, canvas.height - 100, 45, 0, Math.PI * 2);
ctx.stroke();
beads.forEach(bAngle => {
const x = mouseX + Math.cos(ringRotation + bAngle) * 45;
const y = (canvas.height - 100) + Math.sin(ringRotation + bAngle) * 45;
ctx.fillStyle = isSpecialMode ? "#fff" : ringColor;
ctx.beginPath(); ctx.arc(x, y, isSpecialMode ? 10 : 8, 0, Math.PI * 2); ctx.fill();
});
ctx.shadowBlur = 0;
items.forEach(item => item.draw());
bullets.forEach(b => b.draw());
enemyBullets.forEach(eb => eb.draw());
enemies.forEach(e => e.draw());
particles.forEach(p => p.draw());
if (boss) boss.draw();
ctx.restore();
requestAnimationFrame(() => { update(); draw(); });
}
window.addEventListener('mousemove', (e) => { mouseX = e.clientX; });
window.addEventListener('touchmove', (e) => { mouseX = e.touches[0].clientX; });
draw();
</script>
</body>
</html>


■ 今日の学び

「エクレアスイーツ転生」はサウンドノベル形式のゲームで、
シリーズの初期に AI と一緒に作った作品です。

「メモ帳アプリでサウンドノベルは作れる?」という質問から始まり、
AI と相談しながら少しずつ形にしていきました。

お菓子のエクレアが関わっているところやストーリー展開は、
ほとんど AI が考えたものです。

私は「こんな雰囲気でお願い」「もう少しリアルに」など、
ニュアンスの相談を重ねながら作っていきました。(^^;)

ただ、相談するたびに話が変わったり、
仕上がりが安定しなかったこともあって、
記事にはせずに眠っていたゲームのひとつです。

5つの世界から転生先を選び、それぞれの世界で
ハッピーエンドを目指す流れになっています。
今回は、その冒頭の一部分を紹介します。(*^^)

(画像:エクレア・スイーツ転生より)

今回のゲームでは、この主人公の“個性の強さ”が
大きく影響して、全体の雰囲気がより引き立ちました。

全ステージクリアで、ハッピーエンドのストーリー文が流れます。
ぜひ楽しんでみてください。(*^_^)/

■ 次回は、妖怪が主人公

 次回は、ちょっと不思議で、どこか愛嬌のある“妖怪”が主人公です。

 思わず力が入るけど、ほのぼのバトル。
 不思議な世界観をゲームにしようと、AIと相談しながら作りました。

 次回もお楽しみに。(*^v^*)ノシ

次の実験はこちら(釣りからはじまる力比べ):「カッパ綱引き」



 ゲーム作りが楽しくて、どんどん作品が増えています(*^v^*)
 もし応援していただけたら、とても励みになります。

~このブログについて~

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

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


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

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

続きを読む