<!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;
padding: 0;
background-color: #111;
display: flex;
flex-direction: column; /* 縦並びを基本にする */
justify-content: center;
align-items: center;
height: 100vh;
height: 100dvh; /* スマホのアドレスバー対策 */
font-family: 'Courier New', Courier, monospace;
overflow: hidden;
/* スワイプによる更新などを防止 */
touch-action: none;
}
/* ゲーム画面全体を囲み、レイアウトを調整するラッパー */
#gameWrapper {
flex: 1; /* 残りの空間をすべて使う */
width: 100%;
display: flex;
justify-content: center;
align-items: center;
min-height: 0; /* はみ出し防止に必須 */
padding: 10px;
box-sizing: border-box;
}
#gameContainer {
position: relative;
/* アスペクト比 4:3 を維持しつつ親要素内で最大化(はみ出さない) */
aspect-ratio: 4 / 3;
width: 800px; /* 基準幅。max-width/heightで自動縮小される */
max-width: 100%;
max-height: 100%;
}
canvas {
width: 100%;
height: 100%;
background-color: #2ca3e6; /* 明るい海の色に変更 */
/* ドット絵をくっきり表示させるための設定 */
image-rendering: -moz-crisp-edges;
image-rendering: -webkit-crisp-edges;
image-rendering: pixelated;
image-rendering: crisp-edges;
box-shadow: 0 0 20px rgba(0,0,0,0.8);
}
.ui-screen {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
text-shadow: 3px 3px 0 #000;
background: rgba(0, 0, 0, 0.4);
}
.hidden {
display: none !important;
}
h1 {
font-size: 4rem;
margin: 0 0 20px 0;
color: #e43b44;
letter-spacing: 5px;
}
p {
font-size: 1.5rem;
margin: 10px 0;
animation: blink 1.5s infinite;
}
.instructions {
font-size: 1.2rem;
color: #9badb7;
text-align: center;
line-height: 1.8;
margin-top: 40px;
animation: none;
}
.score-text {
font-size: 2.5rem;
color: #ffee83;
animation: none;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
/* --- タッチ操作用UI --- */
.controls-wrapper {
flex: 0 0 auto;
width: 100%;
max-width: 600px;
display: flex;
justify-content: space-between;
align-items: flex-end;
padding: 10px 20px;
/* iPhoneの下部バー(ホームインジケータ)被り対策 */
padding-bottom: calc(10px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.controls-wrapper.hidden {
display: none !important;
}
.dpad {
display: flex;
flex-direction: column;
align-items: center;
-webkit-touch-callout: none;
}
.dpad-row {
display: flex;
}
.dpad-btn {
width: 50px;
height: 50px;
margin: 2px;
background: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(255, 255, 255, 0.5);
border-radius: 10px;
color: white;
font-size: 20px;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
touch-action: none;
cursor: pointer;
}
.dpad-center {
width: 50px;
height: 50px;
margin: 2px;
}
.action-pad {
display: flex;
align-items: flex-end;
pointer-events: auto;
}
.action-btn {
width: 70px;
height: 70px;
margin-bottom: 10px;
background: rgba(228, 59, 68, 0.5);
border: 2px solid rgba(228, 59, 68, 0.8);
border-radius: 50%;
color: white;
font-size: 20px;
font-weight: bold;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
touch-action: none;
cursor: pointer;
}
.dpad-btn.active {
background: rgba(255, 255, 255, 0.6);
border-color: white;
}
.action-btn.active {
background: rgba(228, 59, 68, 0.9);
border-color: white;
transform: scale(0.95);
}
/* スマホ向けのサイズ調整 */
@media (max-width: 600px) {
.dpad-btn, .dpad-center {
width: 45px;
height: 45px;
font-size: 16px;
}
.action-btn {
width: 65px;
height: 65px;
font-size: 16px;
}
h1 { font-size: 2.2rem; }
.score-text { font-size: 1.5rem; }
.instructions { font-size: 1rem; margin-top: 20px; }
.controls-wrapper {
padding: 10px 10px;
padding-bottom: calc(10px + env(safe-area-inset-bottom));
}
}
/* スマホ横画面対策: ゲーム画面と重ならないように左右に配置 */
@media (orientation: landscape) and (max-height: 500px) {
body {
flex-direction: row;
}
#gameWrapper {
padding: 10px; /* 左右の余白は固定せず自然に任せる */
}
.controls-wrapper {
position: absolute;
bottom: 10px;
left: 0;
width: 100%;
max-width: none;
padding: 0;
/* スマホのノッチ(カメラの出っ張り)避け */
padding-left: calc(20px + env(safe-area-inset-left));
padding-right: calc(20px + env(safe-area-inset-right));
padding-bottom: calc(10px + env(safe-area-inset-bottom));
pointer-events: none; /* コンテナはタッチを透過 */
}
.dpad, .action-pad {
pointer-events: auto; /* ボタン部分はタッチを受け付ける */
}
}
</style>
</head>
<body>
<div id="gameWrapper">
<div id="gameContainer">
<!-- 論理サイズは400x300に設定し、CSSで800x600に拡大してレトロ感を出す -->
<canvas id="gameCanvas" width="400" height="300"></canvas>
<div id="uiStart" class="ui-screen">
<h1>メンダコ・ブレイド</h1>
<p>SPACEキーでスタート</p>
<div class="instructions">
移動: WASD または 矢印キー<br>
攻撃: SPACEキー
</div>
</div>
<div id="uiGameOver" class="ui-screen hidden">
<h1>GAME OVER</h1>
<div class="score-text">SCORE: <span id="finalScore">0</span></div>
<p>SPACEキーでリトライ</p>
</div>
</div>
</div>
<!-- 仮想コントローラー (ゲーム画面と重ならないよう分離) -->
<div id="onScreenControls" class="controls-wrapper hidden">
<div class="dpad">
<div class="dpad-row">
<button class="dpad-btn up" data-key="ArrowUp">▲</button>
</div>
<div class="dpad-row">
<button class="dpad-btn left" data-key="ArrowLeft">◀</button>
<div class="dpad-center"></div>
<button class="dpad-btn right" data-key="ArrowRight">▶</button>
</div>
<div class="dpad-row">
<button class="dpad-btn down" data-key="ArrowDown">▼</button>
</div>
</div>
<div class="action-pad">
<button class="action-btn attack" data-key=" ">剣</button>
</div>
</div>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const uiStart = document.getElementById('uiStart');
const uiGameOver = document.getElementById('uiGameOver');
const finalScoreEl = document.getElementById('finalScore');
// --- パレット (レトロ風カラー) ---
const palette = {
0: null,
1: '#e43b44', // 赤 (メンダコ)
2: '#ffffff', // 白
3: '#181425', // 黒 (縁取り・影)
4: '#ffee83', // 黄色 (剣の柄)
5: '#9badb7', // 銀色 (剣の刃)
6: '#2ce8f5', // 水色 (魚)
7: '#0099db', // 青 (魚の影)
};
// --- ピクセルアート配列 ---
// メンダコ
const mendakoArt = [
[0,0,0,0,0,1,1,1,1,0,0,0,0,0],
[0,1,1,0,0,1,1,1,1,0,0,1,1,0], // 耳(ヒレ)を追加
[0,1,1,1,1,1,1,1,1,1,1,1,1,0],
[0,0,1,1,1,1,1,1,1,1,1,1,0,0],
[0,0,1,1,3,3,1,1,3,3,1,1,0,0],
[0,0,1,1,3,3,1,1,3,3,1,1,0,0],
[0,1,1,1,1,1,1,1,1,1,1,1,1,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,1,0,1,0,0,1,1,0,0,1,0,1,0],
[0,1,1,0,0,0,1,1,0,0,0,1,1,0]
];
// 剣
const swordArt = [
[0,0,0,0,0,0,0,5,5],
[0,0,0,0,0,0,5,5,5],
[0,0,0,0,0,5,5,5,0],
[0,0,0,0,5,5,5,0,0],
[0,0,0,5,5,5,0,0,0],
[0,0,5,5,5,0,0,0,0],
[0,5,5,5,0,0,0,0,0],
[5,5,4,4,0,0,0,0,0],
[5,4,4,3,0,0,0,0,0],
[0,5,3,3,0,0,0,0,0]
];
// 魚(敵)
const fishArt = [
[0,0,0,0,0,0,6,6,0,0,0,0],
[0,0,0,0,0,6,6,6,6,0,0,0],
[0,0,0,0,6,6,6,6,6,6,0,0],
[6,0,0,6,6,6,3,3,6,6,6,0],
[6,6,6,6,6,6,3,3,6,6,6,0],
[6,6,6,6,6,6,6,6,6,6,0,0],
[6,0,0,6,6,6,6,6,6,0,0,0],
[0,0,0,0,0,6,7,0,0,0,0,0]
];
// ハート(アイテム)
const heartArt = [
[0,1,1,0,1,1,0],
[1,1,1,1,1,1,1],
[1,1,1,1,1,1,1],
[0,1,1,1,1,1,0],
[0,0,1,1,1,0,0],
[0,0,0,1,0,0,0]
];
const scale = 2; // 1ピクセルを2x2で描画
// アート描画関数
function drawArt(ctx, art, x, y, scale) {
for (let r = 0; r < art.length; r++) {
for (let c = 0; c < art[r].length; c++) {
let colorIndex = art[r][c];
if (colorIndex !== 0) {
ctx.fillStyle = palette[colorIndex];
ctx.fillRect(x + c * scale, y + r * scale, scale, scale);
}
}
}
}
// --- ゲームクラス ---
class Player {
constructor() {
this.w = 14 * scale; // ヒレを追加した分、横幅を広げる
this.h = 10 * scale;
this.x = 50;
this.y = 150;
this.speed = 2.5;
this.hp = 3;
this.maxHp = 3;
this.isAttacking = false;
this.attackTimer = 0;
this.attackDuration = 12; // 攻撃のフレーム数
this.isInvincible = false;
this.invincibleTimer = 0;
this.invincibleDuration = 60; // 被ダメージ時の無敵フレーム数
this.swordScale = 1.0; // 剣のサイズ
}
powerUp() {
// HP回復と剣の巨大化
if (this.hp < this.maxHp) this.hp++;
if (this.swordScale < 2.5) {
this.swordScale += 0.5;
}
}
update(keys) {
// 移動
if (keys['ArrowUp'] || keys['w']) this.y -= this.speed;
if (keys['ArrowDown'] || keys['s']) this.y += this.speed;
if (keys['ArrowLeft'] || keys['a']) this.x -= this.speed;
if (keys['ArrowRight'] || keys['d']) this.x += this.speed;
// 画面端の制限
if (this.x < 0) this.x = 0;
if (this.y < 20) this.y = 20; // UI領域を避ける
if (this.x > canvas.width - this.w) this.x = canvas.width - this.w;
if (this.y > canvas.height - this.h - 30) this.y = canvas.height - this.h - 30; // 30は海底の高さ
// 攻撃
if (keys[' '] && !this.isAttacking && this.attackTimer === 0) {
this.isAttacking = true;
this.attackTimer = this.attackDuration;
}
if (this.attackTimer > 0) {
this.attackTimer--;
if (this.attackTimer === 0) {
this.isAttacking = false;
}
}
// 無敵時間
if (this.invincibleTimer > 0) {
this.invincibleTimer--;
if (this.invincibleTimer === 0) {
this.isInvincible = false;
}
}
}
draw(ctx) {
// 無敵時の点滅エフェクト
if (this.isInvincible && Math.floor(Date.now() / 100) % 2 === 0) {
// 描画スキップで点滅を表現
} else {
drawArt(ctx, mendakoArt, this.x, this.y, scale);
}
// 剣の描画とアニメーション
ctx.save();
// 剣の回転軸をメンダコの右側中央あたりに設定
ctx.translate(this.x + this.w - 4, this.y + this.h / 2);
let angle = -Math.PI / 4; // 待機時の角度 (上向き)
if (this.attackTimer > 0) {
// 攻撃時は振り下ろすアニメーション
let progress = 1 - (this.attackTimer / this.attackDuration);
angle = -Math.PI / 4 + progress * (Math.PI * 0.8);
}
ctx.rotate(angle);
drawArt(ctx, swordArt, 0, -8 * scale * this.swordScale, scale * this.swordScale);
ctx.restore();
// デバッグ用: 攻撃判定の可視化
/*
if (this.isAttacking) {
let hHit = this.getAttackHitbox();
ctx.strokeStyle = 'red';
ctx.strokeRect(hHit.x, hHit.y, hHit.w, hHit.h);
}
*/
}
getHitbox() {
// 見た目より少し小さめの当たり判定
return { x: this.x + 4, y: this.y + 4, w: this.w - 8, h: this.h - 8 };
}
getAttackHitbox() {
if (!this.isAttacking) return null;
// メンダコの右側に攻撃判定を発生させる
return {
x: this.x + this.w - 10,
y: this.y - 15 * this.swordScale,
w: 35 * this.swordScale,
h: (this.h + 30) * this.swordScale
};
}
}
class Enemy {
constructor(speedMultiplier) {
this.w = 12 * scale;
this.h = 8 * scale;
this.x = canvas.width;
this.y = Math.random() * (canvas.height - this.h - 40 - 30) + 30; // 海底を避ける
this.speed = (1.5 + Math.random() * 1.5) * speedMultiplier;
this.baseY = this.y;
this.waveTimer = Math.random() * Math.PI * 2;
this.waveSpeed = 0.03 + Math.random() * 0.05;
this.waveAmp = 10 + Math.random() * 20;
this.markedForDeletion = false;
}
update() {
this.x -= this.speed;
this.waveTimer += this.waveSpeed;
this.y = this.baseY + Math.sin(this.waveTimer) * this.waveAmp;
if (this.x < -this.w) {
this.markedForDeletion = true;
}
}
draw(ctx) {
drawArt(ctx, fishArt, this.x, this.y, scale);
}
getHitbox() {
return { x: this.x + 2, y: this.y + 2, w: this.w - 4, h: this.h - 4 };
}
}
class Item {
constructor() {
this.w = 7 * scale;
this.h = 6 * scale;
this.x = canvas.width;
this.y = Math.random() * (canvas.height - this.h - 60 - 30) + 30; // 海底を避ける
this.speed = 1.5;
this.markedForDeletion = false;
this.waveTimer = 0;
}
update() {
this.x -= this.speed;
this.waveTimer += 0.05;
this.y += Math.sin(this.waveTimer) * 0.5; // ふわふわ揺れる
if (this.x < -this.w) {
this.markedForDeletion = true;
}
}
draw(ctx) {
// 少し光る演出
ctx.shadowBlur = 10;
ctx.shadowColor = '#ffb8b8';
drawArt(ctx, heartArt, this.x, this.y, scale);
ctx.shadowBlur = 0;
}
getHitbox() {
return { x: this.x, y: this.y, w: this.w, h: this.h };
}
}
class Particle {
constructor(x, y, color) {
this.x = x;
this.y = y;
this.vx = (Math.random() - 0.5) * 8;
this.vy = (Math.random() - 0.5) * 8;
this.life = 20 + Math.random() * 10;
this.color = color;
this.size = scale * (1 + Math.random());
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life--;
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.globalAlpha = Math.max(0, this.life / 30);
ctx.fillRect(this.x, this.y, this.size, this.size);
ctx.globalAlpha = 1;
}
}
class Bubble {
constructor() {
this.x = Math.random() * canvas.width;
this.y = canvas.height - 30; // 初期位置も海底から
this.speed = 0.5 + Math.random();
this.size = scale + Math.random() * scale * 2;
}
update() {
// 上に昇りつつ、スクロールに合わせて左に流れる
this.y -= this.speed;
this.x -= (1.5 + Math.sin(this.y * 0.02) * 0.5);
if (this.y < -10 || this.x < -10) {
this.y = canvas.height - 30; // 海底から湧き出る
this.x = canvas.width + Math.random() * 50;
}
}
draw(ctx) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.4)'; // 背景が明るくなったので泡を少し濃く
ctx.lineWidth = 1;
ctx.strokeRect(this.x, this.y, this.size, this.size);
}
}
// --- ゲーム状態と変数 ---
let gameState = 'start'; // 'start', 'playing', 'gameover'
let player;
let enemies = [];
let particles = [];
let bubbles = [];
let items = [];
let score = 0;
let killCount = 0;
let frameCount = 0;
let keys = {};
let bgScroll = 0; // スクロール位置
// --- 入力処理 ---
window.addEventListener('keydown', (e) => {
keys[e.key] = true;
// スペースキー押下時の状態遷移
if (e.key === ' ' || e.code === 'Space') {
if (gameState === 'start') {
startGame();
} else if (gameState === 'gameover') {
// 少しディレイを入れて連打防止
if (frameCount > 30) {
startGame();
}
}
}
});
window.addEventListener('keyup', (e) => {
keys[e.key] = false;
});
// --- タッチ操作対応 ---
const isTouchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
if (isTouchDevice) {
document.getElementById('onScreenControls').classList.remove('hidden');
// UIの文言をタッチ端末向けに変更
document.querySelector('#uiStart p').innerText = '画面タップでスタート';
document.querySelector('#uiStart .instructions').innerHTML = '左下: 移動ボタン<br>右下: 攻撃ボタン';
document.querySelector('#uiGameOver p').innerText = '画面タップでリトライ';
}
// タップでスタート・リトライ
function handleStartTap(e) {
// ボタン自体をタップした時は反応させない
if (e.target.closest('.on-screen-controls')) return;
e.preventDefault();
if (gameState === 'start') {
startGame();
} else if (gameState === 'gameover') {
if (frameCount > 30) {
startGame();
}
}
}
uiStart.addEventListener('touchstart', handleStartTap);
uiStart.addEventListener('click', handleStartTap);
uiGameOver.addEventListener('touchstart', handleStartTap);
uiGameOver.addEventListener('click', handleStartTap);
// 仮想ボタンのイベント処理
const touchBtns = document.querySelectorAll('.dpad-btn, .action-btn');
touchBtns.forEach(btn => {
const key = btn.getAttribute('data-key');
const pressBtn = (e) => {
e.preventDefault();
keys[key] = true;
btn.classList.add('active');
};
const releaseBtn = (e) => {
e.preventDefault();
keys[key] = false;
btn.classList.remove('active');
};
btn.addEventListener('touchstart', pressBtn);
btn.addEventListener('mousedown', pressBtn); // PCのマウスでもテスト可能に
btn.addEventListener('touchend', releaseBtn);
btn.addEventListener('touchcancel', releaseBtn);
btn.addEventListener('mouseup', releaseBtn);
btn.addEventListener('mouseleave', releaseBtn);
});
// 衝突判定ヘルパー
function checkCollision(rect1, rect2) {
return rect1.x < rect2.x + rect2.w &&
rect1.x + rect1.w > rect2.x &&
rect1.y < rect2.y + rect2.h &&
rect1.y + rect1.h > rect2.y;
}
// パーティクル生成
function createExplosion(x, y, colorArr) {
for (let i = 0; i < 15; i++) {
let color = colorArr[Math.floor(Math.random() * colorArr.length)];
particles.push(new Particle(x, y, color));
}
}
// 進行度に応じた背景色を取得 (深く進むほど暗くなる)
function getBackgroundColor(kills) {
// 100キルで一番暗い深海色になるように補間
let ratio = Math.min(kills / 100, 1);
let r = Math.floor(44 + (5 - 44) * ratio); // 44 -> 5
let g = Math.floor(163 + (10 - 163) * ratio); // 163 -> 10
let b = Math.floor(230 + (31 - 230) * ratio); // 230 -> 31
return `rgb(${r}, ${g}, ${b})`;
}
// 遠景(岩山)の描画
function drawBackground(ctx) {
let ratio = Math.min(killCount / 100, 1);
let r = Math.floor(34 + (3 - 34) * ratio);
let g = Math.floor(143 + (6 - 143) * ratio);
let b = Math.floor(210 + (21 - 210) * ratio);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
let patternWidth = 150;
let offset = (bgScroll * 0.3) % patternWidth; // 遠景はゆっくりスクロール
ctx.beginPath();
ctx.moveTo(0, canvas.height - 30);
for (let x = -offset; x <= canvas.width + patternWidth; x += patternWidth) {
ctx.lineTo(x, canvas.height - 30);
ctx.lineTo(x + 50, canvas.height - 100);
ctx.lineTo(x + 100, canvas.height - 60);
ctx.lineTo(x + 150, canvas.height - 30);
}
ctx.lineTo(canvas.width, canvas.height - 30);
ctx.fill();
}
// 海底の描画
function drawFloor(ctx) {
let floorY = canvas.height - 30;
let ratio = Math.min(killCount / 100, 1);
// 砂の色
let r = Math.floor(194 + (20 - 194) * ratio);
let g = Math.floor(178 + (20 - 178) * ratio);
let b = Math.floor(128 + (20 - 128) * ratio);
ctx.fillStyle = `rgb(${r}, ${g}, ${b})`;
ctx.fillRect(0, floorY, canvas.width, 30);
// 地面の模様(岩や海草のシルエット)
ctx.fillStyle = `rgba(0, 0, 0, 0.2)`;
let patternWidth = 60;
let offset = bgScroll % patternWidth; // 近景は速くスクロール
for(let x = -offset; x < canvas.width; x += patternWidth) {
ctx.fillRect(x + 10, floorY + 5, 10, 5);
ctx.fillRect(x + 35, floorY + 15, 15, 8);
}
}
// ゲーム初期化
function startGame() {
player = new Player();
enemies = [];
particles = [];
items = [];
score = 0;
killCount = 0;
frameCount = 0;
bgScroll = 0;
gameState = 'playing';
uiStart.classList.add('hidden');
uiGameOver.classList.add('hidden');
// 泡の初期化
bubbles = [];
for(let i=0; i<15; i++) {
let b = new Bubble();
b.y = Math.random() * canvas.height;
b.x = Math.random() * canvas.width;
bubbles.push(b);
}
}
// メインループ
function gameLoop() {
update();
draw();
requestAnimationFrame(gameLoop);
}
// 更新処理
function update() {
if (gameState !== 'playing') {
// 背景の泡とスクロールだけ動かす
bgScroll += 1.5;
bubbles.forEach(b => b.update());
if (gameState === 'gameover') frameCount++;
return;
}
frameCount++;
bgScroll += 1.5; // スクロール進行
player.update(keys);
bubbles.forEach(b => b.update());
// 敵のスポーン制御 (進む=キル数が増えるほど頻度・数・速度が増す)
let spawnRate = Math.max(15, 70 - Math.floor(killCount / 5) * 5);
let speedMultiplier = 1 + (killCount / 30);
let spawnCount = 1 + Math.floor(killCount / 25); // 25匹倒すごとに同時に出る数が増える
if (frameCount % spawnRate === 0) {
for(let j=0; j<spawnCount; j++) {
let enemy = new Enemy(speedMultiplier);
// 複数出る場合は重ならないようにY軸をずらす
enemy.y += (j * 25);
if (enemy.y > canvas.height - enemy.h - 10) enemy.y = canvas.height - enemy.h - 10;
enemies.push(enemy);
}
}
// 攻撃判定の取得
let attackHitbox = player.getAttackHitbox();
let playerHitbox = player.getHitbox();
// アイテムの更新と判定
for (let i = items.length - 1; i >= 0; i--) {
let item = items[i];
item.update();
if (checkCollision(playerHitbox, item.getHitbox())) {
player.powerUp();
score += 500;
createExplosion(item.x + item.w/2, item.y + item.h/2, ['#ff0000', '#ffffff', '#ffb8b8']);
item.markedForDeletion = true;
}
if (item.markedForDeletion) {
items.splice(i, 1);
}
}
// 敵の更新と判定
for (let i = enemies.length - 1; i >= 0; i--) {
let enemy = enemies[i];
enemy.update();
let enemyHitbox = enemy.getHitbox();
// プレイヤーの攻撃が敵に当たったか
if (attackHitbox && checkCollision(attackHitbox, enemyHitbox)) {
enemy.markedForDeletion = true;
score += 100;
killCount++; // キル数をカウント(進行度)
// 水色と青のパーティクル
createExplosion(enemy.x + enemy.w/2, enemy.y + enemy.h/2, [palette[6], palette[7], '#ffffff']);
// 20匹倒すごとにアイテム出現
if (killCount % 20 === 0) {
items.push(new Item());
}
continue; // 倒したので以下の被ダメージ判定はスキップ
}
// 敵がプレイヤーに当たったか
if (!player.isInvincible && checkCollision(playerHitbox, enemyHitbox)) {
player.hp--;
player.isInvincible = true;
player.invincibleTimer = player.invincibleDuration;
// 赤いダメージパーティクル
createExplosion(player.x + player.w/2, player.y + player.h/2, [palette[1], '#ff0044', '#ffffff']);
if (player.hp <= 0) {
gameState = 'gameover';
finalScoreEl.innerText = score;
uiGameOver.classList.remove('hidden');
frameCount = 0; // リトライ遅延用
}
}
if (enemy.markedForDeletion) {
enemies.splice(i, 1);
}
}
// パーティクルの更新
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update();
if (particles[i].life <= 0) {
particles.splice(i, 1);
}
}
}
// 描画処理
function draw() {
// 背景クリア (進行度に応じて暗くなる)
ctx.fillStyle = getBackgroundColor(killCount);
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 遠景(岩山)の描画
drawBackground(ctx);
// 泡の描画
bubbles.forEach(b => b.draw(ctx));
// 海底の描画
drawFloor(ctx);
if (gameState === 'start') {
return; // UIが表示されているので描画は背景のみ
}
// エンティティの描画
items.forEach(i => i.draw(ctx));
particles.forEach(p => p.draw(ctx));
enemies.forEach(e => e.draw(ctx));
if (gameState === 'playing' || (gameState === 'gameover' && player.hp > 0)) {
player.draw(ctx);
}
// UI (スコアとHP)
ctx.fillStyle = 'white';
ctx.font = '16px monospace';
ctx.textAlign = 'left';
ctx.fillText(`SCORE: ${score}`, 10, 20);
// HPをハートで描画
let hpText = 'HP: ';
for(let i=0; i<player.maxHp; i++) {
hpText += (i < player.hp) ? '♥' : '♡';
}
ctx.fillStyle = '#e43b44';
ctx.fillText(hpText, 10, 40);
}
// 初期泡の生成とループ開始
for(let i=0; i<15; i++) {
let b = new Bubble();
b.y = Math.random() * canvas.height;
bubbles.push(b);
}
requestAnimationFrame(gameLoop);
</script>
</body>
</html>
メンダコが剣を使って魚を倒すゲームです。 20匹倒すごとに敵が増えたり、アイテムが大きくなります。