<!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-scalable=no">
<title>ミズダコの一生 - Realistic Survival</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #001a33;
font-family: 'Helvetica Neue', Arial, sans-serif;
touch-action: none;
user-select: none;
}
#game-canvas {
display: block;
}
.ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
color: white;
text-shadow: 2px 2px 8px rgba(0,0,0,0.9);
}
.interactive {
pointer-events: auto;
}
.progress-bar {
height: 12px;
background: rgba(255,255,255,0.2);
border-radius: 6px;
overflow: hidden;
width: 140px;
border: 1px solid rgba(255,255,255,0.3);
}
@media (min-width: 768px) {
.progress-bar { width: 240px; height: 16px; }
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4ade80, #22c55e);
width: 0%;
transition: width 0.4s ease-out;
}
#joystick-base {
position: absolute;
bottom: 60px;
left: 60px;
width: 110px;
height: 110px;
background: rgba(255, 255, 255, 0.05);
border: 2px solid rgba(255, 255, 255, 0.15);
border-radius: 50%;
display: none;
pointer-events: none;
z-index: 100;
}
#joystick-knob {
position: absolute;
width: 44px;
height: 44px;
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(4px);
border-radius: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
}
</style>
</head>
<body>
<canvas id="game-canvas"></canvas>
<div id="joystick-base">
<div id="joystick-knob"></div>
</div>
<!-- UI Layers -->
<div class="ui-layer" id="ui-start">
<div class="flex-1 flex flex-col items-center justify-center text-center interactive bg-black/50 backdrop-blur-xl rounded-2xl m-6 p-8 shadow-2xl">
<h1 class="text-5xl sm:text-7xl font-black mb-6 text-transparent bg-clip-text bg-gradient-to-b from-cyan-300 to-blue-600">ミズダコの一生</h1>
<p class="text-xl sm:text-2xl mb-8 max-w-xl text-cyan-100 font-medium">
北の冷たい海で育まれる、<br>巨大な生命のドラマを体験してください。
</p>
<button id="start-btn" class="bg-gradient-to-r from-cyan-600 to-blue-700 hover:from-cyan-500 hover:to-blue-600 text-white font-black py-4 px-12 rounded-full text-2xl transition-all transform active:scale-90 shadow-2xl border-b-4 border-blue-900">
生命の旅を始める
</button>
</div>
</div>
<div class="ui-layer hidden" id="ui-game">
<div class="flex justify-between items-start p-6">
<div class="bg-black/30 backdrop-blur-md p-3 rounded-xl border border-white/10">
<div class="text-[10px] font-black uppercase tracking-widest text-cyan-400">生命の形態</div>
<div id="stage-name" class="text-xl sm:text-2xl font-black">幼体</div>
<div id="mimicry-status" class="text-[12px] font-bold text-yellow-400 opacity-0 transition-all duration-500">MIMICRY: 環境と同化中</div>
</div>
<div class="text-right bg-black/30 backdrop-blur-md p-3 rounded-xl border border-white/10">
<div class="text-[10px] font-black uppercase tracking-widest text-green-400">成長度</div>
<div class="progress-bar mt-1">
<div id="growth-fill" class="progress-fill"></div>
</div>
</div>
</div>
<div id="message-area" class="text-center text-2xl sm:text-3xl font-black py-4 px-8 opacity-0 transition-opacity duration-700 pointer-events-none drop-shadow-lg">
メッセージ
</div>
<div class="flex justify-between items-end p-6">
<div class="bg-black/40 p-3 sm:p-5 rounded-2xl backdrop-blur-md border border-white/10 shadow-xl">
<div class="text-[10px] text-red-300 font-black uppercase tracking-tighter">生存エネルギー</div>
<div class="progress-bar h-3 sm:h-4 mt-1 border-red-900/30">
<div id="health-fill" class="progress-fill bg-gradient-to-r from-red-600 to-red-400" style="width: 100%"></div>
</div>
</div>
</div>
</div>
<div class="ui-layer hidden" id="ui-gameover">
<div class="flex-1 flex flex-col items-center justify-center text-center interactive bg-black/80 backdrop-blur-2xl p-8">
<h2 class="text-5xl font-black mb-4 text-red-500">旅の終焉</h2>
<p id="death-reason" class="text-2xl mb-12 text-gray-300">自然の摂理に還りました。</p>
<button id="retry-btn" class="bg-white text-black font-black py-4 px-12 rounded-full text-2xl active:scale-95 transition-transform">
輪廻転生
</button>
</div>
</div>
<div class="ui-layer hidden" id="ui-win">
<div class="flex-1 flex flex-col items-center justify-center text-center interactive bg-black/90 backdrop-blur-3xl p-6 sm:p-12 overflow-y-auto">
<h2 class="text-5xl sm:text-7xl font-black mb-6 text-pink-500 drop-shadow-2xl">命の継承</h2>
<p class="text-2xl sm:text-3xl mb-6 font-bold text-pink-200">偉大なるミズダコとして</p>
<p class="text-lg sm:text-xl mb-12 max-w-3xl leading-relaxed text-gray-200">
あなたは数え切れないほどの困難を乗り越え、命のバトンを次世代へと渡しました。<br>
冷たい海底で卵を守り抜き、あなたの体はやがて海の一部となり、また新しい命を育みます。
</p>
<button id="restart-btn" class="bg-pink-600 text-white font-black py-5 px-14 rounded-full text-2xl active:scale-95 shadow-2xl transition-all">
新しいミズダコとして目覚める
</button>
</div>
</div>
<script>
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
const joyBase = document.getElementById('joystick-base');
const joyKnob = document.getElementById('joystick-knob');
const UI = {
start: document.getElementById('ui-start'),
game: document.getElementById('ui-game'),
gameover: document.getElementById('ui-gameover'),
win: document.getElementById('ui-win'),
growthFill: document.getElementById('growth-fill'),
healthFill: document.getElementById('health-fill'),
stageName: document.getElementById('stage-name'),
message: document.getElementById('message-area'),
mimicryStatus: document.getElementById('mimicry-status'),
deathReason: document.getElementById('death-reason')
};
const STAGES = [
{ name: "幼体 (Paralarva)", color: "rgba(230, 245, 255, 0.2)", minSize: 18, maxSize: 28, threshold: 120, habitat: 'pelagic' },
{ name: "亜成体 (Juvenile)", color: "rgb(160, 60, 45)", minSize: 40, maxSize: 65, threshold: 400, habitat: 'benthic' },
{ name: "成体 (Adult)", color: "rgb(140, 35, 20)", minSize: 75, maxSize: 110, threshold: 1000, habitat: 'benthic' },
{ name: "伝説の巨体 (Giant)", color: "rgb(110, 20, 10)", minSize: 120, maxSize: 180, threshold: 2000, habitat: 'benthic' }
];
let gameState = 'START';
let currentStageIdx = 0;
let score = 0;
let health = 100;
let frameCount = 0;
let camera = { x: 0, y: 0 };
let worldSize = { width: 6000, height: 6000 };
let hatchProgress = 0;
let eggShake = 0;
let entities = [];
let marineSnow = [];
let rocks = [];
let skinBumps = []; // 皮膚のテクスチャ用
let partnerSpawned = false;
const player = {
x: 3000, y: 3000,
size: 20,
vx: 0, vy: 0,
rotation: 0,
arms: [],
mimicryAmount: 0
};
const input = { isMoving: false, joyX: 0, joyY: 0, touchId: null };
const keys = {};
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);
const handleTouchStart = (e) => {
if (gameState === 'HATCHING') { hatchProgress += 18; eggShake = 20; return; }
if (gameState !== 'PLAYING') return;
const touch = e.touches[0];
input.touchId = touch.identifier;
input.isMoving = true;
joyBase.style.display = 'block';
joyBase.style.left = `${touch.clientX - 55}px`;
joyBase.style.top = `${touch.clientY - 55}px`;
updateJoystick(touch.clientX, touch.clientY);
};
const handleTouchMove = (e) => {
if (gameState !== 'PLAYING') return;
e.preventDefault();
for (let touch of e.changedTouches) {
if (touch.identifier === input.touchId) { updateJoystick(touch.clientX, touch.clientY); }
}
};
const handleTouchEnd = (e) => {
for (let touch of e.changedTouches) {
if (touch.identifier === input.touchId) {
input.isMoving = false;
input.joyX = 0; input.joyY = 0;
joyBase.style.display = 'none';
}
}
};
canvas.addEventListener('touchstart', handleTouchStart);
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
canvas.addEventListener('touchend', handleTouchEnd);
canvas.addEventListener('mousedown', (e) => {
if (gameState === 'HATCHING') { hatchProgress += 18; eggShake = 20; }
});
function updateJoystick(tx, ty) {
const bx = parseInt(joyBase.style.left) + 55;
const by = parseInt(joyBase.style.top) + 55;
let dx = tx - bx;
let dy = ty - by;
const dist = Math.sqrt(dx*dx + dy*dy);
const maxDist = 55;
if (dist > maxDist) { dx = (dx / dist) * maxDist; dy = (dy / dist) * maxDist; }
joyKnob.style.left = `${55 + dx}px`;
joyKnob.style.top = `${55 + dy}px`;
input.joyX = dx / maxDist;
input.joyY = dy / maxDist;
}
function init() {
resize();
window.addEventListener('resize', resize);
player.arms = Array.from({ length: 8 }, (_, i) => ({
angle: (Math.PI * 2 / 8) * i,
segments: Array.from({ length: 10 }, () => ({ x: player.x, y: player.y }))
}));
// 海の背景要素生成
for (let i = 0; i < 250; i++) {
marineSnow.push({ x: Math.random()*worldSize.width, y: Math.random()*worldSize.height, size: Math.random()*2+1, speed: 0.15+Math.random()*0.4, alpha: 0.1+Math.random()*0.4 });
}
for (let i = 0; i < 160; i++) {
rocks.push({
x: Math.random()*worldSize.width,
y: Math.random()*worldSize.height,
size: 100+Math.random()*250,
rotation: Math.random()*Math.PI*2,
color: `rgb(${25+Math.random()*15}, ${25+Math.random()*15}, ${20+Math.random()*10})`
});
}
// 皮膚のブツブツ用パーティクル
for(let i=0; i<30; i++){
skinBumps.push({ x: Math.random(), y: Math.random(), size: 0.1+Math.random()*0.2 });
}
requestAnimationFrame(gameLoop);
}
function resize() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; }
function spawnEntity() {
if (entities.length > 100) return;
const isEnemy = Math.random() > 0.8;
let type, color, size, name;
if (!isEnemy) {
if (currentStageIdx === 0) { type = 'plankton'; color = '#fff'; size = 3; }
else if (currentStageIdx === 1) { type = 'shrimp'; color = '#ff9966'; size = 12; }
else { type = 'crab'; color = '#ff4444'; size = 22; }
} else {
if (currentStageIdx === 0) { type = 'smallFish'; color = '#5555ff'; size = 38; name = "小魚"; }
else if (currentStageIdx === 1) { type = 'bigFish'; color = '#3333aa'; size = 80; name = "大型魚"; }
else { type = 'shark'; color = '#222'; size = 280; name = "サメ"; }
}
const angle = Math.random()*Math.PI*2;
const dist = Math.max(canvas.width, canvas.height)*1.1;
entities.push({ x: player.x+Math.cos(angle)*dist, y: player.y+Math.sin(angle)*dist, vx: (Math.random()-0.5)*2.5, vy: (Math.random()-0.5)*2.5, size, color, type, name, isEnemy, pulse: 0 });
}
function update() {
if (gameState === 'HATCHING') {
eggShake *= 0.88;
if (hatchProgress >= 100) { gameState = 'PLAYING'; showMessage("誕生!透明な幼体として一生が始まります", "text-cyan-200"); }
return;
}
if (gameState !== 'PLAYING') return;
const stage = STAGES[currentStageIdx];
const currentGrowthRate = score / stage.threshold;
if (currentStageIdx >= 2 && currentGrowthRate > 0.85 && !partnerSpawned) {
partnerSpawned = true;
entities.push({
x: player.x+1000, y: player.y+1000, size: player.size*0.9, color: "#ff66aa", type: 'partner', name: "パートナー", isEnemy: false
});
showMessage("繁殖の準備が整いました。パートナーを探してください。", "text-pink-300");
}
let moveX = input.joyX, moveY = input.joyY;
if (keys['KeyW'] || keys['ArrowUp']) moveY = -1;
if (keys['KeyS'] || keys['ArrowDown']) moveY = 1;
if (keys['KeyA'] || keys['ArrowLeft']) moveX = -1;
if (keys['KeyD'] || keys['ArrowRight']) moveX = 1;
if (moveX !== 0 || moveY !== 0) {
const mag = Math.sqrt(moveX*moveX + moveY*moveY);
player.vx += (moveX / mag) * 0.5;
player.vy += (moveY / mag) * 0.5;
player.rotation = Math.atan2(moveY, moveX);
}
player.vx *= 0.95; player.vy *= 0.95;
player.x += player.vx; player.y += player.vy;
const currentSpeed = Math.sqrt(player.vx*player.vx + player.vy*player.vy);
if (currentSpeed < 1.2) player.mimicryAmount = Math.min(1, player.mimicryAmount + 0.015);
else player.mimicryAmount = Math.max(0, player.mimicryAmount - 0.08);
UI.mimicryStatus.style.opacity = player.mimicryAmount > 0.7 ? "1" : "0";
player.x = Math.max(0, Math.min(worldSize.width, player.x));
player.y = Math.max(0, Math.min(worldSize.height, player.y));
camera.x = player.x - canvas.width / 2; camera.y = player.y - canvas.height / 2;
player.arms.forEach((arm, i) => {
const baseAngle = player.rotation + arm.angle;
let px = player.x, py = player.y;
arm.segments.forEach((seg, j) => {
const targetX = px - Math.cos(baseAngle + Math.sin(frameCount*0.12 + i + j*0.35)*0.25) * (player.size * 0.5);
const targetY = py - Math.sin(baseAngle + Math.sin(frameCount*0.12 + i + j*0.35)*0.25) * (player.size * 0.5);
seg.x += (targetX - seg.x) * 0.45; seg.y += (targetY - seg.y) * 0.45;
px = seg.x; py = seg.y;
});
});
for (let i = entities.length - 1; i >= 0; i--) {
const e = entities[i];
e.x += e.vx; e.y += e.vy;
const dist = Math.sqrt((e.x-player.x)**2 + (e.y-player.y)**2);
if (dist < player.size + e.size) {
if (e.type === 'partner') { gameWin(); return; }
const canEat = (currentStageIdx >= 1 && currentGrowthRate > 0.4 && e.type === 'smallFish');
if (e.isEnemy && !canEat) {
if (player.mimicryAmount < 0.75) {
health -= 25; showMessage(`${e.name}に攻撃された!`, "text-red-400");
entities.splice(i, 1);
if (health <= 0) gameOver();
}
} else {
score += e.size * 5; health = Math.min(100, health + 12);
entities.splice(i, 1); checkGrowth();
}
} else if (dist > 4000) entities.splice(i, 1);
}
marineSnow.forEach(s => { s.y += s.speed; if (s.y > worldSize.height) s.y = 0; });
if (frameCount % 35 === 0) spawnEntity();
UI.healthFill.style.width = `${health}%`;
frameCount++;
}
function checkGrowth() {
const stage = STAGES[currentStageIdx];
const progress = score / stage.threshold;
player.size = stage.minSize + (stage.maxSize - stage.minSize) * Math.min(1, progress);
UI.growthFill.style.width = `${Math.min(100, progress * 100)}%`;
if (score >= stage.threshold && STAGES[currentStageIdx + 1]) {
currentStageIdx++; score = 0; UI.stageName.innerText = STAGES[currentStageIdx].name;
partnerSpawned = false;
showMessage("成長!体が変化しました。", "text-yellow-300");
}
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const stage = STAGES[currentStageIdx];
const isBenthic = stage.habitat === 'benthic';
// 背景描画
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
if (isBenthic) {
grad.addColorStop(0, "#2c4a5c"); grad.addColorStop(1, "#0a151b");
} else {
grad.addColorStop(0, "#0066cc"); grad.addColorStop(1, "#002b55");
}
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 岩場の描画
if (isBenthic) {
rocks.forEach(r => {
const rx = r.x - camera.x, ry = r.y - camera.y;
if (rx > -r.size && rx < canvas.width + r.size && ry > -r.size && ry < canvas.height + r.size) {
ctx.fillStyle = r.color;
ctx.beginPath();
ctx.ellipse(rx, ry, r.size, r.size*0.7, r.rotation, 0, Math.PI*2);
ctx.fill();
// 岩のハイライト
ctx.strokeStyle = "rgba(255,255,255,0.05)";
ctx.stroke();
}
});
}
// マリンスノー
ctx.fillStyle = "rgba(255,255,255,0.25)";
marineSnow.forEach(s => {
const sx = s.x - camera.x, sy = s.y - camera.y;
if (sx > 0 && sx < canvas.width && sy > 0 && sy < canvas.height) {
ctx.beginPath(); ctx.arc(sx, sy, s.size, 0, Math.PI*2); ctx.fill();
}
});
if (gameState === 'HATCHING') {
const ex = canvas.width / 2 + (Math.random()-0.5)*eggShake;
const ey = canvas.height / 2 + (Math.random()-0.5)*eggShake;
// 卵の影
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.beginPath(); ctx.ellipse(ex+10, ey+10, 65, 85, 0, 0, Math.PI*2); ctx.fill();
// 卵本体
ctx.fillStyle = `rgba(255, 255, 250, ${0.9 - hatchProgress/150})`;
ctx.beginPath(); ctx.ellipse(ex, ey, 60, 80, 0, 0, Math.PI*2); ctx.fill();
// ヒビ
if(hatchProgress > 30) {
ctx.strokeStyle = "rgba(0,0,0,0.2)"; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(ex-30, ey-40); ctx.lineTo(ex+10, ey); ctx.lineTo(ex-10, ey+40); ctx.stroke();
}
// 中の影
ctx.fillStyle = "rgba(100, 150, 200, 0.3)";
ctx.beginPath(); ctx.arc(ex, ey + 15, 25 + hatchProgress/6, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "white"; ctx.font = "bold 24px sans-serif"; ctx.textAlign="center";
ctx.fillText("連打して卵から生まれる", canvas.width/2, canvas.height/2 + 150);
return;
}
const px = player.x - camera.x, py = player.y - camera.y;
// 擬態と皮膚カラーの計算
let baseColor = stage.color;
if (currentStageIdx > 0) {
// 亜成体以降の岩場への擬態
const rockRGB = [44, 74, 92]; // 海底カラー
const octopusRGB = stage.color.match(/\d+/g).map(Number);
const r = octopusRGB[0] + (rockRGB[0] - octopusRGB[0]) * player.mimicryAmount;
const g = octopusRGB[1] + (rockRGB[1] - octopusRGB[1]) * player.mimicryAmount;
const b = octopusRGB[2] + (targetB = rockRGB[2] - octopusRGB[2]) * player.mimicryAmount;
baseColor = `rgb(${Math.floor(r)}, ${Math.floor(g)}, ${Math.floor(octopusRGB[2] + (rockRGB[2]-octopusRGB[2])*player.mimicryAmount)})`;
} else {
// 幼体:擬態中はより透明に
baseColor = `rgba(230, 245, 255, ${0.3 * (1 - player.mimicryAmount * 0.9)})`;
}
// 腕と吸盤の描画
player.arms.forEach((arm, i) => {
ctx.strokeStyle = baseColor;
ctx.lineWidth = currentStageIdx === 0 ? 3 : player.size * 0.7;
ctx.beginPath(); ctx.moveTo(px, py);
arm.segments.forEach(seg => ctx.lineTo(seg.x - camera.x, seg.y - camera.y));
ctx.stroke();
// 吸盤の描画(亜成体以降)
if (currentStageIdx > 0) {
arm.segments.forEach((seg, idx) => {
if (idx % 2 === 0 && idx > 0) {
ctx.fillStyle = `rgba(255, 255, 255, ${0.3 * (1 - player.mimicryAmount * 0.5)})`;
ctx.beginPath(); ctx.arc(seg.x - camera.x, seg.y - camera.y, player.size * 0.18, 0, Math.PI*2); ctx.fill();
}
});
}
});
// 本体(外套膜)の描画
ctx.save(); ctx.translate(px, py); ctx.rotate(player.rotation);
ctx.fillStyle = baseColor;
ctx.beginPath(); ctx.ellipse(0, 0, player.size, player.size * 0.9, 0, 0, Math.PI * 2); ctx.fill();
if (currentStageIdx === 0) {
// 幼体のディテール:透ける内臓
ctx.fillStyle = `rgba(150, 100, 50, ${0.7 * (1 - player.mimicryAmount)})`;
ctx.beginPath(); ctx.ellipse(-player.size*0.2, 0, player.size*0.5, player.size*0.4, 0, 0, Math.PI*2); ctx.fill();
// 大きな目
const eyeA = 1 - player.mimicryAmount * 0.8;
ctx.fillStyle = `rgba(0,0,0,${eyeA})`;
ctx.beginPath(); ctx.arc(player.size*0.5, -player.size*0.55, player.size*0.45, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(player.size*0.5, player.size*0.55, player.size*0.45, 0, Math.PI*2); ctx.fill();
// 目のハイライト
ctx.fillStyle = `rgba(255,255,255,${eyeA*0.8})`;
ctx.beginPath(); ctx.arc(player.size*0.65, -player.size*0.65, 3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(player.size*0.65, player.size*0.65, 3, 0, Math.PI*2); ctx.fill();
} else {
// 皮膚のブツブツテクスチャ
ctx.fillStyle = `rgba(0,0,0,${0.2 * (1 - player.mimicryAmount)})`;
skinBumps.forEach(b => {
ctx.beginPath(); ctx.arc((b.x-0.5)*player.size*1.5, (b.y-0.5)*player.size*1.5, player.size*b.size, 0, Math.PI*2); ctx.fill();
});
// 成体の目
const eyeA = 1 - player.mimicryAmount * 0.4;
ctx.fillStyle = `rgba(255,255,255,${eyeA})`;
ctx.beginPath(); ctx.arc(player.size*0.45, -player.size*0.45, player.size*0.35, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(player.size*0.45, player.size*0.45, player.size*0.35, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = `rgba(0,0,0,${eyeA})`;
ctx.beginPath(); ctx.arc(player.size*0.55, -player.size*0.45, player.size*0.18, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(player.size*0.55, player.size*0.45, player.size*0.18, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
// 他の生物
entities.forEach(e => {
const ex = e.x - camera.x, ey = e.y - camera.y;
if (ex < -300 || ex > canvas.width + 300 || ey < -300 || ey > canvas.height + 300) return;
ctx.fillStyle = e.color;
if (e.isEnemy) {
ctx.save(); ctx.translate(ex, ey); ctx.rotate(Math.atan2(e.vy, e.vx));
ctx.beginPath(); ctx.ellipse(0, 0, e.size, e.size*0.42, 0, 0, Math.PI*2); ctx.fill();
// ヒレ
ctx.beginPath(); ctx.moveTo(-e.size*0.8, 0); ctx.lineTo(-e.size*1.7, -e.size*0.65); ctx.lineTo(-e.size*1.7, e.size*0.65); ctx.closePath(); ctx.fill();
ctx.restore();
} else if (e.type === 'partner') {
ctx.beginPath(); ctx.arc(ex, ey, e.size, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = "#ff66aa"; ctx.font = "bold 24px Arial"; ctx.fillText("♥", ex-12, ey-e.size-15);
} else {
ctx.beginPath(); ctx.arc(ex, ey, e.size, 0, Math.PI*2); ctx.fill();
}
});
}
function showMessage(txt, colorClass) {
UI.message.innerText = txt; UI.message.className = `text-center font-black py-3 opacity-100 transition-opacity ${colorClass}`;
setTimeout(() => UI.message.style.opacity = "0", 4000);
}
function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); }
function startGame() {
gameState = 'HATCHING'; currentStageIdx = 0; score = 0; health = 100; entities = [];
hatchProgress = 0; partnerSpawned = false;
player.x = 3000; player.y = 3000; player.mimicryAmount = 0;
UI.start.classList.add('hidden'); UI.gameover.classList.add('hidden'); UI.win.classList.add('hidden');
UI.game.classList.remove('hidden');
}
function gameOver() { gameState = 'GAMEOVER'; UI.game.classList.add('hidden'); UI.gameover.classList.remove('hidden'); }
function gameWin() { gameState = 'WIN'; UI.game.classList.add('hidden'); UI.win.classList.remove('hidden'); }
document.getElementById('start-btn').onclick = startGame;
document.getElementById('retry-btn').onclick = startGame;
document.getElementById('restart-btn').onclick = startGame;
init();
</script>
</body>
</html>
ミズダコの一生を体感できます! 可愛い幼体から、大きくなっていきます!