<!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>Kirby Ultra Realistic Adventure</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@700;800&display=swap');
body {
margin: 0;
background-color: #000;
font-family: 'M PLUS Rounded 1c', sans-serif;
overflow: hidden;
touch-action: none;
}
#game-wrapper {
position: relative;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: radial-gradient(circle at center, #1a1a2e 0%, #000000 100%);
}
#canvas-container {
position: relative;
width: 100%;
max-width: 1024px;
aspect-ratio: 16 / 9;
background: #000;
box-shadow: 0 0 50px rgba(0, 0, 0, 1), 0 0 100px rgba(56, 189, 248, 0.2);
border: 2px solid #1e293b;
border-radius: 12px;
overflow: hidden;
}
/* 究極のリアルさを出すためのポストプロセッシング(ビネット&微細なノイズ) */
#canvas-container::after {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle, rgba(0,0,0,0) 50%, rgba(0,0,0,0.6) 100%);
box-shadow: inset 0 0 20px rgba(255,255,255,0.05);
pointer-events: none;
z-index: 10;
}
canvas {
width: 100%;
height: 100%;
image-rendering: auto;
filter: contrast(1.1) saturate(1.1); /* コントラストを高めてリッチに */
}
/* UI Overlay */
#hud {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 80px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), rgba(0,0,0,0));
display: flex;
align-items: flex-end;
padding: 15px 25px;
pointer-events: none;
display: none;
z-index: 20;
}
.hp-bar-bg {
width: 200px;
height: 18px;
background: rgba(0,0,0,0.6);
border: 2px solid rgba(255,255,255,0.8);
border-radius: 9px;
overflow: hidden;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
}
.hp-bar-fill {
height: 100%;
background: linear-gradient(to right, #fb7185, #e11d48);
width: 100%;
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: inset 0 0 5px rgba(255,255,255,0.5);
}
#title-screen {
position: absolute;
inset: 0;
background: radial-gradient(circle at top, #334155 0%, #0f172a 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 50;
}
.start-btn {
background: linear-gradient(to bottom, #f8fafc, #cbd5e1);
color: #0f172a;
padding: 1rem 4rem;
border-radius: 9999px;
font-size: 1.5rem;
font-weight: 800;
box-shadow: 0 10px 20px rgba(0,0,0,0.5), inset 0 -3px 0 rgba(0,0,0,0.2);
transition: all 0.1s;
cursor: pointer;
margin-top: 3rem;
pointer-events: auto;
text-shadow: 0 1px 1px rgba(255,255,255,0.8);
}
.start-btn:active {
transform: translateY(4px);
box-shadow: 0 6px 10px rgba(0,0,0,0.5), inset 0 -1px 0 rgba(0,0,0,0.2);
}
.char-btn {
width: 60px;
height: 60px;
border-radius: 50%;
cursor: pointer;
border: 3px solid rgba(255,255,255,0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 10px 15px rgba(0,0,0,0.5);
}
.char-btn.selected {
border: 4px solid #fff;
transform: scale(1.2) translateY(-10px);
box-shadow: 0 15px 25px rgba(0,0,0,0.6), 0 0 20px rgba(255,255,255,0.4);
}
#touch-ui {
position: absolute;
bottom: 20px;
width: 100%;
display: none;
justify-content: space-between;
padding: 0 40px;
pointer-events: none;
z-index: 100;
}
.touch-btn {
background: rgba(255, 255, 255, 0.15);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
pointer-events: auto;
color: rgba(255,255,255,0.9);
font-weight: bold;
user-select: none;
backdrop-filter: blur(4px);
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
transition: background 0.1s;
}
.touch-btn:active {
background: rgba(255, 255, 255, 0.3);
}
/* 縦持ち(スマートフォン)対応のレイアウト最適化 */
@media (orientation: portrait) {
#game-wrapper {
flex-direction: column;
justify-content: flex-start;
padding-top: 10vh; /* 上部にゲーム画面を配置 */
}
#canvas-container {
border-radius: 0;
border-left: none;
border-right: none;
}
/* コントローラーを画面下部の黒帯部分に固定配置 */
#touch-ui {
position: fixed;
bottom: 8vh;
left: 0;
width: 100vw;
padding: 0 5vw;
box-sizing: border-box;
z-index: 100;
}
/* ボタンを親指で押しやすいサイズに相対調整 */
.touch-btn {
width: 16vw !important;
height: 16vw !important;
font-size: 6vw !important;
}
#t-shift {
width: 14vw !important;
height: 14vw !important;
font-size: 3vw !important;
}
#t-x, #t-z {
width: 20vw !important;
height: 20vw !important;
font-size: 8vw !important;
}
/* タイトル画面の文字サイズ・余白調整(縦画面でもボタンが隠れないようにする) */
#title-screen h1 { font-size: 14vw; line-height: 1; margin-top: -2vw; }
#title-screen p { font-size: 3vw; margin-top: 0 !important; }
#title-screen > div.mt-12 { margin-top: 0.5rem !important; margin-bottom: 0.25rem !important; font-size: 2.5vw; }
#char-select { gap: 10px !important; }
.char-btn { width: 8vw; height: 8vw; border-width: 2px; }
.char-btn.selected { transform: scale(1.2) translateY(-5px); border-width: 3px; }
.start-btn { font-size: 4vw; padding: 0.5rem 1.5rem; margin-top: 1rem !important; box-shadow: 0 5px 10px rgba(0,0,0,0.5), inset 0 -2px 0 rgba(0,0,0,0.2); }
/* HUD(UI)の縮小調整 */
#hud {
padding: 10px;
height: auto;
}
#ability-icon { width: 40px; height: 40px; }
#ability-text { font-size: 8px; }
.hp-bar-bg { width: 100px; height: 12px; }
.mb-1 { margin-bottom: 2px; }
#stage-text { font-size: 1.2rem; }
#score-text { font-size: 1rem; }
}
</style>
</head>
<body>
<div id="game-wrapper">
<div id="canvas-container">
<canvas id="gameCanvas" width="1024" height="576"></canvas>
<div id="title-screen">
<h1 class="text-7xl text-transparent bg-clip-text bg-gradient-to-b from-white to-gray-400 drop-shadow-[0_0_15px_rgba(255,255,255,0.5)] font-black italic tracking-tighter">KIRBY</h1>
<p class="text-blue-300 text-2xl mt-2 font-bold tracking-[0.3em] drop-shadow-md">ULTRA REALISTIC</p>
<div class="mt-12 text-white/80 font-bold mb-4 tracking-widest text-sm">SELECT PLAYER COLOR</div>
<div id="char-select" class="flex gap-6 pointer-events-auto">
<div class="char-btn selected" style="background: radial-gradient(circle at 30% 30%, #ffb7c5, #e11d48);" data-color="pink" onclick="selectChar('pink')"></div>
<div class="char-btn" style="background: radial-gradient(circle at 30% 30%, #fef08a, #d97706);" data-color="yellow" onclick="selectChar('yellow')"></div>
<div class="char-btn" style="background: radial-gradient(circle at 30% 30%, #bae6fd, #0284c7);" data-color="blue" onclick="selectChar('blue')"></div>
<div class="char-btn" style="background: radial-gradient(circle at 30% 30%, #bbf7d0, #166534);" data-color="green" onclick="selectChar('green')"></div>
</div>
<div class="start-btn" onclick="startGame()">ENTER THE WORLD</div>
</div>
<div id="hud">
<div class="flex items-end gap-5 flex-wrap w-full">
<div id="ability-icon" class="w-16 h-16 bg-black/40 rounded-2xl flex items-center justify-center border-2 border-white/20 backdrop-blur-md shadow-[0_0_15px_rgba(0,0,0,0.5)]">
<span id="ability-text" class="text-xs font-bold text-white tracking-wider">NORMAL</span>
</div>
<div class="mb-1">
<div class="text-white/80 text-[10px] mb-1 font-bold tracking-widest">HP</div>
<div class="hp-bar-bg">
<div id="hp-fill" class="hp-bar-fill"></div>
</div>
</div>
<div class="mb-1">
<div class="text-white/80 text-[10px] mb-1 font-bold tracking-widest">WING STAMINA</div>
<div style="width: 120px; height: 12px; background: rgba(0,0,0,0.6); border: 2px solid rgba(255,255,255,0.8); border-radius: 6px; overflow: hidden; box-shadow: 0 0 10px rgba(0,0,0,0.5);">
<div id="wing-fill" style="height: 100%; background: linear-gradient(to right, #38bdf8, #818cf8); width: 100%; transition: width 0.1s; box-shadow: inset 0 0 5px rgba(255,255,255,0.8);"></div>
</div>
</div>
<div id="boss-hp-container" class="ml-auto mb-1" style="display: none; width: 30%;">
<div class="text-yellow-500 text-[10px] mb-1 font-bold tracking-widest text-right">BOSS</div>
<div style="width: 100%; height: 18px; background: rgba(0,0,0,0.8); border: 2px solid #eab308; border-radius: 9px; overflow: hidden; box-shadow: 0 0 15px rgba(234, 179, 8, 0.4);">
<div id="boss-hp-fill" style="height: 100%; background: linear-gradient(to right, #facc15, #b45309); width: 100%; transition: width 0.1s; box-shadow: inset 0 0 10px rgba(255,255,255,0.5);"></div>
</div>
</div>
</div>
<div class="absolute top-5 right-5 text-right z-20">
<div class="text-white text-3xl font-black drop-shadow-[0_2px_4px_rgba(0,0,0,0.8)] tracking-tighter" id="stage-text">STAGE 1</div>
<div id="score-text" class="text-white/90 text-lg font-bold drop-shadow-md tracking-wider font-mono">0000000</div>
<div class="text-white/60 text-xs font-bold drop-shadow-md mt-1">ONLINE: <span id="online-count">1</span></div>
</div>
</div>
<div id="stage-clear-screen" class="absolute inset-0 bg-white/10 backdrop-blur-lg flex flex-col items-center justify-center hidden z-40">
<h2 class="text-7xl font-black text-transparent bg-clip-text bg-gradient-to-b from-yellow-300 to-yellow-600 drop-shadow-[0_0_20px_rgba(250,204,21,0.6)] italic tracking-tighter">STAGE CLEAR</h2>
</div>
<div id="game-clear-screen" class="absolute inset-0 bg-black/80 backdrop-blur-xl flex flex-col items-center justify-center hidden z-50">
<h2 class="text-8xl font-black text-transparent bg-clip-text bg-gradient-to-b from-white to-blue-400 drop-shadow-[0_0_30px_rgba(255,255,255,0.8)] tracking-tighter">ALL CLEAR</h2>
<div class="start-btn" onclick="location.reload()">RETURN TO WORLD</div>
</div>
<div id="touch-ui">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;">
<div class="touch-btn" id="t-up" style="grid-column: 2; grid-row: 1; width:65px; height:65px;">↑</div>
<div class="touch-btn" id="t-left" style="grid-column: 1; grid-row: 2; width:65px; height:65px;">←</div>
<div class="touch-btn" id="t-right" style="grid-column: 3; grid-row: 2; width:65px; height:65px;">→</div>
<div class="touch-btn" id="t-down" style="grid-column: 2; grid-row: 3; width:65px; height:65px;">↓</div>
</div>
<div class="flex items-end gap-4 mb-4">
<div class="touch-btn text-xs flex-col" id="t-shift" style="width:70px; height:70px; background: rgba(168, 85, 247, 0.4); border-color: rgba(216, 180, 254, 0.6);"><span>WING</span></div>
<div class="touch-btn" id="t-x" style="width:80px; height:80px; font-size: 28px; background: rgba(239, 68, 68, 0.4); border-color: rgba(252, 165, 165, 0.6);">B</div>
<div class="touch-btn" id="t-z" style="width:80px; height:80px; font-size: 28px; background: rgba(59, 130, 246, 0.4); border-color: rgba(147, 197, 253, 0.6);">A</div>
</div>
</div>
</div>
</div>
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, doc, setDoc, onSnapshot, collection } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const GRAVITY = 0.45;
const FRICTION = 0.82;
const JUMP_FORCE = -10;
const ACCEL = 0.8;
const MAX_SPEED = 5.5;
const CHAR_COLORS = {
'pink': { body: '#ffb7c5', cheek: '#f43f5e', feet: '#e11d48', shadow: '#9f1239' },
'yellow': { body: '#fef08a', cheek: '#f59e0b', feet: '#ea580c', shadow: '#9a3412' },
'blue': { body: '#bae6fd', cheek: '#38bdf8', feet: '#0284c7', shadow: '#075985' },
'green': { body: '#bbf7d0', cheek: '#22c55e', feet: '#166534', shadow: '#14532d' }
};
let gameState = 'TITLE';
const keys = {};
// 超リアルな翼の描画(透過と重なりを表現)
function drawRealisticWing(ctx, flapAngle, isForeground) {
ctx.save();
if (isForeground) {
ctx.translate(-8, 0);
ctx.rotate((-flapAngle - 20) * Math.PI / 180);
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
} else {
ctx.translate(8, -5);
ctx.rotate((flapAngle + 20) * Math.PI / 180);
ctx.fillStyle = 'rgba(226, 232, 240, 0.8)'; // 奥は少し暗く透過
}
ctx.strokeStyle = 'rgba(148, 163, 184, 0.5)';
ctx.lineWidth = 1;
for (let i = 0; i < 5; i++) {
ctx.save();
ctx.rotate((-20 + i * 15) * Math.PI / 180);
// 羽のグラデーション
let grad = ctx.createLinearGradient(0, 0, -30, 0);
grad.addColorStop(0, ctx.fillStyle);
grad.addColorStop(1, 'rgba(255,255,255,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.ellipse(-14 - i * 2, -2 - i, 16 + i * 2, 5 + i * 0.5, 0, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
// 羽の芯
ctx.strokeStyle = 'rgba(255,255,255,0.8)';
ctx.beginPath(); ctx.moveTo(-2, -2 - i); ctx.lineTo(-26 - i*4, -2 - i); ctx.stroke();
ctx.restore();
}
ctx.restore();
}
class Camera {
constructor() {
this.baseX = 0; this.baseY = 0;
this.x = 0; this.y = 0;
this.lerp = 0.08; // カメラ追従を少し滑らかに
this.shakeX = 0; this.shakeY = 0;
this.shakeTimer = 0; this.shakeIntensity = 0;
}
shake(intensity, duration) {
this.shakeIntensity = intensity;
this.shakeTimer = duration;
}
update(targetX, targetY, mapW, mapH, boss) {
let tx = targetX - canvas.width / 2;
let ty = targetY - canvas.height / 2 - 20; // プレイヤーを少し下寄りに配置して空を広く
if (boss && boss.hp > 0) {
let bossAreaStartX = boss.x - canvas.width + 200;
if (tx > bossAreaStartX) tx = bossAreaStartX;
}
this.baseX += (tx - this.baseX) * this.lerp;
this.baseY += (ty - this.baseY) * this.lerp;
this.baseX = Math.max(0, Math.min(this.baseX, mapW - canvas.width));
this.baseY = Math.max(-200, Math.min(this.baseY, mapH - canvas.height)); // 上空にも少し行けるように
if (this.shakeTimer > 0) {
this.shakeX = (Math.random() - 0.5) * this.shakeIntensity;
this.shakeY = (Math.random() - 0.5) * this.shakeIntensity;
this.shakeTimer--;
this.shakeIntensity *= 0.85;
} else {
this.shakeX = 0; this.shakeY = 0;
}
this.x = this.baseX + this.shakeX;
this.y = this.baseY + this.shakeY;
}
}
class Kirby {
constructor(x, y, colorType = 'pink') {
this.x = x; this.y = y;
this.vx = 0; this.vy = 0;
this.w = 40; this.h = 40;
this.hp = 6; this.maxHp = 6;
this.facing = 1;
this.state = 'IDLE';
this.ability = 'NORMAL';
this.invincibility = 0;
this.onGround = false;
this.frame = 0;
this.swallowedType = null;
this.colorType = colorType;
this.isWinged = false;
this.prevShift = false;
this.wingStamina = 600;
this.maxWingStamina = 600;
this.aimY = 0;
}
update() {
this.frame++;
if (this.invincibility > 0) this.invincibility--;
let moveX = 0; let moveY = 0;
if (keys['ArrowLeft'] || keys['t-left']) moveX = -1;
if (keys['ArrowRight'] || keys['t-right']) moveX = 1;
if (keys['ArrowUp'] || keys['t-up']) moveY = -1;
if (keys['ArrowDown'] || keys['t-down']) moveY = 1;
const jumpTrigger = (keys['z'] || keys['t-z']) && !this.prevZ;
const actionTrigger = (keys['x'] || keys['t-x']) && !this.prevX;
const shiftTrigger = (keys['Shift'] || keys['t-shift']) && !this.prevShift;
this.prevZ = !!(keys['z'] || keys['t-z']);
this.prevX = !!(keys['x'] || keys['t-x']);
this.prevShift = !!(keys['Shift'] || keys['t-shift']);
let attackDirX = this.facing;
let attackDirY = 0;
this.aimY = 0;
if (keys['ArrowUp'] || keys['t-up']) {
attackDirX = 0; attackDirY = -1; this.aimY = -1;
}
if (shiftTrigger && this.wingStamina > 0) {
this.isWinged = !this.isWinged;
game.particles.push(new Particle(this.x+20, this.y+20, 'COPY_FLASH'));
}
if (this.isWinged) {
this.wingStamina -= 1;
if (this.wingStamina <= 0) {
this.wingStamina = 0; this.isWinged = false;
game.particles.push(new Particle(this.x+20, this.y+20, 'PUFF'));
}
if (moveX !== 0) {
this.vx += moveX * ACCEL; this.facing = moveX;
} else this.vx *= FRICTION;
if (moveY !== 0) this.vy += moveY * ACCEL;
else this.vy *= FRICTION;
this.state = 'HOVER';
if (actionTrigger) game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'AIR');
} else {
if (this.state === 'INHALE') {
this.vx *= 0.7;
if (!(keys['x'] || keys['t-x'])) this.state = 'IDLE';
if (this.frame % 3 === 0) {
if (this.aimY === -1) game.particles.push(new Particle(this.x + 20 + (Math.random()-0.5)*20, this.y - 30, 'INHALE_WIND', 0, -8));
else game.particles.push(new Particle(this.x + 20 + this.facing * 50, this.y + 20 + (Math.random()-0.5)*20, 'INHALE_WIND', -this.facing * 8, 0));
}
} else if (this.state === 'ATTACK') {
this.vx *= 0.8;
if (this.ability === 'FIRE' || this.ability === 'ICE' || this.ability === 'SPARK') {
if (!(keys['x'] || keys['t-x'])) this.state = 'IDLE';
if (this.ability === 'FIRE' && this.frame % 4 === 0) game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'FIRE');
else if (this.ability === 'ICE' && this.frame % 4 === 0) game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'ICE');
else if (this.ability === 'SPARK' && this.frame % 2 === 0) {
let sx = this.x + 20 + (Math.random() - 0.5) * 60;
let sy = this.y + 20 + (Math.random() - 0.5) * 60;
game.spawnBullet(sx, sy, attackDirX, attackDirY, 'SPARK');
}
} else {
this.attackTimer--;
if (this.attackTimer <= 0) this.state = 'IDLE';
}
} else {
if (moveX !== 0) {
this.vx += moveX * ACCEL; this.facing = moveX;
if (this.onGround && this.state !== 'FULL' && this.state !== 'HOVER') this.state = 'RUN';
} else {
this.vx *= FRICTION;
if (this.onGround && this.state !== 'FULL' && this.state !== 'HOVER') this.state = 'IDLE';
}
if (jumpTrigger) {
if (this.onGround) {
this.vy = JUMP_FORCE; this.onGround = false;
} else {
if (this.state === 'FULL') {
game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'STAR');
this.swallowedType = null;
}
this.state = 'HOVER'; this.vy = -5;
game.particles.push(new Particle(this.x+20, this.y+40, 'PUFF'));
}
}
if (actionTrigger) {
if (this.state === 'FULL') {
game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'STAR');
this.state = 'IDLE'; this.swallowedType = null;
game.camera.shake(4, 5); // 星を吐いた時の軽い振動
} else if (this.state === 'HOVER') {
game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'AIR');
this.state = 'IDLE';
} else if (this.ability !== 'NORMAL') {
this.state = 'ATTACK';
if (this.ability === 'SWORD') {
this.attackTimer = 15;
game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'SWORD_WAVE');
game.camera.shake(3, 5);
} else if (this.ability === 'CUTTER') {
this.attackTimer = 15;
game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'CUTTER');
} else if (this.ability === 'SUPER') {
this.attackTimer = 20;
game.spawnBullet(this.x + 20, this.y + 20, attackDirX, attackDirY, 'SUPER_STAR');
this.vx = -this.facing * 3;
game.camera.shake(12, 15); // スーパー弾の大振動
}
} else {
this.state = 'INHALE';
}
}
if (this.state === 'FULL' && (keys['ArrowDown'] || keys['t-down'])) {
if (this.swallowedType && this.swallowedType !== 'NORMAL') {
this.ability = this.swallowedType;
game.particles.push(new Particle(this.x+20, this.y+20, 'COPY_FLASH'));
game.camera.shake(5, 10);
} else {
this.ability = 'NORMAL';
}
updateAbilityUI(this.ability);
this.state = 'IDLE';
this.swallowedType = null;
}
}
}
if (!this.isWinged) {
let grav = (this.state === 'HOVER') ? GRAVITY * 0.3 : GRAVITY;
this.vy += grav;
}
if (!this.isWinged && this.onGround) {
this.wingStamina += 4;
if (this.wingStamina > this.maxWingStamina) this.wingStamina = this.maxWingStamina;
}
if (this.vx > MAX_SPEED) this.vx = MAX_SPEED;
if (this.vx < -MAX_SPEED) this.vx = -MAX_SPEED;
if (this.vy > MAX_SPEED * 1.5) this.vy = MAX_SPEED * 1.5;
if (this.vy < -MAX_SPEED * 1.5) this.vy = -MAX_SPEED * 1.5;
this.x += this.vx;
this.y += this.vy;
if (game.boss && game.boss.hp > 0) {
let camLeft = game.camera.x;
if (this.x < camLeft) this.x = camLeft;
}
let wasOnGround = this.onGround;
this.onGround = false;
for (let b of game.map.blocks) {
if (this.checkCollision(b)) {
if (this.vy > 0 && this.y + this.h - this.vy <= b.y + 10) {
this.y = b.y - this.h;
this.vy = 0;
this.onGround = true;
if (this.state === 'HOVER') this.state = 'IDLE';
if (!wasOnGround && this.vy > 5) game.particles.push(new Particle(this.x+20, this.y+40, 'PUFF')); // 着地土煙
} else if (this.vy < 0 && this.y - this.vy >= b.y + b.h - 10) {
this.y = b.y + b.h; this.vy = 0;
} else {
if (this.vx > 0) this.x = b.x - this.w;
else if (this.vx < 0) this.x = b.x + b.w;
this.vx = 0;
}
}
}
game.enemies.forEach(e => e.isBeingInhaled = false);
if (this.state === 'INHALE') {
game.enemies.forEach(e => {
let dx = (this.x + 20) - (e.x + 15);
let dy = (this.y + 20) - (e.y + 15);
let dist = Math.sqrt(dx*dx + dy*dy);
let validAngle = false;
if (this.aimY === -1) {
if (dy > 0 && Math.abs(dx) < dy) validAngle = true;
} else {
if (Math.sign(-dx) === this.facing && Math.abs(dy) < Math.abs(dx)) validAngle = true;
}
if (dist < 180 && (validAngle || dist < 40)) { // 吸い込み範囲を少し拡大
e.isBeingInhaled = true;
let pullStrength = (e.ability === 'NORMAL') ? 0.15 : 0.02;
e.x += (this.x + 20 - 15 - e.x) * pullStrength;
e.y += (this.y + 20 - 15 - e.y) * pullStrength;
if (dist < 30) {
e.hp = 0; this.state = 'FULL'; this.swallowedType = e.ability;
game.score += 100;
}
}
});
}
if (this.y > 650) this.takeDamage(this.hp);
document.getElementById('hp-fill').style.width = `${(this.hp / this.maxHp) * 100}%`;
document.getElementById('wing-fill').style.width = `${(this.wingStamina / this.maxWingStamina) * 100}%`;
document.getElementById('score-text').innerText = String(game.score).padStart(7, '0');
}
checkCollision(rect) {
return this.x < rect.x + rect.w && this.x + this.w > rect.x && this.y < rect.y + rect.h && this.y + this.h > rect.y;
}
takeDamage(n) {
if (this.invincibility > 0) return;
this.hp -= n;
this.invincibility = 60;
this.vy = -7;
this.vx = -this.facing * 6;
this.ability = 'NORMAL';
this.state = 'IDLE';
this.swallowedType = null;
updateAbilityUI('NORMAL');
game.camera.shake(15, 20);
// ダメージパーティクル(星が飛び散る)
for(let i=0; i<8; i++) game.particles.push(new Particle(this.x+20, this.y+20, 'STAR_PIECE', (Math.random()-0.5)*10, (Math.random()-1)*10));
if (this.hp <= 0) game.reset();
}
draw(ctx, cam) {
if (this.invincibility > 0 && (this.frame % 6 < 3)) return;
ctx.save();
ctx.translate(this.x - cam.x + 20, this.y - cam.y + 20);
// リアルなドロップシャドウ(影)
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.filter = 'blur(2px)'; // 影をぼかす
ctx.beginPath();
let shadowScale = this.onGround ? 1 : Math.max(0.3, 1 - (cam.y+20 - this.y)/100);
ctx.ellipse(0, 25 + (cam.y+20 > this.y ? 0 : this.y - cam.y), 20 * shadowScale, 6 * shadowScale, 0, 0, Math.PI*2);
ctx.fill();
ctx.filter = 'none';
if (this.facing === -1) ctx.scale(-1, 1);
if (this.isWinged) {
let flap = Math.sin(this.frame * 0.4) * 30;
drawRealisticWing(ctx, flap, false);
}
const colors = CHAR_COLORS[this.colorType] || CHAR_COLORS['pink'];
let walkCycle = 0;
if (this.onGround && Math.abs(this.vx) > 0.5) walkCycle = Math.sin(this.frame * 0.3) * 5;
else if (!this.onGround) walkCycle = -4;
// 足(リアルな立体感)
let drawFoot = (x, y) => {
let gradF = ctx.createRadialGradient(x, y-2, 2, x, y, 8);
gradF.addColorStop(0, 'rgba(255,255,255,0.7)');
gradF.addColorStop(0.3, colors.feet);
gradF.addColorStop(1, colors.shadow);
ctx.fillStyle = gradF;
ctx.beginPath(); ctx.ellipse(x, y, 9, 6, 0, 0, Math.PI*2); ctx.fill();
};
drawFoot(-8, 16 + walkCycle);
drawFoot(8, 16 - walkCycle);
let r = (this.state === 'FULL' || this.state === 'HOVER') ? 25 : 20;
// 体(究極の球体ライティング)
let gradBody = ctx.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r*1.1);
gradBody.addColorStop(0, '#ffffff'); // ハイライトコア
gradBody.addColorStop(0.1, 'rgba(255,255,255,0.8)');
gradBody.addColorStop(0.4, colors.body); // ベースカラー
gradBody.addColorStop(0.8, colors.shadow); // コアシャドウ
gradBody.addColorStop(1, colors.body); // 環境光の反射(リムライト)
ctx.fillStyle = gradBody;
ctx.beginPath(); ctx.arc(0, 0, r, 0, Math.PI*2); ctx.fill();
let faceOffsetY = 0;
if (this.aimY === -1 && (this.state === 'INHALE' || this.state === 'ATTACK')) faceOffsetY = -8;
ctx.fillStyle = colors.cheek;
ctx.beginPath(); ctx.ellipse(-10, faceOffsetY, 4, 2.5, -0.1, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(10, faceOffsetY, 4, 2.5, 0.1, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#1e293b'; // 目を少しネイビー寄りの黒に
if (this.state === 'INHALE' || this.state === 'ATTACK') {
ctx.beginPath(); ctx.ellipse(-5, -6 + faceOffsetY, 2.5, 7, Math.PI/8, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(5, -6 + faceOffsetY, 2.5, 7, -Math.PI/8, 0, Math.PI*2); ctx.fill();
} else {
ctx.beginPath(); ctx.ellipse(-5, -6 + faceOffsetY, 2.5, 7, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(5, -6 + faceOffsetY, 2.5, 7, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(-5, -9 + faceOffsetY, 1.5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(5, -9 + faceOffsetY, 1.5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#38bdf8'; // 瞳孔の下部反射
ctx.beginPath(); ctx.arc(-5, -4 + faceOffsetY, 1, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(5, -4 + faceOffsetY, 1, 0, Math.PI*2); ctx.fill();
}
if (this.state === 'INHALE' || this.state === 'ATTACK') {
ctx.fillStyle = '#450a0a';
ctx.beginPath(); ctx.arc(0, 4 + faceOffsetY, 8, 0, Math.PI*2); ctx.fill();
// 口の中の奥の暗闇
ctx.fillStyle = '#000';
ctx.beginPath(); ctx.arc(0, 4 + faceOffsetY, 5, 0, Math.PI*2); ctx.fill();
} else if (this.state === 'FULL' || this.state === 'HOVER') {
ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.moveTo(-4, 6 + faceOffsetY); ctx.lineTo(4, 6 + faceOffsetY); ctx.stroke();
} else {
ctx.strokeStyle = '#1e293b'; ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(0, 3 + faceOffsetY, 3, 0.1*Math.PI, 0.9*Math.PI); ctx.stroke();
// 笑った時の口の中
if (!this.onGround && this.vy > 0) {
ctx.fillStyle = '#e11d48';
ctx.beginPath(); ctx.arc(0, 5 + faceOffsetY, 2, 0, Math.PI*2); ctx.fill();
}
}
let armRot = 0;
if (this.state === 'INHALE' || this.state === 'ATTACK') armRot = (this.aimY === -1) ? -Math.PI / 1.5 : -Math.PI / 4;
else if (this.state === 'HOVER' && this.vy < 0) armRot = Math.PI / 2;
else if (!this.onGround) armRot = Math.PI / 4;
else armRot = Math.sin(this.frame * 0.1) * 0.2;
let drawArm = (x, y, rot) => {
ctx.save(); ctx.translate(x, y); ctx.rotate(rot);
let gradA = ctx.createRadialGradient(-2, -2, 1, 0, 0, 7);
gradA.addColorStop(0, 'rgba(255,255,255,0.7)'); gradA.addColorStop(0.5, colors.body); gradA.addColorStop(1, colors.shadow);
ctx.fillStyle = gradA;
ctx.beginPath(); ctx.ellipse(0, 0, 8, 6, 0, 0, Math.PI*2); ctx.fill();
ctx.restore();
};
ctx.save(); ctx.translate(12, 5);
if (this.ability === 'SWORD' && this.state !== 'FULL' && this.state !== 'INHALE' && this.state !== 'HOVER') {
let swordRot = armRot;
if (this.state === 'ATTACK') swordRot = (this.aimY === -1) ? -Math.PI / 2 : Math.PI / 2;
ctx.save();
ctx.rotate(swordRot);
ctx.fillStyle = '#f59e0b'; ctx.fillRect(0, -5, 12, 4);
// 剣の金属の質感(グラデーション)
let gradSword = ctx.createLinearGradient(12, -7, 12, 1);
gradSword.addColorStop(0, '#f8fafc'); gradSword.addColorStop(0.5, '#cbd5e1'); gradSword.addColorStop(1, '#64748b');
ctx.fillStyle = gradSword;
ctx.beginPath(); ctx.moveTo(12, -7); ctx.lineTo(38, -3); ctx.lineTo(12, 1); ctx.closePath(); ctx.fill();
// 剣の輝き
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(12, -6); ctx.lineTo(35, -3); ctx.stroke();
ctx.restore();
}
ctx.restore();
drawArm(14, 5, armRot); // 手前
drawArm(-14, 5, -armRot); // 奥
if (this.isWinged) {
let flap = Math.sin(this.frame * 0.4) * 30;
drawRealisticWing(ctx, flap, true);
}
// コピー能力の帽子のリッチ化
if (this.ability === 'FIRE') {
// 燃えるようなグラデーション
let gradHat = ctx.createLinearGradient(0, -35, 0, -15);
gradHat.addColorStop(0, '#fef08a'); gradHat.addColorStop(0.5, '#f97316'); gradHat.addColorStop(1, '#be123c');
ctx.fillStyle = gradHat;
ctx.beginPath(); ctx.moveTo(-15, -15); ctx.quadraticCurveTo(0, -20, 5, -40); ctx.quadraticCurveTo(5, -20, 15, -15); ctx.fill();
} else if (this.ability === 'SWORD') {
ctx.fillStyle = '#166534';
ctx.beginPath(); ctx.moveTo(-16, -12); ctx.lineTo(-12, -45); ctx.lineTo(20, -15); ctx.fill();
ctx.strokeStyle = '#facc15'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(-18, -12); ctx.lineTo(18, -12); ctx.stroke();
} else if (this.ability === 'ICE') {
// 氷の結晶のような半透明
ctx.fillStyle = 'rgba(186, 230, 253, 0.8)';
ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-10, -35); ctx.lineTo(-5, -20); ctx.lineTo(0, -40); ctx.lineTo(5, -20); ctx.lineTo(10, -35); ctx.lineTo(15, -15); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke();
} else if (this.ability === 'CUTTER') {
ctx.fillStyle = '#facc15'; ctx.beginPath(); ctx.arc(0, -15, 16, Math.PI, 0); ctx.fill();
let gradCutter = ctx.createLinearGradient(0, -50, 0, -31);
gradCutter.addColorStop(0, '#fff'); gradCutter.addColorStop(1, '#94a3b8');
ctx.fillStyle = gradCutter;
ctx.beginPath(); ctx.moveTo(-5, -31); ctx.lineTo(0, -55); ctx.lineTo(5, -31); ctx.fill();
} else if (this.ability === 'SPARK') {
ctx.fillStyle = '#a3e635';
ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-20, -25); ctx.lineTo(-10, -20); ctx.lineTo(0, -40); ctx.lineTo(10, -20); ctx.lineTo(20, -25); ctx.lineTo(15, -15); ctx.fill();
// バチバチのエフェクト
if(Math.random() < 0.3) { ctx.strokeStyle = '#fff'; ctx.beginPath(); ctx.moveTo(0, -40); ctx.lineTo((Math.random()-0.5)*20, -50); ctx.stroke(); }
} else if (this.ability === 'SUPER') {
// グローする虹色の王冠
ctx.shadowBlur = 10; ctx.shadowColor = '#fff';
ctx.fillStyle = `hsl(${this.frame * 15 % 360}, 100%, 60%)`;
ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-10, -30); ctx.lineTo(0, -15); ctx.lineTo(10, -30); ctx.lineTo(15, -15); ctx.fill();
ctx.shadowBlur = 0;
}
ctx.restore();
}
}
class Enemy {
constructor(x, y, type) {
this.x = x; this.y = y; this.startY = y;
this.w = 30; this.h = 30;
this.type = type;
const abilityMap = {
'FIRE_DUDE': 'FIRE', 'SWORD_DUDE': 'SWORD', 'ICE_DUDE': 'ICE', 'CUTTER_DUDE': 'CUTTER',
'SPARK_DUDE': 'SPARK', 'FLYING_DUDE': 'NORMAL', 'WADDLE': 'NORMAL', 'SUPER_DUDE': 'SUPER'
};
this.ability = abilityMap[type] || 'NORMAL';
this.hp = (type === 'SUPER_DUDE') ? 3 : 1;
this.vx = -1.2; this.vy = 0;
this.frame = 0;
this.isBeingInhaled = false;
this.isFlying = (type === 'FLYING_DUDE');
}
update() {
if (this.isBeingInhaled) return;
this.frame++;
if (this.isFlying) {
this.x += this.vx;
this.y = this.startY + Math.sin(this.frame * 0.05) * 40;
for (let b of game.map.blocks) {
if (this.x < b.x + b.w && this.x + this.w > b.x && this.y < b.y + b.h && this.y + this.h > b.y) {
this.x -= this.vx; this.vx *= -1;
}
}
return;
}
this.vy += GRAVITY;
if (this.vy > 10) this.vy = 10;
this.y += this.vy;
for (let b of game.map.blocks) {
if (this.x < b.x + b.w && this.x + this.w > b.x && this.y < b.y + b.h && this.y + this.h > b.y) {
if (this.vy > 0 && this.y + this.h - this.vy <= b.y + 15) { this.y = b.y - this.h; this.vy = 0; }
else if (this.vy < 0 && this.y - this.vy >= b.y + b.h - 15) { this.y = b.y + b.h; this.vy = 0; }
}
}
this.x += this.vx;
for (let b of game.map.blocks) {
if (this.x < b.x + b.w && this.x + this.w > b.x && this.y < b.y + b.h && this.y + this.h > b.y) {
if (this.vx > 0) { this.x = b.x - this.w; this.vx *= -1; }
else if (this.vx < 0) { this.x = b.x + b.w; this.vx *= -1; }
}
}
}
draw(ctx, cam) {
ctx.save();
ctx.translate(this.x - cam.x + 15, this.y - cam.y + 15);
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.filter = 'blur(2px)';
ctx.beginPath();
let shadowScale = this.isFlying ? 0.5 : 1;
ctx.ellipse(0, 20 + (this.isFlying ? 30 : 0), 16 * shadowScale, 4 * shadowScale, 0, 0, Math.PI*2);
ctx.fill();
ctx.filter = 'none';
let walkCycle = Math.sin(this.frame * 0.2) * 3;
if (this.isFlying) {
let flap = Math.sin(this.frame * 0.6) * 20;
ctx.save(); ctx.scale(0.7, 0.7);
drawRealisticWing(ctx, flap, false);
ctx.restore();
}
let gradFootE = ctx.createRadialGradient(0, 10, 1, 0, 12, 8);
gradFootE.addColorStop(0, 'rgba(255,255,255,0.7)'); gradFootE.addColorStop(1, 'rgba(0,0,0,0.6)');
if (!this.isFlying) {
ctx.fillStyle = gradFootE;
ctx.beginPath(); ctx.ellipse(-5, 12 + walkCycle, 6, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(5, 12 - walkCycle, 6, 4, 0, 0, Math.PI*2); ctx.fill();
} else {
ctx.fillStyle = gradFootE;
ctx.beginPath(); ctx.ellipse(-5, 12, 6, 4, -Math.PI/8, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(5, 12, 6, 4, Math.PI/8, 0, Math.PI*2); ctx.fill();
}
const colorMap = {
'FIRE': {b:'#f97316', s:'#9a3412'}, 'SWORD': {b:'#16a34a', s:'#14532d'},
'ICE': {b:'#0ea5e9', s:'#075985'}, 'CUTTER': {b:'#94a3b8', s:'#334155'},
'SPARK': {b:'#eab308', s:'#854d0e'}, 'NORMAL': {b: this.isFlying?'#fbcfe8':'#fb923c', s: this.isFlying?'#9d174d':'#9a3412'}
};
let rE = 15;
let eColor = colorMap[this.ability] || colorMap['NORMAL'];
if (this.ability === 'SUPER') {
eColor = {b: `hsl(${this.frame * 5 % 360}, 100%, 60%)`, s: '#000'};
ctx.shadowBlur = 15; ctx.shadowColor = eColor.b;
}
let gradEnemy = ctx.createRadialGradient(-rE*0.3, -rE*0.3, rE*0.1, 0, 0, rE*1.1);
gradEnemy.addColorStop(0, '#fff');
gradEnemy.addColorStop(0.2, eColor.b);
gradEnemy.addColorStop(0.8, eColor.s);
gradEnemy.addColorStop(1, eColor.b);
ctx.fillStyle = gradEnemy;
ctx.beginPath(); ctx.arc(0,0,rE,0,Math.PI*2); ctx.fill();
ctx.shadowBlur = 0;
if (this.isFlying) {
let flap = Math.sin(this.frame * 0.6) * 20;
ctx.save(); ctx.scale(0.7, 0.7);
drawRealisticWing(ctx, flap, true);
ctx.restore();
}
ctx.fillStyle = '#ffedd5';
ctx.beginPath(); ctx.ellipse(-2, 2, 10, 9, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#1e293b';
ctx.beginPath(); ctx.ellipse(-4, 0, 1.5, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(2, 0, 1.5, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
}
class Boss {
constructor(x, y, stageNum) {
this.x = x; this.y = y;
this.w = 120; this.h = 120; // ボスのサイズを大きく
this.stage = stageNum;
this.maxHp = 40 * stageNum; // ボスのHPさらにアップ
this.hp = this.maxHp;
this.vx = 0; this.vy = 0;
this.frame = 0;
this.actionTimer = 0;
this.invincibility = 0;
}
update() {
if (this.hp <= 0) return;
this.frame++; this.actionTimer++;
if (this.invincibility > 0) this.invincibility--;
let distToPlayer = game.player.x - this.x;
let dirToPlayer = Math.sign(distToPlayer) || -1;
if (this.stage === 1) {
this.vy += GRAVITY; if (this.y > 330) { this.y = 330; this.vy = 0; }
if (this.actionTimer > 90) {
game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0, 'FIRE', true);
this.actionTimer = 0;
}
} else if (this.stage === 2) {
this.y = 120 + Math.sin(this.frame * 0.04) * 60;
if (this.actionTimer > 70) {
game.spawnBullet(this.x + 60, this.y + 120, 0, 1.5, 'ICE', true);
this.actionTimer = 0;
}
} else if (this.stage === 3) {
this.vy += GRAVITY; if (this.y > 330) { this.y = 330; this.vy = 0; }
if (this.actionTimer > 50) {
game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0.2, 'CUTTER', true);
game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, -0.7, 'CUTTER', true);
this.actionTimer = 0;
}
} else if (this.stage === 4) {
this.vy += GRAVITY; if (this.y > 330) { this.y = 330; this.vy = 0; }
if (this.actionTimer > 60) {
for(let i=-1; i<=1; i++) game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, i*0.8, 'SPARK', true);
this.actionTimer = 0;
}
} else if (this.stage === 5) {
this.y = 80 + Math.sin(this.frame * 0.02) * 100;
if (this.actionTimer > 40) {
let r = Math.random();
if (r < 0.4) {
game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0.3, 'SUPER_STAR', true);
game.camera.shake(8, 15);
} else if (r < 0.7) {
for(let i=-1.5; i<=1.5; i+=0.5) game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, i, 'FIRE', true);
} else {
game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0.8, 'CUTTER', true);
game.spawnBullet(this.x + 60, this.y + 60, dirToPlayer, 0, 'CUTTER', true);
}
this.actionTimer = 0;
}
}
if (this.x < game.camera.x + 50) this.x = game.camera.x + 50;
if (this.x > game.map.w - 200) this.x = game.map.w - 200;
this.y += this.vy;
}
takeDamage(n) {
if (this.invincibility > 0) return;
this.hp -= n;
this.invincibility = 10;
game.particles.push(new Particle(this.x + 60, this.y + 60, 'HIT'));
game.camera.shake(12, 10);
if (this.hp <= 0) {
game.score += 10000;
for(let i=0; i<40; i++) game.particles.push(new Particle(this.x + 60, this.y + 60, 'STAR_PIECE', (Math.random()-0.5)*15, (Math.random()-0.5)*15));
game.camera.shake(30, 40); // ボス撃破時の超特大揺れ
}
}
draw(ctx, cam) {
if (this.hp <= 0) return;
if (this.invincibility > 0 && this.frame % 4 < 2) {
ctx.globalCompositeOperation = 'lighter'; // ダメージ時フラッシュ
}
ctx.save();
ctx.translate(this.x - cam.x + 60, this.y - cam.y + 60);
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.filter = 'blur(4px)';
ctx.beginPath(); ctx.ellipse(0, 65, 55, 15, 0, 0, Math.PI*2); ctx.fill();
ctx.filter = 'none';
let scale = 1.2 + Math.sin(this.frame * 0.1) * 0.05;
if (this.stage === 5) scale *= 1.6;
ctx.scale(scale, scale);
let colors = [
{b:'#f43f5e', s:'#881337'}, {b:'#38bdf8', s:'#082f49'}, {b:'#22c55e', s:'#14532d'},
{b:'#eab308', s:'#713f12'}, {b:'#111827', s:'#000000'}
];
let color = colors[this.stage - 1];
if (this.stage >= 3) {
ctx.shadowColor = (this.stage === 5) ? '#ef4444' : color.b;
ctx.shadowBlur = 30;
}
// リアルなドラゴンの関節尻尾
ctx.shadowBlur = 0;
let tailBaseX = 0, tailBaseY = 30;
for (let i = 1; i <= 10; i++) {
let tX = tailBaseX + Math.sin(this.frame * 0.05 - i * 0.3) * (20 + i*5);
let tY = tailBaseY + i * 14;
let r = Math.max(22 - i * 2, 3);
let gradT = ctx.createRadialGradient(tX-r*0.3, tY-r*0.3, r*0.1, tX, tY, r);
gradT.addColorStop(0, color.b); gradT.addColorStop(1, color.s);
ctx.fillStyle = gradT;
ctx.beginPath(); ctx.arc(tX, tY, r, 0, Math.PI*2); ctx.fill();
if (i % 2 === 0) {
ctx.fillStyle = '#facc15'; // 尻尾のトゲ
ctx.beginPath(); ctx.moveTo(tX - r, tY); ctx.lineTo(tX - r - 12, tY - 12); ctx.lineTo(tX, tY - r); ctx.fill();
ctx.beginPath(); ctx.moveTo(tX + r, tY); ctx.lineTo(tX + r + 12, tY - 12); ctx.lineTo(tX, tY - r); ctx.fill();
}
}
// 超絶リアルなドラゴンの翼
ctx.fillStyle = (this.stage === 5) ? '#450a0a' : '#1e293b';
ctx.strokeStyle = (this.stage === 5) ? '#fca5a5' : '#94a3b8';
let flap = Math.sin(this.frame * 0.12) * 20;
let drawDragonWing = (dir) => {
ctx.save(); ctx.scale(dir, 1);
ctx.lineWidth = 5;
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(-50, -50 - flap); ctx.lineTo(-120, -40 - flap * 1.5); ctx.stroke();
ctx.beginPath();
ctx.moveTo(0, 0); ctx.lineTo(-50, -50 - flap); ctx.lineTo(-120, -40 - flap * 1.5);
ctx.quadraticCurveTo(-100, 10, -110, 40); ctx.quadraticCurveTo(-80, 30, -60, 50); ctx.quadraticCurveTo(-40, 30, 0, 20);
ctx.fill();
ctx.lineWidth = 2; // 翼の骨格(被膜の筋)
ctx.beginPath(); ctx.moveTo(-50, -50 - flap); ctx.lineTo(-100, 10); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-50, -50 - flap); ctx.lineTo(-80, 30); ctx.stroke();
ctx.beginPath(); ctx.moveTo(-50, -50 - flap); ctx.lineTo(-40, 30); ctx.stroke();
ctx.restore();
};
drawDragonWing(1); drawDragonWing(-1);
// 体(重厚なグラデーション)
let gradB = ctx.createRadialGradient(-15, -15, 10, 0, 0, 50);
gradB.addColorStop(0, '#fff'); gradB.addColorStop(0.2, color.b); gradB.addColorStop(1, color.s);
ctx.fillStyle = gradB;
ctx.beginPath(); ctx.ellipse(0, 15, 35, 50, 0, 0, Math.PI*2); ctx.fill();
// 腹の鱗
ctx.fillStyle = '#fef08a';
ctx.beginPath(); ctx.ellipse(0, 20, 22, 40, 0, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#b45309'; ctx.lineWidth = 2;
for(let i=0; i<6; i++) {
ctx.beginPath(); ctx.moveTo(-18 + i*2, 5 + i*12); ctx.lineTo(18 - i*2, 5 + i*12); ctx.stroke();
}
// 首
let neckBaseX = 0, neckBaseY = -15;
let headY = -55 + Math.sin(this.frame * 0.1) * 10;
let headX = Math.cos(this.frame * 0.05) * 15;
for(let i=1; i<=4; i++) {
let nx = neckBaseX + (headX - neckBaseX) * (i/5);
let ny = neckBaseY + (headY - neckBaseY) * (i/5);
let rN = 20 - i*2;
let gradN = ctx.createRadialGradient(nx-rN*0.3, ny-rN*0.3, rN*0.1, nx, ny, rN);
gradN.addColorStop(0, color.b); gradN.addColorStop(1, color.s);
ctx.fillStyle = gradN;
ctx.beginPath(); ctx.arc(nx, ny, rN, 0, Math.PI*2); ctx.fill();
}
// 頭
ctx.save();
ctx.translate(headX, headY);
let gradHead = ctx.createRadialGradient(-5, -5, 5, 0, 0, 25);
gradHead.addColorStop(0, color.b); gradHead.addColorStop(1, color.s);
ctx.fillStyle = gradHead;
ctx.beginPath();
ctx.moveTo(-18, -12); ctx.lineTo(18, -12); ctx.lineTo(28, 12); ctx.lineTo(12, 28);
ctx.lineTo(-12, 28); ctx.lineTo(-28, 12); ctx.closePath();
ctx.fill();
ctx.fillStyle = (this.stage === 5) ? '#fca5a5' : '#fed7aa';
ctx.beginPath(); ctx.ellipse(0, 18, 20, 15, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#000'; // 鼻
ctx.beginPath(); ctx.arc(-8, 22, 3, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(8, 22, 3, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff'; // 鋭い牙
ctx.beginPath(); ctx.moveTo(-14, 30); ctx.lineTo(-8, 40); ctx.lineTo(-2, 30); ctx.fill();
ctx.beginPath(); ctx.moveTo(14, 30); ctx.lineTo(8, 40); ctx.lineTo(2, 30); ctx.fill();
ctx.fillStyle = '#facc15'; // 巨大な角
ctx.beginPath(); ctx.moveTo(-18, -12); ctx.quadraticCurveTo(-40, -40, -55, -25); ctx.quadraticCurveTo(-30, -10, -25, 5); ctx.fill();
ctx.beginPath(); ctx.moveTo(18, -12); ctx.quadraticCurveTo(40, -40, 55, -25); ctx.quadraticCurveTo(30, -10, 25, 5); ctx.fill();
if(this.stage === 5) { // 第3の角
ctx.beginPath(); ctx.moveTo(0, -18); ctx.lineTo(-8, -45); ctx.lineTo(8, -45); ctx.fill();
}
// 発光する目
ctx.shadowBlur = 15; ctx.shadowColor = (this.stage === 5) ? '#ef4444' : '#fff';
ctx.fillStyle = (this.stage === 5) ? '#ef4444' : '#fff';
ctx.beginPath(); ctx.ellipse(-12, -2, 8, 5, -Math.PI/6, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(12, -2, 8, 5, Math.PI/6, 0, Math.PI*2); ctx.fill();
ctx.shadowBlur = 0; ctx.fillStyle = '#000'; // 瞳孔
ctx.beginPath(); ctx.ellipse(-12, -2, 2, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(12, -2, 2, 4, 0, 0, Math.PI*2); ctx.fill();
ctx.restore();
ctx.restore();
ctx.globalCompositeOperation = 'source-over';
}
}
class Bullet {
constructor(x, y, dirX, dirY, type, isEnemy = false) {
this.x = x; this.y = y; this.dirX = dirX; this.dirY = dirY; this.type = type;
this.isEnemy = isEnemy;
this.life = 40;
this.dead = false;
this.timer = 0;
if (type === 'STAR') this.speed = 12;
else if (type === 'FIRE' || type === 'ICE') this.speed = 8;
else if (type === 'CUTTER') this.speed = 15;
else if (type === 'SPARK') this.speed = 0;
else if (type === 'SUPER_STAR') this.speed = 14;
else this.speed = 10;
this.vx = this.dirX * this.speed;
this.vy = this.dirY * this.speed;
if (type === 'FIRE' || type === 'ICE') {
this.vx += (Math.random() - 0.5) * 3;
this.vy += (Math.random() - 0.5) * 3;
}
if (type === 'SPARK') this.life = 12;
if (type === 'SUPER_STAR') this.life = 100;
}
update() {
this.timer++;
if (this.type === 'CUTTER') {
if (this.dirX !== 0) this.vx -= this.dirX * 0.5;
if (this.dirY !== 0) this.vy -= this.dirY * 0.5;
this.x += this.vx; this.y += this.vy;
if (this.timer > 15 && Math.abs(this.x - (game.player.x + 20)) < 30 && Math.abs(this.y - (game.player.y + 20)) < 30) this.dead = true;
this.life = 100;
if (this.timer > 100) this.dead = true;
} else {
this.x += this.vx; this.y += this.vy;
this.life--;
if (this.life <= 0) this.dead = true;
}
if (this.isEnemy) {
let hitbox = (this.type === 'SUPER_STAR') ? 50 : 30;
if (Math.abs(this.x - (game.player.x + 20)) < hitbox && Math.abs(this.y - (game.player.y + 20)) < hitbox) {
game.player.takeDamage((this.type === 'SUPER_STAR') ? 2 : 1);
this.dead = true;
}
} else {
game.enemies.forEach(e => {
if (e.hp > 0 && Math.abs(this.x - (e.x + 15)) < 40 && Math.abs(this.y - (e.y + 15)) < 40) {
e.hp -= (this.type === 'SUPER_STAR') ? 10 : 1;
if (this.type !== 'SWORD_WAVE' && this.type !== 'STAR' && this.type !== 'CUTTER' && this.type !== 'SUPER_STAR') this.dead = true;
if (e.hp <= 0) {
game.score += 200;
for(let i=0; i<5; i++) game.particles.push(new Particle(e.x+15, e.y+15, 'STAR_PIECE', (Math.random()-0.5)*10, (Math.random()-0.5)*10));
}
}
});
if (game.boss && game.boss.hp > 0) {
let hitboxSize = (this.type === 'SUPER_STAR') ? 90 : 60;
if (Math.abs(this.x - (game.boss.x + 60)) < hitboxSize && Math.abs(this.y - (game.boss.y + 60)) < hitboxSize) {
game.boss.takeDamage((this.type === 'SUPER_STAR') ? 5 : 1);
if (this.type !== 'SWORD_WAVE' && this.type !== 'STAR' && this.type !== 'CUTTER' && this.type !== 'SUPER_STAR') this.dead = true;
}
}
}
}
draw(ctx, cam) {
ctx.save();
ctx.translate(this.x - cam.x, this.y - cam.y);
let angle = Math.atan2(this.vy, this.vx);
// 圧倒的な光(ブルーム)を加算合成で表現
ctx.globalCompositeOperation = 'lighter';
if (this.type === 'STAR' || this.type === 'SUPER_STAR') {
ctx.rotate(this.life * 0.4 * (this.vx < 0 ? -1 : 1));
if (this.type === 'SUPER_STAR') {
ctx.scale(3, 3);
let hue = this.life * 20 % 360;
ctx.fillStyle = `hsl(${hue}, 100%, 70%)`;
// 巨大な光のオーラ
let gradG = ctx.createRadialGradient(0,0,5, 0,0,25);
gradG.addColorStop(0, `hsl(${hue}, 100%, 80%)`); gradG.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,25,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff';
} else {
ctx.fillStyle = '#fef08a';
let gradG = ctx.createRadialGradient(0,0,5, 0,0,20);
gradG.addColorStop(0, '#facc15'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,20,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff';
}
ctx.beginPath();
for (let i = 0; i < 5; i++) {
ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 16, -Math.sin((18 + i * 72) / 180 * Math.PI) * 16);
ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 8, -Math.sin((54 + i * 72) / 180 * Math.PI) * 8);
}
ctx.closePath(); ctx.fill();
} else if (this.type === 'CUTTER') {
ctx.rotate(this.timer * 0.6 * (this.vx < 0 ? -1 : 1));
let gradG = ctx.createRadialGradient(0,0,5, 0,0,20);
gradG.addColorStop(0, 'rgba(255,255,255,0.8)'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,20,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#e2e8f0'; ctx.lineWidth = 3; ctx.strokeStyle = '#fff';
ctx.beginPath(); ctx.arc(0, 0, 14, 0, Math.PI, false); ctx.lineTo(0, 6); ctx.closePath();
ctx.fill(); ctx.stroke();
} else if (this.type === 'SPARK') {
ctx.rotate(Math.random() * Math.PI * 2);
let gradG = ctx.createRadialGradient(0,0,5, 0,0,30);
gradG.addColorStop(0, '#fef08a'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,30,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(6, -6); ctx.lineTo(20, 0); ctx.lineTo(6, 6);
ctx.lineTo(0, 20); ctx.lineTo(-6, 6); ctx.lineTo(-20, 0); ctx.lineTo(-6, -6); ctx.closePath(); ctx.fill();
} else if (this.type === 'SWORD_WAVE') {
ctx.rotate(angle);
let gradG = ctx.createRadialGradient(0,0,10, 0,0,30);
gradG.addColorStop(0, '#4ade80'); gradG.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,30,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff';
ctx.beginPath(); ctx.arc(0, 0, 25, -Math.PI/2, Math.PI/2, false); ctx.quadraticCurveTo(15, 0, 0, -25); ctx.fill();
} else if (this.type === 'FIRE' || this.type === 'ICE') {
let color = (this.type === 'FIRE') ? '#f43f5e' : '#38bdf8';
let gradG = ctx.createRadialGradient(0,0,5, 0,0,25);
gradG.addColorStop(0, color); gradG.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = gradG; ctx.beginPath(); ctx.arc(0,0,25,0,Math.PI*2); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(0,0, 10, 0, Math.PI*2); ctx.fill();
} else {
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.beginPath(); ctx.arc(0,0, 12, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
}
class Particle {
constructor(x, y, type, vx = 0, vy = 0) {
this.x = x; this.y = y; this.type = type;
this.life = (type === 'ENV') ? Math.random()*100 + 50 : 25;
this.maxLife = this.life;
this.vx = vx; this.vy = vy;
// 環境パーティクル用
if (type === 'ENV') {
this.vx = (Math.random() - 0.5) * 2;
this.vy = (Math.random() - 0.5) * 2 - 1; // やや上に昇る
this.size = Math.random() * 4 + 1;
}
}
update() {
this.life--;
this.x += this.vx; this.y += this.vy;
if (this.type === 'STAR_PIECE') {
this.vy += GRAVITY * 0.5; // 重力
this.vx *= 0.95; // 空気抵抗
}
}
draw(ctx, cam) {
let alpha = this.life / this.maxLife;
ctx.save();
ctx.globalAlpha = alpha;
ctx.translate(this.x - cam.x, this.y - cam.y);
ctx.globalCompositeOperation = 'lighter';
if (this.type === 'HIT') {
ctx.rotate(this.life * 0.4);
ctx.fillStyle = '#fde047';
ctx.beginPath(); ctx.moveTo(0, -20); ctx.lineTo(5, -5); ctx.lineTo(20, 0); ctx.lineTo(5, 5);
ctx.lineTo(0, 20); ctx.lineTo(-5, 5); ctx.lineTo(-20, 0); ctx.lineTo(-5, -5); ctx.fill();
} else if (this.type === 'STAR_PIECE') {
ctx.rotate(this.life * 0.5);
ctx.fillStyle = `hsl(${Math.random()*60 + 30}, 100%, 70%)`; // 黄色〜オレンジ
ctx.beginPath();
for (let i = 0; i < 5; i++) {
ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 8, -Math.sin((18 + i * 72) / 180 * Math.PI) * 8);
ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 4, -Math.sin((54 + i * 72) / 180 * Math.PI) * 4);
}
ctx.closePath(); ctx.fill();
} else if (this.type === 'ENV') {
// ステージに応じた環境光の粒(STAGE 5は火の粉、他はホコリ/妖精の粉)
ctx.fillStyle = (game.stage === 5) ? '#ef4444' : '#7dd3fc';
ctx.beginPath(); ctx.arc(0, 0, this.size, 0, Math.PI*2); ctx.fill();
let grad = ctx.createRadialGradient(0,0,0, 0,0,this.size*3);
grad.addColorStop(0, ctx.fillStyle); grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(0, 0, this.size*3, 0, Math.PI*2); ctx.fill();
} else {
ctx.fillStyle = (this.type === 'COPY_FLASH') ? '#fff' : 'rgba(255,255,255,0.6)';
let size = (this.type === 'COPY_FLASH') ? (25-this.life)*30 : 6;
ctx.beginPath(); ctx.arc(0, 0, size, 0, Math.PI*2); ctx.fill();
}
ctx.restore();
}
}
class Item {
constructor(x, y, ability) {
this.x = x; this.y = y; this.w = 30; this.h = 30;
this.ability = ability; this.frame = 0;
}
update() {
this.frame++;
if (Math.abs(this.x + 15 - (game.player.x + 20)) < 30 && Math.abs(this.y + 15 - (game.player.y + 20)) < 30) {
if (keys['ArrowDown'] || keys['t-down']) {
if (game.player.ability !== this.ability) {
game.player.ability = this.ability;
updateAbilityUI(this.ability);
game.particles.push(new Particle(game.player.x + 20, game.player.y + 20, 'COPY_FLASH'));
game.camera.shake(6, 10);
}
}
}
}
draw(ctx, cam) {
ctx.save();
let floatY = Math.sin(this.frame * 0.08) * 5;
ctx.translate(this.x - cam.x + 15, this.y - cam.y + 15 + floatY);
// リッチな台座
ctx.fillStyle = '#64748b'; ctx.fillRect(-14, 10 - floatY, 28, 8);
ctx.fillStyle = '#334155'; ctx.fillRect(-18, 18 - floatY, 36, 6);
ctx.strokeStyle = '#94a3b8'; ctx.lineWidth=2; ctx.strokeRect(-18, 18 - floatY, 36, 6);
let color = '#fff';
if (this.ability === 'FIRE') color = '#f43f5e';
else if (this.ability === 'SWORD') color = '#22c55e';
else if (this.ability === 'ICE') color = '#38bdf8';
else if (this.ability === 'CUTTER') color = '#facc15';
else if (this.ability === 'SPARK') color = '#a3e635';
else if (this.ability === 'SUPER') color = `hsl(${this.frame * 15 % 360}, 100%, 60%)`;
// 発光するアイコン
ctx.globalCompositeOperation = 'lighter';
let grad = ctx.createRadialGradient(0,0,5, 0,0,25);
grad.addColorStop(0, color); grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(0,0,25,0,Math.PI*2); ctx.fill();
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = color;
ctx.beginPath(); ctx.arc(0, 0, 12, 0, Math.PI*2); ctx.fill();
ctx.lineWidth = 2; ctx.strokeStyle = '#fff'; ctx.stroke();
ctx.fillStyle = '#fff';
ctx.beginPath();
for (let i = 0; i < 5; i++) {
ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 6, -Math.sin((18 + i * 72) / 180 * Math.PI) * 6);
ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 3, -Math.sin((54 + i * 72) / 180 * Math.PI) * 3);
}
ctx.closePath(); ctx.fill();
ctx.fillStyle = 'rgba(255,255,255,0.9)';
ctx.font = '900 18px sans-serif'; ctx.textAlign = 'center';
ctx.fillText("↓", 0, -20);
ctx.restore();
}
}
const game = {
player: new Kirby(100, 300),
camera: new Camera(),
score: 0,
stage: 1,
maxStage: 5,
goal: null,
boss: null,
map: { w: 10000, h: 540, blocks: [] },
enemies: [], bullets: [], particles: [], items: [], envParticles: [],
remotePlayers: {}, userId: null, db: null,
appId: typeof __app_id !== 'undefined' ? __app_id : 'modern-kirby-123',
init() {
this.stage = 1; this.score = 0;
this.player = new Kirby(100, 100, this.selectedColor || 'pink');
this.loadStage(this.stage);
this.initMultiplayer();
},
loadStage(stageNum) {
this.map.blocks = []; this.enemies = []; this.bullets = [];
this.particles = []; this.items = []; this.envParticles = [];
this.boss = null;
this.player.x = 100; this.player.y = 100; this.player.vx = 0; this.player.vy = 0;
if (stageNum !== 5) this.map.blocks.push({ x: 0, y: 450, w: 20000, h: 100 });
else this.map.blocks.push({ x: 0, y: 600, w: 20000, h: 50 });
let mapLength = 3000 + stageNum * 1500;
this.map.w = mapLength + 1000;
// 環境パーティクルの初期化
for(let i=0; i<50; i++) {
this.envParticles.push(new Particle(Math.random()*this.map.w, Math.random()*this.map.h, 'ENV'));
}
const enemyTypes = ['WADDLE', 'WADDLE', 'WADDLE', 'WADDLE', 'WADDLE', 'FIRE_DUDE', 'SWORD_DUDE', 'ICE_DUDE', 'CUTTER_DUDE', 'SPARK_DUDE'];
if (stageNum === 1) {
for(let i=1; i<mapLength/600; i++) {
let cx = i * 600;
if (i % 2 === 0) this.map.blocks.push({ x: cx, y: 330, w: 150, h: 30 });
else this.map.blocks.push({ x: cx, y: 380, w: 100, h: 70 });
this.enemies.push(new Enemy(cx, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
if (Math.random() < 0.3) this.enemies.push(new Enemy(cx + 100, 150 + Math.random() * 100, 'FLYING_DUDE'));
if (Math.random() < 0.1) this.enemies.push(new Enemy(cx + 200, 100, 'SUPER_DUDE'));
}
} else if (stageNum === 2) {
for(let i=1; i<mapLength/500; i++) {
let cx = i * 500;
this.map.blocks.push({ x: cx, y: 350 + (i%3)*40 - 50, w: 120, h: 30 });
this.map.blocks.push({ x: cx + 250, y: 250 + (i%2)*50, w: 100, h: 30 });
this.enemies.push(new Enemy(cx, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
if(i%3===0) this.enemies.push(new Enemy(cx+250, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
if (Math.random() < 0.4) this.enemies.push(new Enemy(cx + 150, 100 + Math.random() * 100, 'FLYING_DUDE'));
if (Math.random() < 0.15) this.enemies.push(new Enemy(cx + 300, 100, 'SUPER_DUDE'));
}
} else if (stageNum === 3) {
for(let i=1; i<mapLength/400; i++) {
let cx = i * 400;
if(i%4===0) this.map.blocks.push({ x: cx, y: 200, w: 60, h: 250 });
else this.map.blocks.push({ x: cx, y: 300, w: 150, h: 30 });
this.enemies.push(new Enemy(cx + 50, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
this.enemies.push(new Enemy(cx + 250, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
if (Math.random() < 0.5) this.enemies.push(new Enemy(cx + 100, 150 + Math.random() * 150, 'FLYING_DUDE'));
if (Math.random() < 0.1) this.enemies.push(new Enemy(cx + 150, 100, 'SUPER_DUDE'));
}
} else if (stageNum === 4) {
for(let i=1; i<mapLength/600; i++) {
let cx = i * 600;
this.map.blocks.push({ x: cx, y: 350 + (i%2)*50, w: 120, h: 30 });
this.map.blocks.push({ x: cx + 300, y: 200 + (i%3)*50, w: 100, h: 30 });
this.enemies.push(new Enemy(cx, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
if (Math.random() < 0.5) this.enemies.push(new Enemy(cx + 300, 100, 'FLYING_DUDE'));
if (Math.random() < 0.2) this.enemies.push(new Enemy(cx + 150, 100, 'SUPER_DUDE'));
}
} else if (stageNum === 5) {
for(let i=1; i<mapLength/400; i++) {
let cx = i * 400;
this.map.blocks.push({ x: cx, y: 350 + (i%3)*40, w: 140, h: 30 });
this.enemies.push(new Enemy(cx + 50, 100, enemyTypes[Math.floor(Math.random() * enemyTypes.length)]));
if (Math.random() < 0.6) this.enemies.push(new Enemy(cx + 100, 100, 'FLYING_DUDE'));
if (Math.random() < 0.2) this.enemies.push(new Enemy(cx + 50, 100, 'SUPER_DUDE'));
}
this.map.blocks.push({ x: mapLength - 800, y: 350, w: 1000, h: 200 });
this.items.push(new Item(mapLength - 750, 300, 'FIRE'));
this.items.push(new Item(mapLength - 650, 300, 'ICE'));
this.items.push(new Item(mapLength - 550, 300, 'SWORD'));
this.items.push(new Item(mapLength - 450, 300, 'SPARK'));
this.items.push(new Item(mapLength - 350, 300, 'CUTTER'));
this.items.push(new Item(mapLength - 250, 300, 'SUPER'));
}
this.goal = { x: mapLength, y: 350, w: 80, h: 100 };
this.boss = new Boss(mapLength - 200, 200, stageNum);
document.getElementById('stage-text').innerText = `STAGE ${stageNum}`;
},
async initMultiplayer() {
if (typeof __firebase_config === 'undefined') return;
const app = initializeApp(JSON.parse(__firebase_config));
const auth = getAuth(app); this.db = getFirestore(app);
if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) await signInWithCustomToken(auth, __initial_auth_token);
else await signInAnonymously(auth);
onAuthStateChanged(auth, (user) => {
if (user) { this.userId = user.uid; this.listenPlayers(); }
});
},
listenPlayers() {
const ref = collection(this.db, 'artifacts', this.appId, 'public', 'data', 'players');
onSnapshot(ref, (snap) => {
snap.docChanges().forEach(change => {
if (change.doc.id === this.userId) return;
if (change.type === 'removed') delete this.remotePlayers[change.doc.id];
else this.remotePlayers[change.doc.id] = change.doc.data();
});
let count = 1;
for(let id in this.remotePlayers) if(this.remotePlayers[id].stage === this.stage) count++;
const onlineCountEl = document.getElementById('online-count');
if (onlineCountEl) onlineCountEl.innerText = count;
}, (error) => {
console.error("Firebase Snapshot Error:", error);
});
},
sync() {
if (!this.userId) return;
const ref = doc(this.db, 'artifacts', this.appId, 'public', 'data', 'players', this.userId);
setDoc(ref, {
x: this.player.x, y: this.player.y, ability: this.player.ability, facing: this.player.facing,
stage: this.stage, colorType: this.player.colorType, t: Date.now()
}, { merge: true });
},
spawnBullet(x, y, dirX, dirY, type, isEnemy = false) {
this.bullets.push(new Bullet(x, y, dirX, dirY, type, isEnemy));
},
reset() {
this.player.hp = 6; this.player.ability = 'NORMAL';
updateAbilityUI('NORMAL'); this.loadStage(this.stage);
},
handleStageClear() {
gameState = 'CLEAR';
if (this.stage >= this.maxStage) {
document.getElementById('game-clear-screen').style.display = 'flex';
document.getElementById('touch-ui').style.display = 'none';
} else {
const clearScreen = document.getElementById('stage-clear-screen');
clearScreen.style.display = 'flex';
setTimeout(() => {
this.stage++; this.player.hp = this.player.maxHp;
this.loadStage(this.stage);
clearScreen.style.display = 'none'; gameState = 'PLAYING';
}, 3000);
}
},
loop() {
if (gameState !== 'PLAYING') {
if (gameState === 'CLEAR') {
this.player.vx *= 0.8; this.player.x += this.player.vx;
this.draw(); requestAnimationFrame(() => this.loop());
}
return;
}
if (this.goal && (!this.boss || this.boss.hp <= 0) && this.player.x > this.goal.x && this.player.x < this.goal.x + this.goal.w && this.player.onGround) {
this.handleStageClear(); requestAnimationFrame(() => this.loop()); return;
}
this.player.update();
this.camera.update(this.player.x, this.player.y, this.map.w, this.map.h, this.boss);
if (this.boss) {
this.boss.update();
if (this.boss.hp > 0 && Math.abs((this.boss.x + 60) - (this.player.x + 20)) < 70 && Math.abs((this.boss.y + 60) - (this.player.y + 20)) < 70) this.player.takeDamage(1);
}
this.enemies = this.enemies.filter(e => e.hp > 0);
this.enemies.forEach(e => {
e.update();
if (Math.abs(e.x - this.player.x) < 30 && Math.abs(e.y - this.player.y) < 30) this.player.takeDamage(1);
});
this.bullets = this.bullets.filter(b => !b.dead);
this.bullets.forEach(b => b.update());
this.particles = this.particles.filter(p => p.life > 0);
this.particles.forEach(p => p.update());
// 環境パーティクルの補充
this.envParticles = this.envParticles.filter(p => p.life > 0);
this.envParticles.forEach(p => { p.update(); p.x += this.camera.shakeX * 0.5; }); // カメラ揺れに少し追従
while(this.envParticles.length < 50) {
this.envParticles.push(new Particle(this.camera.x + Math.random()*canvas.width, this.camera.y + Math.random()*canvas.height, 'ENV'));
}
this.items.forEach(i => i.update());
if (this.player.frame % 10 === 0) this.sync();
if (this.boss && this.boss.hp > 0) {
document.getElementById('boss-hp-container').style.display = 'block';
document.getElementById('boss-hp-fill').style.width = `${(this.boss.hp / this.boss.maxHp) * 100}%`;
} else {
document.getElementById('boss-hp-container').style.display = 'none';
}
this.draw();
requestAnimationFrame(() => this.loop());
},
draw() {
ctx.clearRect(0,0, canvas.width, canvas.height);
if (this.stage === 5) {
let gradBg = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradBg.addColorStop(0, '#1a0505'); gradBg.addColorStop(1, '#7f1d1d');
ctx.fillStyle = gradBg; ctx.fillRect(0,0, canvas.width, canvas.height);
// 超絶リアルな活火山(多重スクロール)
ctx.fillStyle = '#290000';
for(let i=0; i<15; i++) {
let mx = (i * 300 - this.camera.baseX * 0.15) % 3000;
if(mx < -400) mx += 3000;
ctx.beginPath(); ctx.moveTo(mx, 600 - this.camera.y); ctx.lineTo(mx + 150, 100 - this.camera.y + (i%3)*50); ctx.lineTo(mx + 300, 600 - this.camera.y); ctx.fill();
ctx.strokeStyle = '#dc2626'; ctx.lineWidth = 4;
ctx.beginPath(); ctx.moveTo(mx+150, 100 - this.camera.y + (i%3)*50); ctx.lineTo(mx+130+i*10, 300 - this.camera.y); ctx.stroke();
// 火山の煙
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(239,68,68,0.1)';
ctx.beginPath(); ctx.arc(mx+150, 50 - this.camera.y + (i%3)*50, 80, 0, Math.PI*2); ctx.fill();
ctx.globalCompositeOperation = 'source-over';
}
// マグマの海(ブルーム効果付き)
ctx.fillStyle = '#991b1b'; ctx.fillRect(0, 550 - this.camera.y, canvas.width, canvas.height);
ctx.fillStyle = '#f97316'; ctx.shadowBlur = 30; ctx.shadowColor = '#facc15';
for(let i=0; i<30; i++) {
let wx = (i * 60 - this.camera.x * 0.6) % 1500;
if (wx < -100) wx += 1500;
ctx.beginPath(); ctx.ellipse(wx, 550 - this.camera.y + Math.sin(this.player.frame*0.05 + i)*15, 60, 20, 0, Math.PI, false); ctx.fill();
}
ctx.shadowBlur = 0;
// マグマの照り返しフォグ
let gradFog = ctx.createLinearGradient(0, canvas.height, 0, canvas.height - 300);
gradFog.addColorStop(0, 'rgba(239,68,68,0.3)'); gradFog.addColorStop(1, 'rgba(239,68,68,0)');
ctx.fillStyle = gradFog; ctx.fillRect(0,0, canvas.width, canvas.height);
} else {
let gradBg = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradBg.addColorStop(0, '#38bdf8'); gradBg.addColorStop(1, '#f0f9ff');
ctx.fillStyle = gradBg; ctx.fillRect(0,0, canvas.width, canvas.height);
// 美しい連なる山脈と大気フォグ
for(let layer=0; layer<2; layer++) {
ctx.fillStyle = layer===0 ? '#94a3b8' : '#cbd5e1';
let speed = layer===0 ? 0.05 : 0.1;
for(let i=0; i<20; i++) {
let mx = (i * 200 - this.camera.baseX * speed) % 3000;
if(mx < -300) mx += 3000;
ctx.beginPath(); ctx.moveTo(mx, 500 - this.camera.y); ctx.lineTo(mx + 150, 200 - this.camera.y + layer*50 + (i%4)*40); ctx.lineTo(mx + 300, 500 - this.camera.y); ctx.fill();
}
// レイヤー間の空気の層(フォグ)
ctx.fillStyle = 'rgba(240,249,255,0.4)'; ctx.fillRect(0,0, canvas.width, canvas.height);
}
ctx.fillStyle = 'rgba(255,255,255,0.9)'; ctx.shadowBlur = 20; ctx.shadowColor = '#fff';
for(let i=0; i<15; i++) {
let cx = (i * 250 - this.camera.x * 0.2) % 2000;
if(cx < -100) cx += 2000;
ctx.beginPath(); ctx.arc(cx, 100 + (i%3)*40, 40, 0, Math.PI*2); ctx.arc(cx+40, 120 + (i%3)*40, 30, 0, Math.PI*2); ctx.arc(cx-40, 120 + (i%3)*40, 30, 0, Math.PI*2); ctx.fill();
}
ctx.shadowBlur = 0;
}
// 環境パーティクルの描画(奥)
this.envParticles.forEach(p => p.draw(ctx, this.camera));
// マップブロックの超リアル描画
this.map.blocks.forEach(b => {
let bx = b.x - this.camera.x; let by = b.y - this.camera.y;
if (this.stage === 5) {
ctx.fillStyle = '#0f0a0a'; ctx.fillRect(bx, by, b.w, b.h);
let gradEdge = ctx.createLinearGradient(0, by, 0, by + 10);
gradEdge.addColorStop(0, '#f87171'); gradEdge.addColorStop(1, '#7f1d1d');
ctx.fillStyle = gradEdge; ctx.fillRect(bx, by, b.w, 10);
ctx.strokeStyle = '#dc2626'; ctx.lineWidth = 2; ctx.strokeRect(bx, by, b.w, b.h);
} else {
ctx.fillStyle = '#713f12'; ctx.fillRect(bx, by, b.w, b.h);
// リアルな草のグラデーション
let gradGrass = ctx.createLinearGradient(0, by, 0, by + 20);
gradGrass.addColorStop(0, '#86efac'); gradGrass.addColorStop(0.5, '#22c55e'); gradGrass.addColorStop(1, '#14532d');
ctx.fillStyle = gradGrass; ctx.fillRect(bx, by, b.w, 20);
ctx.strokeStyle = '#000'; ctx.lineWidth = 3; ctx.strokeRect(bx, by, b.w, b.h);
}
});
if (this.goal && (!this.boss || this.boss.hp <= 0)) {
let gx = this.goal.x - this.camera.x; let gy = this.goal.y - this.camera.y;
ctx.fillStyle = '#0f172a'; ctx.fillRect(gx, gy, this.goal.w, this.goal.h);
ctx.fillStyle = '#020617'; ctx.fillRect(gx + 10, gy + 10, this.goal.w - 20, this.goal.h - 10);
ctx.save(); ctx.translate(gx + this.goal.w/2, gy + this.goal.h/2);
ctx.shadowBlur = 20; ctx.shadowColor = '#facc15';
ctx.fillStyle = '#fef08a'; ctx.beginPath();
for (let i = 0; i < 5; i++) {
ctx.lineTo(Math.cos((18 + i * 72) / 180 * Math.PI) * 20, -Math.sin((18 + i * 72) / 180 * Math.PI) * 20);
ctx.lineTo(Math.cos((54 + i * 72) / 180 * Math.PI) * 10, -Math.sin((54 + i * 72) / 180 * Math.PI) * 10);
}
ctx.closePath(); ctx.fill(); ctx.restore();
}
if (this.boss) this.boss.draw(ctx, this.camera);
this.enemies.forEach(e => e.draw(ctx, this.camera));
this.items.forEach(i => i.draw(ctx, this.camera));
for (let id in this.remotePlayers) {
let p = this.remotePlayers[id];
if (p.stage !== this.stage) continue;
const cColor = CHAR_COLORS[p.colorType] || CHAR_COLORS['pink'];
ctx.save(); ctx.globalAlpha = 0.5; ctx.translate(p.x - this.camera.x + 20, p.y - this.camera.y + 20);
ctx.fillStyle = cColor.body; ctx.beginPath(); ctx.arc(0,0,20,0,Math.PI*2); ctx.fill();
ctx.fillStyle = cColor.feet; ctx.beginPath(); ctx.ellipse(-8, 16, 9, 5, 0, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(8, 16, 9, 5, 0, 0, Math.PI*2); ctx.fill();
ctx.restore();
}
this.player.draw(ctx, this.camera);
this.bullets.forEach(b => b.draw(ctx, this.camera));
this.particles.forEach(p => p.draw(ctx, this.camera));
}
};
window.selectChar = (color) => {
document.querySelectorAll('.char-btn').forEach(b => b.classList.remove('selected'));
document.querySelector(`.char-btn[data-color="${color}"]`).classList.add('selected');
};
window.startGame = () => {
const selectedColor = document.querySelector('.char-btn.selected').dataset.color;
game.selectedColor = selectedColor;
document.getElementById('title-screen').style.display = 'none';
document.getElementById('hud').style.display = 'flex';
// 修正: タッチ対応端末であれば、画面の幅(縦持ち・横持ち)に関わらずコントローラーを表示する
if ('ontouchstart' in window || navigator.maxTouchPoints > 0) {
document.getElementById('touch-ui').style.display = 'flex';
}
gameState = 'PLAYING';
game.init(); game.loop();
};
function updateAbilityUI(ability) {
const text = document.getElementById('ability-text');
const icon = document.getElementById('ability-icon');
text.innerText = ability;
if (ability === 'FIRE') icon.style.background = 'radial-gradient(circle, #f43f5e, #9f1239)';
else if (ability === 'SWORD') icon.style.background = 'radial-gradient(circle, #22c55e, #14532d)';
else if (ability === 'ICE') icon.style.background = 'radial-gradient(circle, #38bdf8, #075985)';
else if (ability === 'CUTTER') icon.style.background = 'radial-gradient(circle, #cbd5e1, #334155)';
else if (ability === 'SPARK') icon.style.background = 'radial-gradient(circle, #eab308, #854d0e)';
else if (ability === 'SUPER') icon.style.background = 'linear-gradient(45deg, #f87171, #facc15, #4ade80, #38bdf8, #c084fc)';
else icon.style.background = 'rgba(0,0,0,0.4)';
}
window.addEventListener('keydown', e => {
keys[e.key] = true;
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' ', 'Shift'].includes(e.key)) e.preventDefault();
});
window.addEventListener('keyup', e => keys[e.key] = false);
const touchBtns = ['t-left', 't-right', 't-up', 't-down', 't-z', 't-x', 't-shift'];
touchBtns.forEach(id => {
const el = document.getElementById(id);
if (!el) return;
el.addEventListener('touchstart', e => { e.preventDefault(); keys[id] = true; }, { passive: false });
el.addEventListener('touchend', e => { e.preventDefault(); keys[id] = false; }, { passive: false });
// 修正: 指がボタンの外にスライドして離れた場合などにも確実に入力をリセットする
el.addEventListener('touchcancel', e => { e.preventDefault(); keys[id] = false; }, { passive: false });
});
</script>
</body>
</html>
カービー