<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
text-align: center;
background-color: #f0f4f8;
margin: 0;
padding: 20px;
user-select: none;
}
h1 { color: #333; margin-bottom: 5px; }
/* 全体情報エリア */
#info {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
font-size: 16px;
margin-bottom: 15px;
font-weight: bold;
flex-wrap: wrap;
}
/* スタミナゲージの外枠 */
.stamina-container {
display: flex;
align-items: center;
gap: 5px;
}
.stamina-bar-bg {
width: 120px;
height: 16px;
background-color: #ddd;
border-radius: 8px;
overflow: hidden;
border: 1px solid #aaa;
box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
}
/* スタミナゲージの本体(動的に動く) */
#stamina-bar {
width: 100%;
height: 100%;
background-color: #2ed573;
transition: width 0.1s linear, background-color 0.3s;
}
#gameCanvas {
background-color: #fff;
border: 4px solid #ccc;
border-radius: 10px;
display: block;
margin: 0 auto;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
#message {
margin-top: 15px;
font-size: 24px;
font-weight: bold;
color: #ff4757;
height: 35px;
}
#btn-retry {
margin-top: 10px;
padding: 12px 24px;
font-size: 18px;
cursor: pointer;
display: none;
background-color: #2ed573;
color: white;
border: none;
border-radius: 5px;
}
#controls {
margin-top: 15px;
}
.btn-dir {
padding: 15px 25px;
font-size: 18px;
margin: 5px;
cursor: pointer;
background-color: #1e90ff;
color: white;
border: none;
border-radius: 5px;
touch-action: manipulation;
}
</style>
</head>
<body>
<h1 id="stage-title">🦔ハリネズミのリンゴ集め🍎</h1>
<div id="info">
<div>ステージ: <span id="stage-num">1</span></div>
<div class="stamina-container">
スタミナ:
<div class="stamina-bar-bg">
<div id="stamina-bar"></div>
</div>
(<span id="stamina-text">25.0</span>)
</div>
<div>スコア: <span id="score">0</span> / 5</div>
<div id="boss-hp-area" style="display:none;">| ボスHP: <span id="boss-hp">5</span></div>
</div>
<canvas id="gameCanvas" width="420" height="420"></canvas>
<div id="message"></div>
<button id="btn-retry" onclick="resetGame()">最初からあそぶ</button>
<div id="controls">
<div><button class="btn-dir" id="btn-up">↑</button></div>
<button class="btn-dir" id="btn-left">←</button>
<button class="btn-dir" id="btn-right">→</button>
<div><button class="btn-dir" id="btn-down">↓</button></div>
</div>
<p style="color: #666; font-size: 14px; margin-top: 10px;">※サボテンを拾うとボスに自動発射!</p>
<script>
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const BOARD_SIZE = 7;
const CELL_SIZE = 60;
let currentStage = 1;
let player = { x: 0, y: 0, dir: 'right' };
let apple = { x: 3, y: 3 };
let poops = [];
let puddles = [];
let launchers = [];
let needles = [];
// ボスをコブラに設定
let boss = { x: 3, y: 1, vx: 0.06, vy: 0, hp: 5, width: 1.5, height: 1.5 };
let maxStamina = 25.0; // 割合計算用の最大値スタミナ
let stamina = 25.0;
let score = 0;
let isGameOver = false;
let isClear = false;
let keys = {};
let touchDirs = { up: false, down: false, left: false, right: false };
let launcherSpawnTimer = 0;
let endingTimer = 0;
function resetGame() {
currentStage = 1;
initStage();
}
function initStage() {
player = { x: 0, y: BOARD_SIZE - 1, dir: 'right' };
poops = [];
puddles = [];
launchers = [];
needles = [];
isGameOver = false;
isClear = false;
launcherSpawnTimer = 0;
endingTimer = 0;
score = 0;
document.getElementById('message').innerText = "";
document.getElementById('btn-retry').style.display = "none";
document.getElementById('stage-num').innerText = currentStage;
document.getElementById('controls').style.display = "block";
const bossArea = document.getElementById('boss-hp-area');
if (currentStage === 1) {
stamina = 25.0;
maxStamina = 25.0;
document.getElementById('stage-title').innerText = "STAGE 1:うんちを避けろ!💩";
if(bossArea) bossArea.style.display = "none";
spawnApple();
spawnNewPoop(0.04);
} else if (currentStage === 2) {
stamina = 30.0;
maxStamina = 30.0;
document.getElementById('stage-title').innerText = "STAGE 2:泥沼の罠!💧";
if(bossArea) bossArea.style.display = "none";
spawnApple();
for(let i=0; i<2; i++) {
puddles.push({
x: Math.floor(Math.random() * (BOARD_SIZE - 2)) + 1,
y: Math.floor(Math.random() * (BOARD_SIZE - 2)) + 1
});
}
spawnNewPoop(0.04);
spawnNewPoop(0.05);
} else if (currentStage === 3) {
stamina = 45.0;
maxStamina = 45.0;
boss.hp = 5;
boss.x = BOARD_SIZE / 2 - boss.width / 2;
boss.y = 0.5;
document.getElementById('stage-title').innerText = "STAGE 3:対決!巨大コブラ🐍";
if(bossArea) bossArea.style.display = "inline";
const bossHpEl = document.getElementById('boss-hp');
if(bossHpEl) bossHpEl.innerText = boss.hp;
spawnNewPoop(0.03);
spawnNewPoop(0.03);
spawnLauncher();
}
updateStaminaGauge();
}
// スタミナゲージの見た目を更新する関数
function updateStaminaGauge() {
const bar = document.getElementById('stamina-bar');
const text = document.getElementById('stamina-text');
if (!bar || !text) return;
text.innerText = stamina.toFixed(1);
// 割合を計算してバーの幅を変更
let percentage = (stamina / maxStamina) * 100;
if (percentage < 0) percentage = 0;
bar.style.width = percentage + "%";
// 残量に応じて色を変化させる(緑 -> 黄 -> 赤)
if (percentage > 50) {
bar.style.backgroundcolor = "#2ed573"; // 緑
} else if (percentage > 20) {
bar.style.backgroundcolor = "#ffa500"; // オレンジ・黄
} else {
bar.style.backgroundcolor = "#ff4757"; // 赤
}
}
function spawnApple() {
let valid = false;
let attempts = 0;
while (!valid && attempts < 100) {
attempts++;
apple.x = Math.floor(Math.random() * BOARD_SIZE);
apple.y = Math.floor(Math.random() * BOARD_SIZE);
let nearPlayer = Math.abs(apple.x - player.x) < 1 && Math.abs(apple.y - player.y) < 1;
let inPuddle = puddles.some(p => p.x === apple.x && p.y === apple.y);
if (!nearPlayer && !inPuddle) valid = true;
}
}
function spawnNewPoop(speed) {
let px = Math.random() * (BOARD_SIZE - 1);
let py = Math.random() * (BOARD_SIZE - 3);
const angle = Math.random() * Math.PI * 2;
poops.push({ x: px, y: py, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed });
}
function spawnLauncher() {
let lx = Math.floor(Math.random() * BOARD_SIZE);
let ly = Math.floor(Math.random() * (BOARD_SIZE - 3)) + 3;
launchers.push({ x: lx, y: ly });
}
function drawHedgehog(cx, cy, radius, direction) {
ctx.save();
ctx.translate(cx, cy);
if (direction === 'left') ctx.scale(-1, 1);
ctx.fillStyle = "#6d4c41";
ctx.beginPath();
ctx.moveTo(-radius, 0);
for (let i = 0; i < 6; i++) {
let angle = Math.PI + (i * Math.PI / 5);
let x1 = Math.cos(angle) * radius;
let y1 = Math.sin(angle) * radius;
let x2 = Math.cos(angle) * (radius * 1.3);
let y2 = Math.sin(angle) * (radius * 1.3);
ctx.lineTo(x1, y1); ctx.lineTo(x2, y2);
}
ctx.fill();
ctx.fillStyle = "#e0a96d";
ctx.beginPath(); ctx.arc(0, 0, radius * 0.9, 0, Math.PI * 2); ctx.fill();
ctx.beginPath();
ctx.moveTo(radius * 0.5, -radius * 0.2);
ctx.lineTo(radius * 1.2, radius * 0.2);
ctx.lineTo(radius * 0.5, radius * 0.5);
ctx.fill();
ctx.fillStyle = "#000000";
ctx.beginPath(); ctx.arc(radius * 1.2, radius * 0.2, 3, 0, Math.PI * 2); ctx.fill();
ctx.beginPath(); ctx.arc(radius * 0.4, -radius * 0.1, 3, 0, Math.PI * 2); ctx.fill();
ctx.restore();
}
function update() {
if (isGameOver) return;
if (isClear) { endingTimer++; return; }
let baseSpeed = 0.08;
let inPuddle = puddles.some(p => Math.hypot(player.x - p.x, player.y - p.y) < 0.6);
let currentSpeed = inPuddle ? baseSpeed * 0.4 : baseSpeed;
let dx = 0, dy = 0;
if (keys['ArrowUp'] || touchDirs.up) dy = -currentSpeed;
if (keys['ArrowDown'] || touchDirs.down) dy = currentSpeed;
if (keys['ArrowLeft'] || touchDirs.left) { dx = -currentSpeed; player.dir = 'left'; }
if (keys['ArrowRight'] || touchDirs.right) { dx = currentSpeed; player.dir = 'right'; }
if (dx !== 0 || dy !== 0) {
player.x += dx; player.y += dy;
stamina -= 0.015;
player.x = Math.max(0, Math.min(BOARD_SIZE - 1, player.x));
player.y = Math.max(0, Math.min(BOARD_SIZE - 1, player.y));
}
poops.forEach(p => {
p.x += p.vx; p.y += p.vy;
if (p.x < 0 || p.x > BOARD_SIZE - 1) p.vx *= -1;
if (p.y < 0 || p.y > BOARD_SIZE - 1) p.vy *= -1;
});
if (currentStage === 3) {
boss.x += boss.vx;
if (boss.x < 0 || boss.x > BOARD_SIZE - boss.width) boss.vx *= -1;
if (launchers.length === 0) {
launcherSpawnTimer++;
if (launcherSpawnTimer > 100) { spawnLauncher(); launcherSpawnTimer = 0; }
}
for (let idx = needles.length - 1; idx >= 0; idx--) {
let n = needles[idx];
n.x += n.vx; n.y += n.vy;
if (n.x >= boss.x && n.x <= boss.x + boss.width && n.y >= boss.y && n.y <= boss.y + boss.height) {
boss.hp--;
const bossHpEl = document.getElementById('boss-hp');
if(bossHpEl) bossHpEl.innerText = boss.hp;
needles.splice(idx, 1);
if (boss.hp <= 0) {
startEnding();
return;
}
continue;
}
if (n.x < 0 || n.x > BOARD_SIZE || n.y < 0 || n.y > BOARD_SIZE) {
needles.splice(idx, 1);
}
}
}
if (poops.some(p => Math.hypot(player.x - p.x, player.y - p.y) < 0.45)) {
endGame("ゲームオーバー!うんちに衝突。");
return;
}
if (currentStage !== 3 && Math.hypot(player.x - apple.x, player.y - apple.y) < 0.6) {
score++;
stamina += 6.0;
if (stamina > maxStamina) stamina = maxStamina; // 最大値を超えない処理
if (score >= 5) {
nextStage();
} else {
spawnNewPoop(currentStage === 1 ? 0.04 : 0.05);
spawnApple();
}
}
if (currentStage === 3) {
for (let idx = launchers.length - 1; idx >= 0; idx--) {
let l = launchers[idx];
if (Math.hypot(player.x - l.x, player.y - l.y) < 0.6) {
launchers.splice(idx, 1);
let bossCenterX = boss.x + boss.width / 2;
let bossCenterY = boss.y + boss.height / 2;
let angle = Math.atan2(bossCenterY - player.y, bossCenterX - player.x);
let needleSpeed = 0.16;
needles.push({ x: player.x, y: player.y, vx: Math.cos(angle) * needleSpeed, vy: Math.sin(angle) * needleSpeed });
}
}
}
if (stamina <= 0) { stamina = 0; endGame("ゲームオーバー!スタミナ切れです。"); }
updateStaminaGauge(); // 毎フレームスタミナバーを更新
}
function nextStage() {
currentStage++;
initStage();
}
function startEnding() {
isClear = true; poops = []; launchers = []; needles = [];
document.getElementById('controls').style.display = "none";
document.getElementById('stage-title').innerText = "✨ EPILOGUE ✨";
}
function endGame(msg) {
isGameOver = true;
document.getElementById('message').innerText = msg;
document.getElementById('message').style.color = "#ff4757";
document.getElementById('btn-retry').style.display = "inline-block";
}
function draw() {
const scoreEl = document.getElementById('score');
if(scoreEl) scoreEl.innerText = score;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (!isClear) {
ctx.strokeStyle = "#e8eef3"; ctx.lineWidth = 1;
for (let i = 0; i <= BOARD_SIZE; i++) {
ctx.beginPath(); ctx.moveTo(i * CELL_SIZE, 0); ctx.lineTo(i * CELL_SIZE, canvas.height); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, i * CELL_SIZE); ctx.lineTo(canvas.width, i * CELL_SIZE); ctx.stroke();
}
ctx.textAlign = "center"; ctx.textBaseline = "middle";
if (currentStage === 2) {
ctx.font = "34px Arial";
puddles.forEach(p => ctx.fillText('💧', p.x * CELL_SIZE + CELL_SIZE/2, p.y * CELL_SIZE + CELL_SIZE/2));
}
if (currentStage === 3) {
ctx.font = "34px Arial";
launchers.forEach(l => ctx.fillText('🌵', l.x * CELL_SIZE + CELL_SIZE/2, l.y * CELL_SIZE + CELL_SIZE/2));
ctx.font = "20px Arial";
needles.forEach(n => ctx.fillText('📍', n.x * CELL_SIZE + CELL_SIZE/2, n.y * CELL_SIZE + CELL_SIZE/2));
// ボスをコブラ(🐍👑)に描き替え
ctx.font = "70px Arial";
ctx.fillText('🐍👑', (boss.x + boss.width/2) * CELL_SIZE, (boss.y + boss.height/2) * CELL_SIZE);
}
if (currentStage !== 3) {
ctx.font = "38px Arial";
if (apple.x !== undefined) ctx.fillText('🍎', apple.x * CELL_SIZE + CELL_SIZE/2, apple.y * CELL_SIZE + CELL_SIZE/2);
}
ctx.font = "36px Arial";
poops.forEach(p => ctx.fillText('💩', p.x * CELL_SIZE + CELL_SIZE/2, p.y * CELL_SIZE + CELL_SIZE/2));
let px = player.x * CELL_SIZE + CELL_SIZE/2;
let py = player.y * CELL_SIZE + CELL_SIZE/2;
drawHedgehog(px, py, 20, player.dir);
} else {
ctx.fillStyle = "#1e272e"; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.textAlign = "center"; ctx.fillStyle = "#ffffff";
if (endingTimer < 120) {
ctx.font = "24px 'Helvetica Neue'";
ctx.fillText("激闘の末、大蛇コブラは逃げ出した!", canvas.width/2, 180);
ctx.font = "40px Arial"; ctx.fillText("🏃♂️💨🐍", canvas.width/2, 260);
} else if (endingTimer < 260) {
ctx.font = "22px 'Helvetica Neue'";
ctx.fillText("森に平和が戻り、たくさんの", canvas.width/2, 160);
ctx.fillText("特大リンゴが実りました。", canvas.width/2, 200);
ctx.font = "45px Arial"; ctx.fillText("🍎🍏🍎🍏🍎", canvas.width/2, 280);
} else {
ctx.fillStyle = "#ffd32a"; ctx.font = "bold 32px 'Helvetica Neue'";
ctx.fillText("✨ HAPPY END ✨", canvas.width/2, 100);
drawHedgehog(canvas.width/2, 180, 30, 'right');
ctx.fillStyle = "#ffffff"; ctx.font = "18px 'Helvetica Neue'";
ctx.fillText("こうしてハリネズミの満腹な旅は幕を閉じた。", canvas.width/2, 260);
ctx.fillText("遊んでくれてありがとう!", canvas.width/2, 300);
document.getElementById('btn-retry').style.display = "inline-block";
document.getElementById('btn-retry').innerText = "もう一度最初から遊ぶ";
}
}
}
function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); }
window.addEventListener('keydown', (e) => {
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) e.preventDefault();
keys[e.key] = true;
});
window.addEventListener('keyup', (e) => { keys[e.key] = false; });
function setupButton(id, dirStr) {
const btn = document.getElementById(id);
const startEvent = (e) => { e.preventDefault(); if(!isGameOver && !isClear) touchDirs[dirStr] = true; };
const endEvent = (e) => { e.preventDefault(); touchDirs[dirStr] = false; };
btn.addEventListener('mousedown', startEvent); btn.addEventListener('mouseup', endEvent); btn.addEventListener('mouseleave', endEvent);
btn.addEventListener('touchstart', startEvent); btn.addEventListener('touchend', endEvent);
}
setupButton('btn-up', 'up'); setupButton('btn-down', 'down'); setupButton('btn-left', 'left'); setupButton('btn-right', 'right');
resetGame();
gameLoop();
</script>