<!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>恐竜王 - リアル無限サバイバル</title>
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
animation: {
'fade-in-up': 'fadeInUp 0.5s ease-out forwards',
'fade-in': 'fadeIn 0.5s ease-out forwards',
},
keyframes: {
fadeInUp: {
'0%': { opacity: '0', transform: 'translate(-50%, calc(-50% + 20px))' },
'100%': { opacity: '1', transform: 'translate(-50%, -50%)' },
},
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
}
}
}
}
}
</script>
<style>
body { margin: 0; overflow: hidden; background-color: #2a3d20; touch-action: none; }
/* カスタムアニメーション用のクラス補正 */
.centered-overlay { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
</style>
</head>
<body class="font-sans select-none text-white">
<div id="app" class="fixed inset-0 flex items-center justify-center">
<!-- Game Canvas -->
<canvas id="gameCanvas" class="absolute inset-0 z-0 transition-opacity duration-500 opacity-0 pointer-events-none"></canvas>
<!-- UI: Boss Warning -->
<div id="ui-boss-warning" class="centered-overlay z-40 flex flex-col items-center animate-pulse hidden">
<svg class="text-red-600 mb-2 w-16 h-16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<h2 class="text-4xl md:text-6xl font-black text-red-600 drop-shadow-[0_0_10px_rgba(220,38,38,0.8)] tracking-widest text-center">
WARNING<br><span id="boss-name-text">Spinosaurus</span>
</h2>
</div>
<!-- UI: Boss Defeated -->
<div id="ui-boss-defeated" class="centered-overlay z-40 flex flex-col items-center hidden" style="animation: fadeInUp 0.5s ease-out forwards;">
<svg class="text-amber-400 mb-2 w-16 h-16" fill="currentColor" viewBox="0 0 24 24"><path d="M21.5 5h-3.14a4.48 4.48 0 0 0-3.36-2H9a4.48 4.48 0 0 0-3.36 2H2.5A1.5 1.5 0 0 0 1 6.5v2.85a7 7 0 0 0 3.32 6.77L6 17.5V19a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-1.5l1.68-1.38A7 7 0 0 0 23 9.35V6.5A1.5 1.5 0 0 0 21.5 5zM4 14.28a5 5 0 0 1-1-4.93V7h2.64A12 12 0 0 0 5 11v3.28zM21 9.35a5 5 0 0 1-1 4.93v-3.28a12 12 0 0 0-.64-4H22v2.35z"/></svg>
<h2 class="text-4xl md:text-6xl font-black text-amber-400 drop-shadow-[0_0_10px_rgba(251,191,36,0.8)] tracking-widest text-center">
BOSS DEFEATED!<br>
<span class="text-2xl text-white">見事な狩りだ</span>
</h2>
</div>
<!-- UI: Stage Clear -->
<div id="ui-stage-clear" class="absolute inset-0 flex flex-col items-center justify-center z-40 bg-stone-950/70 backdrop-blur-sm hidden animate-fade-in">
<h2 class="text-5xl md:text-7xl font-black text-amber-500 drop-shadow-[0_0_15px_rgba(245,158,11,0.8)] tracking-widest text-center uppercase mb-4">
Stage Cleared
</h2>
<p id="next-level-text" class="text-2xl text-stone-200 tracking-widest uppercase animate-pulse">
Entering Level 2 ...
</p>
</div>
<!-- UI: Start Screen -->
<div id="ui-start" class="absolute inset-0 flex flex-col items-center justify-center p-4 bg-black/60 z-50">
<div class="bg-stone-900 p-8 rounded-xl shadow-2xl border-2 border-stone-700 text-center max-w-lg w-full">
<h1 class="text-5xl md:text-6xl font-black text-stone-300 mb-2 tracking-widest uppercase" style="font-family: Impact, sans-serif;">
Jungle Apex
</h1>
<p class="text-stone-500 mb-8 font-bold tracking-widest text-sm uppercase">果てしない生存競争</p>
<div class="text-left text-stone-400 bg-stone-950 p-6 rounded-lg mb-8 border border-stone-800">
<ul class="space-y-4 text-sm">
<li class="flex items-center">
<span class="w-8 h-8 flex items-center justify-center bg-stone-800 rounded mr-3">W</span>
<span><strong>移動:</strong> WASDキー または 矢印キー</span>
</li>
<li class="flex items-center">
<span class="w-16 h-8 flex items-center justify-center bg-stone-800 rounded mr-3 text-xs">SPACE</span>
<span><strong>噛み付く:</strong> スペースキー</span>
</li>
<li class="flex items-center">
<span class="w-16 h-8 flex items-center justify-center bg-stone-800 rounded mr-3 text-xs">SHIFT</span>
<span><strong>尻尾攻撃:</strong> Shiftキー または Cキー</span>
</li>
<li class="flex items-center text-amber-600/80">
<span class="text-xl mr-3">🍖</span>
<span>獲物を狩り、肉を喰らい、スコア1000を目指せ。</span>
</li>
</ul>
</div>
<button id="btn-start" class="w-full bg-stone-800 hover:bg-stone-700 text-stone-200 font-bold py-4 px-8 rounded text-xl transition-all border border-stone-600 flex items-center justify-center group uppercase tracking-widest">
<svg class="mr-3 w-6 h-6" fill="currentColor" viewBox="0 0 24 24"><path d="M5 3l14 9-14 9V3z"/></svg>
Hunt Begins
</button>
</div>
</div>
<!-- UI: Playing HUD -->
<div id="ui-playing" class="absolute top-0 left-0 w-full p-4 flex justify-between items-start pointer-events-none z-10 hidden">
<div class="bg-stone-950/80 p-3 rounded border border-stone-800 pointer-events-auto backdrop-blur-sm">
<div class="flex items-center gap-2 mb-2">
<svg class="text-stone-400 w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
<span class="text-stone-400 font-bold text-sm tracking-widest uppercase">Health</span>
<span id="hp-text" class="text-stone-300 font-mono font-bold ml-2">100</span>
</div>
<div class="w-32 md:w-48 bg-stone-900 h-2 overflow-hidden border border-stone-800">
<div id="hp-bar" class="bg-stone-500 h-full transition-all duration-300" style="width: 100%;"></div>
</div>
</div>
<div class="bg-stone-950/80 p-3 px-6 rounded border border-stone-800 pointer-events-auto flex flex-col items-center backdrop-blur-sm absolute left-1/2 transform -translate-x-1/2">
<span class="text-amber-500/80 text-xs font-bold tracking-widest uppercase mb-1">Level</span>
<span id="level-text" class="text-amber-400 font-mono font-black text-2xl leading-none">1</span>
</div>
<div class="bg-stone-950/80 p-3 px-6 rounded border border-stone-800 pointer-events-auto flex flex-col items-center backdrop-blur-sm">
<span class="text-stone-500 text-xs font-bold tracking-widest uppercase mb-1">Score</span>
<span id="score-text" class="text-stone-300 font-mono font-black text-2xl leading-none">0</span>
</div>
</div>
<!-- UI: Game Over -->
<div id="ui-gameover" class="absolute inset-0 flex items-center justify-center bg-stone-950/90 backdrop-blur-sm z-50 hidden">
<div class="bg-stone-900 p-8 rounded-xl shadow-2xl border border-stone-800 text-center max-w-sm w-full" style="animation: fadeInUp 0.5s ease-out forwards;">
<div class="text-stone-500 text-6xl mb-6 flex justify-center">💀</div>
<h2 class="text-4xl font-black text-stone-400 mb-2 uppercase tracking-widest">Extinct</h2>
<p class="text-stone-600 mb-8 text-sm">食物連鎖の敗者となった</p>
<div class="bg-stone-950 p-4 rounded mb-8 border border-stone-800">
<span class="text-stone-500 text-xs tracking-widest uppercase block mb-2">Final Score</span>
<div id="final-score-text" class="flex justify-center items-center text-3xl font-mono font-bold text-stone-300">
0
</div>
</div>
<button id="btn-retry" class="w-full bg-stone-800 hover:bg-stone-700 text-stone-300 font-bold py-4 px-8 rounded transition-all flex items-center justify-center uppercase tracking-widest text-sm">
<svg class="mr-3 w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>
Rebirth
</button>
</div>
</div>
<!-- UI: Mobile Controls -->
<div id="ui-mobile-controls" class="md:hidden absolute bottom-0 left-0 w-full h-48 pointer-events-none z-20 hidden">
<div class="absolute bottom-8 left-4 w-32 h-32 bg-stone-900/60 rounded-full border border-stone-700 pointer-events-auto backdrop-blur-md">
<button id="btn-up" class="absolute top-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-stone-500/20 rounded-t active:bg-stone-500/50"></button>
<button id="btn-down" class="absolute bottom-0 left-1/2 -translate-x-1/2 w-10 h-10 bg-stone-500/20 rounded-b active:bg-stone-500/50"></button>
<button id="btn-left" class="absolute left-0 top-1/2 -translate-y-1/2 w-10 h-10 bg-stone-500/20 rounded-l active:bg-stone-500/50"></button>
<button id="btn-right" class="absolute right-0 top-1/2 -translate-y-1/2 w-10 h-10 bg-stone-500/20 rounded-r active:bg-stone-500/50"></button>
</div>
<button id="btn-tail" class="absolute bottom-10 right-28 w-16 h-16 bg-stone-700/80 border border-stone-500 rounded-full active:bg-stone-600 active:scale-95 pointer-events-auto backdrop-blur-md flex items-center justify-center uppercase text-xs tracking-widest text-stone-300 font-bold">
Tail
</button>
<button id="btn-bite" class="absolute bottom-10 right-4 w-20 h-20 bg-stone-800/80 border border-stone-600 rounded-full active:bg-stone-700 active:scale-95 pointer-events-auto backdrop-blur-md flex items-center justify-center uppercase text-sm tracking-widest text-stone-300 font-bold">
Bite
</button>
</div>
</div>
<script>
// --- ゲーム定数 ---
const TILE_SIZE = 150;
const MAX_ENEMIES = 30;
const SPAWN_RADIUS = 1200;
const DESPAWN_RADIUS = 1800;
const SCORE_TO_BOSS = 1000;
// --- サウンドエンジン ---
let audioCtx = null;
const initAudio = () => {
try {
if (!audioCtx && (window.AudioContext || window.webkitAudioContext)) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
} catch (e) {
console.error("Audio init error", e);
}
};
const playSound = (type) => {
try {
if (!audioCtx) return;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
switch (type) {
case 'bite':
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(400, t); osc.frequency.exponentialRampToValueAtTime(100, t + 0.1);
gain.gain.setValueAtTime(0.1, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
osc.start(t); osc.stop(t + 0.1); break;
case 'tail':
osc.type = 'sine'; osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(40, t + 0.2);
gain.gain.setValueAtTime(0.2, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.2);
osc.start(t); osc.stop(t + 0.2); break;
case 'hit':
osc.type = 'square'; osc.frequency.setValueAtTime(150, t); osc.frequency.exponentialRampToValueAtTime(40, t + 0.15);
gain.gain.setValueAtTime(0.2, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.15);
osc.start(t); osc.stop(t + 0.15); break;
case 'damage':
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(200, t); osc.frequency.linearRampToValueAtTime(50, t + 0.3);
gain.gain.setValueAtTime(0.3, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.3);
const osc2 = audioCtx.createOscillator(); osc2.type = 'square'; osc2.frequency.setValueAtTime(100, t);
osc2.connect(gain); osc2.start(t); osc2.stop(t + 0.3);
osc.start(t); osc.stop(t + 0.3); break;
case 'eat':
osc.type = 'sine'; osc.frequency.setValueAtTime(600, t); osc.frequency.linearRampToValueAtTime(800, t + 0.1);
gain.gain.setValueAtTime(0.1, t); gain.gain.linearRampToValueAtTime(0, t + 0.1);
osc.start(t); osc.stop(t + 0.1); break;
case 'roar':
osc.type = 'sawtooth'; osc.frequency.setValueAtTime(80, t); osc.frequency.exponentialRampToValueAtTime(30, t + 2.5);
const lfo = audioCtx.createOscillator(); lfo.type = 'sine'; lfo.frequency.value = 25;
const lfoGain = audioCtx.createGain(); lfoGain.gain.value = 30; lfo.connect(lfoGain); lfoGain.connect(osc.frequency);
gain.gain.setValueAtTime(0, t); gain.gain.linearRampToValueAtTime(0.6, t + 0.2); gain.gain.linearRampToValueAtTime(0.4, t + 1.5); gain.gain.linearRampToValueAtTime(0, t + 2.5);
lfo.start(t); osc.start(t); lfo.stop(t + 2.5); osc.stop(t + 2.5); break;
case 'alert':
osc.type = 'square'; osc.frequency.setValueAtTime(400, t); osc.frequency.setValueAtTime(600, t + 0.5);
osc.frequency.setValueAtTime(400, t + 1.0); osc.frequency.setValueAtTime(600, t + 1.5);
gain.gain.setValueAtTime(0.15, t); gain.gain.linearRampToValueAtTime(0, t + 2.0);
osc.start(t); osc.stop(t + 2.0); break;
case 'levelup':
osc.type = 'sine'; osc.frequency.setValueAtTime(440, t); osc.frequency.setValueAtTime(554, t + 0.15);
osc.frequency.setValueAtTime(659, t + 0.3); osc.frequency.setValueAtTime(880, t + 0.45);
gain.gain.setValueAtTime(0, t); gain.gain.linearRampToValueAtTime(0.15, t + 0.1); gain.gain.linearRampToValueAtTime(0, t + 0.8);
osc.start(t); osc.stop(t + 0.8); break;
case 'step':
osc.type = 'sine'; osc.frequency.setValueAtTime(60, t); osc.frequency.exponentialRampToValueAtTime(20, t + 0.1);
gain.gain.setValueAtTime(0.15, t); gain.gain.exponentialRampToValueAtTime(0.01, t + 0.1);
osc.start(t); osc.stop(t + 0.1); break;
}
} catch(e) {}
};
// --- 色設定 ---
const COLORS = {
grass: '#5c8a3f', grassDark: '#456b2d', player: '#e68a35', playerStripe: '#8a4b18',
raptor: '#7aa385', raptorStripe: '#4d6954', trike: '#b0b59a', trikeDetail: '#7a7d69',
bossSpino: '#768a94', bossSpinoSail: '#de5454', bossAnkylo: '#877053', bossAnkyloArmor: '#4a3b2b',
bossCarno: '#a34736', bossBrachio: '#697a6c', dust: '#c7b497', meat: '#e84f4f', bone: '#ffffff'
};
const randomRange = (min, max) => Math.random() * (max - min) + min;
const distance = (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1);
const hashCoord = (x, y) => Math.abs(Math.sin(x * 12.9898 + y * 78.233) * 43758.5453);
// --- ゲーム状態管理 ---
const engine = {
gameState: 'start', // start, playing, gameover
keys: {},
time: 0,
level: 1,
nextBossScore: SCORE_TO_BOSS,
currentBossName: '',
player: { x: 0, y: 0, radius: 25, speed: 7, hp: 100, maxHp: 100, dir: 1, isMoving: false, isAttacking: false, attackTimer: 0, isTailAttacking: false, tailAttackTimer: 0, isRoaring: false, roarTimer: 0 },
enemies: [], obstacles: [], items: [], particles: [],
camera: { x: 0, y: 0 },
lastScoreUpdate: 0,
bossSpawned: false, bossActive: false,
isMobileTouch: { up: false, down: false, left: false, right: false, attack: false, tail: false }
};
let animationFrameId = null;
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// UI制御関数
function updateUI() {
document.getElementById('ui-start').classList.toggle('hidden', engine.gameState !== 'start');
document.getElementById('ui-playing').classList.toggle('hidden', engine.gameState !== 'playing');
document.getElementById('ui-gameover').classList.toggle('hidden', engine.gameState !== 'gameover');
const isMobile = window.innerWidth < 768;
document.getElementById('ui-mobile-controls').classList.toggle('hidden', engine.gameState !== 'playing' || !isMobile);
if (engine.gameState === 'start') {
canvas.style.opacity = '0';
} else {
canvas.style.opacity = '1';
document.getElementById('hp-text').innerText = Math.max(0, Math.floor(engine.player.hp));
document.getElementById('hp-bar').style.width = Math.max(0, (engine.player.hp / engine.player.maxHp) * 100) + '%';
document.getElementById('score-text').innerText = engine.lastScoreUpdate;
document.getElementById('level-text').innerText = engine.level;
document.getElementById('final-score-text').innerText = engine.lastScoreUpdate;
}
}
// --- ゲーム初期化 ---
function initGame() {
initAudio();
engine.gameState = 'playing';
engine.level = 1;
engine.nextBossScore = SCORE_TO_BOSS;
engine.currentBossName = '';
engine.player = { x: 0, y: 0, radius: 25, speed: 7, hp: 100, maxHp: 100, dir: 1, isMoving: false, isAttacking: false, attackTimer: 0, isTailAttacking: false, tailAttackTimer: 0, isRoaring: false, roarTimer: 0 };
engine.enemies = []; engine.obstacles = []; engine.items = []; engine.particles = [];
engine.time = 0; engine.bossSpawned = false; engine.bossActive = false; engine.lastScoreUpdate = 0;
engine.camera = { x: 0, y: 0 };
for (let i = 0; i < 150; i++) spawnObstacle(engine, 0, 0, SPAWN_RADIUS, 300);
for (let i = 0; i < 15; i++) spawnEnemy(engine, 0, 0, SPAWN_RADIUS, 400);
document.getElementById('ui-boss-warning').classList.add('hidden');
document.getElementById('ui-boss-defeated').classList.add('hidden');
document.getElementById('ui-stage-clear').classList.add('hidden');
updateUI();
}
function spawnObstacle(state, px, py, maxRadius, minRadius = 0) {
const angle = Math.random() * Math.PI * 2; const r = randomRange(minRadius, maxRadius);
const type = Math.random() > 0.5 ? 'tree' : 'rock';
state.obstacles.push({ x: px + Math.cos(angle) * r, y: py + Math.sin(angle) * r, radius: type === 'tree' ? randomRange(20, 50) : randomRange(15, 60), type: type });
}
function spawnEnemy(state, px, py, maxRadius, minRadius = 0) {
const angle = Math.random() * Math.PI * 2; const r = randomRange(minRadius || maxRadius * 0.5, maxRadius);
const type = Math.random() > 0.6 ? 'trike' : 'raptor';
const hpMulti = 1 + (state.level - 1) * 0.2; const speedMulti = 1 + (state.level - 1) * 0.05;
state.enemies.push({
id: Math.random().toString(), type: type, x: px + Math.cos(angle) * r, y: py + Math.sin(angle) * r,
radius: type === 'trike' ? 35 : 18, speed: (type === 'trike' ? 2.5 : 5.5) * speedMulti,
hp: (type === 'trike' ? 60 : 30) * hpMulti, maxHp: (type === 'trike' ? 60 : 30) * hpMulti,
dir: 1, isMoving: true, targetX: null, targetY: null, stateTimer: 0, isBoss: false
});
}
function spawnBoss(state) {
state.bossSpawned = true; state.bossActive = true;
const bossIndex = (state.level - 1) % 4;
let bossType = 'spino'; let radius = 55; let speed = 4.2; let hp = 350; let bossNameStr = 'Spinosaurus';
if (bossIndex === 0) { bossType = 'spino'; radius = 55; speed = 4.2; hp = 350; bossNameStr = 'Spinosaurus'; }
else if (bossIndex === 1) { bossType = 'ankylosaurus'; radius = 48; speed = 2.5; hp = 600; bossNameStr = 'Ankylosaurus'; }
else if (bossIndex === 2) { bossType = 'carno'; radius = 40; speed = 6.8; hp = 250; bossNameStr = 'Carnotaurus'; }
else if (bossIndex === 3) { bossType = 'brachio'; radius = 80; speed = 1.5; hp = 1000; bossNameStr = 'Brachiosaurus'; }
state.currentBossName = bossNameStr;
document.getElementById('boss-name-text').innerText = bossNameStr;
document.getElementById('ui-boss-warning').classList.remove('hidden');
playSound('alert');
setTimeout(() => document.getElementById('ui-boss-warning').classList.add('hidden'), 4000);
const angle = Math.random() * Math.PI * 2; const r = SPAWN_RADIUS * 0.8;
const hpMulti = 1 + (state.level - 1) * 0.4; const speedMulti = 1 + (state.level - 1) * 0.05;
state.enemies.push({
id: `boss_${bossType}_${Date.now()}`, type: bossType,
x: state.player.x + Math.cos(angle) * r, y: state.player.y + Math.sin(angle) * r,
radius: radius, speed: speed * speedMulti, hp: hp * hpMulti, maxHp: hp * hpMulti,
dir: 1, isMoving: true, targetX: null, targetY: null, stateTimer: 0, isBoss: true
});
}
function handleBossDefeat(state, p) {
state.bossActive = false;
p.isRoaring = true; p.roarTimer = 300;
playSound('roar');
document.getElementById('ui-boss-defeated').classList.remove('hidden');
setTimeout(() => {
document.getElementById('ui-boss-defeated').classList.add('hidden');
document.getElementById('next-level-text').innerText = `Entering Level ${state.level + 1} ...`;
document.getElementById('ui-stage-clear').classList.remove('hidden');
playSound('levelup');
setTimeout(() => {
document.getElementById('ui-stage-clear').classList.add('hidden');
state.level += 1;
state.nextBossScore = state.lastScoreUpdate + SCORE_TO_BOSS * state.level;
state.bossSpawned = false;
p.maxHp += 20; p.hp = p.maxHp;
updateUI();
}, 3000);
}, 4000);
}
function spawnMeat(state, x, y, isBoss) {
state.items.push({ x, y, radius: isBoss ? 20 : 10, type: 'meat', floatOffset: 0, heal: isBoss ? 100 : 25 });
}
function spawnDust(state, x, y, count) {
for (let i = 0; i < count; i++) {
state.particles.push({ x, y, vx: randomRange(-6, 6), vy: randomRange(-6, 6), life: 1.0, color: COLORS.dust });
}
}
function tailAttack(state) {
const p = state.player; p.isTailAttacking = true; p.tailAttackTimer = 20; playSound('tail');
const attackRange = 70; const hitX = p.x - (p.dir * attackRange); const hitY = p.y; const hitRadius = 75;
if (Math.random() > 0.3) spawnDust(state, hitX, hitY, 5);
let isHit = false;
state.obstacles = state.obstacles.filter(obs => {
if (distance(hitX, hitY, obs.x, obs.y) < hitRadius + obs.radius) { spawnDust(state, obs.x, obs.y, 15); isHit = true; return false; }
return true;
});
state.enemies.forEach(enemy => {
if (distance(hitX, hitY, enemy.x, enemy.y) < hitRadius + enemy.radius) {
enemy.hp -= 20; spawnDust(state, enemy.x, enemy.y, 15);
const knockback = enemy.type === 'brachio' ? 5 : (enemy.isBoss ? 15 : 60);
enemy.x -= p.dir * knockback; isHit = true;
if (enemy.hp <= 0) {
spawnMeat(state, enemy.x, enemy.y, enemy.isBoss);
state.lastScoreUpdate += (enemy.isBoss ? 2000 : (enemy.type === 'trike' ? 50 : 30));
if (enemy.isBoss) handleBossDefeat(state, p);
}
}
});
if (isHit) playSound('hit');
updateUI();
}
function attack(state) {
const p = state.player; p.isAttacking = true; p.attackTimer = 15; playSound('bite');
const attackRange = 60; const hitX = p.x + (p.dir * attackRange); const hitY = p.y; const hitRadius = 55;
if (Math.random() > 0.5) spawnDust(state, hitX, hitY, 3);
let isHit = false;
state.obstacles = state.obstacles.filter(obs => {
if (distance(hitX, hitY, obs.x, obs.y) < hitRadius + obs.radius) { spawnDust(state, obs.x, obs.y, 15); isHit = true; return false; }
return true;
});
state.enemies.forEach(enemy => {
if (distance(hitX, hitY, enemy.x, enemy.y) < hitRadius + enemy.radius) {
enemy.hp -= 25; spawnDust(state, enemy.x, enemy.y, 15);
const knockback = enemy.type === 'brachio' ? 0 : (enemy.isBoss ? 5 : 30);
enemy.x += p.dir * knockback; isHit = true;
if (enemy.hp <= 0) {
spawnMeat(state, enemy.x, enemy.y, enemy.isBoss);
state.lastScoreUpdate += (enemy.isBoss ? 2000 : (enemy.type === 'trike' ? 50 : 30));
if (enemy.isBoss) handleBossDefeat(state, p);
}
}
});
if (isHit) playSound('hit');
updateUI();
}
// --- メインループ ---
function gameLoop() {
try {
if (canvas.width !== window.innerWidth || canvas.height !== window.innerHeight) {
canvas.width = window.innerWidth; canvas.height = window.innerHeight;
}
if (engine.gameState === 'playing') {
const state = engine;
const p = state.player;
state.time++;
let dx = 0; let dy = 0;
const keys = state.keys; const touch = state.isMobileTouch;
if (p.isRoaring) {
p.roarTimer--; if (p.roarTimer <= 0) p.isRoaring = false;
} else {
if (keys['KeyW'] || keys['ArrowUp'] || touch.up) dy -= 1;
if (keys['KeyS'] || keys['ArrowDown'] || touch.down) dy += 1;
if (keys['KeyA'] || keys['ArrowLeft'] || touch.left) dx -= 1;
if (keys['KeyD'] || keys['ArrowRight'] || touch.right) dx += 1;
}
if (dx !== 0 && dy !== 0) { const len = Math.hypot(dx, dy); dx /= len; dy /= len; }
p.isMoving = (dx !== 0 || dy !== 0);
if (dx > 0) p.dir = 1; if (dx < 0) p.dir = -1;
if (p.isMoving && !p.isRoaring && state.time % 20 === 0) playSound('step');
let nextX = p.x + dx * p.speed; let nextY = p.y + dy * p.speed;
state.obstacles.forEach(obs => {
const dist = distance(nextX, nextY, obs.x, obs.y);
if (dist < p.radius + obs.radius) {
const overlap = (p.radius + obs.radius) - dist;
const angle = Math.atan2(nextY - obs.y, nextX - obs.x);
nextX += Math.cos(angle) * overlap; nextY += Math.sin(angle) * overlap;
}
});
p.x = nextX; p.y = nextY;
if (p.isAttacking) { p.attackTimer--; if (p.attackTimer <= 0) p.isAttacking = false; }
if (p.isTailAttacking) { p.tailAttackTimer--; if (p.tailAttackTimer <= 0) p.isTailAttacking = false; }
if (state.lastScoreUpdate >= state.nextBossScore && !state.bossSpawned) spawnBoss(state);
state.obstacles = state.obstacles.filter(o => distance(p.x, p.y, o.x, o.y) < DESPAWN_RADIUS);
state.enemies = state.enemies.filter(e => distance(p.x, p.y, e.x, e.y) < DESPAWN_RADIUS || e.isBoss);
state.items = state.items.filter(i => distance(p.x, p.y, i.x, i.y) < DESPAWN_RADIUS);
while (state.obstacles.length < 250) spawnObstacle(state, p.x, p.y, SPAWN_RADIUS, SPAWN_RADIUS - 200);
const currentMaxEnemies = MAX_ENEMIES + (state.level - 1) * 5;
if (!state.bossActive && state.enemies.length < currentMaxEnemies && Math.random() < 0.05) spawnEnemy(state, p.x, p.y, SPAWN_RADIUS, SPAWN_RADIUS - 200);
state.enemies = state.enemies.filter(e => e.hp > 0);
state.enemies.forEach(enemy => {
enemy.stateTimer--; let eDx = 0; let eDy = 0; const distToPlayer = distance(enemy.x, enemy.y, p.x, p.y);
if (enemy.type === 'raptor' || enemy.type === 'spino' || enemy.type === 'carno') {
const aggroRange = enemy.isBoss ? 1500 : 600;
if (distToPlayer < aggroRange) {
eDx = p.x - enemy.x; eDy = p.y - enemy.y;
if (distToPlayer < p.radius + enemy.radius + 5 && state.time % (enemy.isBoss ? 40 : 30) === 0) {
const dmgMulti = 1 + (state.level - 1) * 0.2;
p.hp -= (enemy.isBoss ? 15 : 10) * dmgMulti;
spawnDust(state, p.x, p.y, 8); playSound('damage'); updateUI();
}
} else {
if (enemy.stateTimer <= 0) {
enemy.targetX = enemy.x + randomRange(-150, 150); enemy.targetY = enemy.y + randomRange(-150, 150);
enemy.stateTimer = randomRange(40, 90);
}
if (enemy.targetX) { eDx = enemy.targetX - enemy.x; eDy = enemy.targetY - enemy.y; }
}
} else if (enemy.type === 'ankylosaurus' || enemy.type === 'brachio' || enemy.type === 'trike') {
if (enemy.stateTimer <= 0) {
enemy.targetX = enemy.x + randomRange(-100, 100); enemy.targetY = enemy.y + randomRange(-100, 100);
enemy.stateTimer = randomRange(80, 150);
}
if (enemy.targetX) { eDx = enemy.targetX - enemy.x; eDy = enemy.targetY - enemy.y; }
if (distToPlayer < p.radius + enemy.radius && state.time % 60 === 0) {
const dmgMulti = 1 + (state.level - 1) * 0.2;
p.hp -= (enemy.isBoss ? 25 : 15) * dmgMulti;
spawnDust(state, p.x, p.y, 10); playSound('damage'); updateUI();
}
}
if (enemy.targetX !== null) {
const dx = enemy.targetX - enemy.x; const dy = enemy.targetY - enemy.y; const len = Math.hypot(dx, dy);
if (len <= enemy.speed) {
enemy.x = enemy.targetX; enemy.y = enemy.targetY; enemy.targetX = null; enemy.targetY = null; enemy.isMoving = false;
} else {
enemy.x += (dx / len) * enemy.speed; enemy.y += (dy / len) * enemy.speed;
if (dx > 0) enemy.dir = 1; if (dx < 0) enemy.dir = -1; enemy.isMoving = true;
}
} else if (eDx !== 0 || eDy !== 0) {
const len = Math.hypot(eDx, eDy);
if (len > 0) {
enemy.x += (eDx / len) * enemy.speed; enemy.y += (eDy / len) * enemy.speed;
if (eDx > 0) enemy.dir = 1; if (eDx < 0) enemy.dir = -1; enemy.isMoving = true;
}
} else { enemy.isMoving = false; }
});
state.items.forEach((item, index) => {
item.floatOffset = Math.sin(state.time * 0.1) * 4;
if (distance(p.x, p.y, item.x, item.y) < p.radius + item.radius + 15) {
p.hp = Math.min(p.maxHp, p.hp + item.heal); state.items.splice(index, 1); playSound('eat'); updateUI();
}
});
state.particles.forEach(pt => { pt.x += pt.vx; pt.y += pt.vy; pt.life -= 0.04; });
state.particles = state.particles.filter(pt => pt.life > 0);
state.camera.x += (p.x - canvas.width / 2 - state.camera.x) * 0.1;
state.camera.y += (p.y - canvas.height / 2 - state.camera.y) * 0.1;
if (p.hp <= 0) { state.gameState = 'gameover'; updateUI(); }
if (state.time % 10 === 0) updateUI(); // 定期的にUI更新(HPバーなど)
}
if (engine.gameState !== 'start') drawFrame(engine, ctx, canvas);
} catch (e) { console.error("Game Loop Error:", e); }
animationFrameId = requestAnimationFrame(gameLoop);
}
function drawFrame(state, ctx, canvas) {
ctx.fillStyle = COLORS.grass; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
const camX = Math.floor(state.camera.x); const camY = Math.floor(state.camera.y);
ctx.translate(-camX, -camY);
const startX = Math.floor(camX / TILE_SIZE) * TILE_SIZE - TILE_SIZE;
const startY = Math.floor(camY / TILE_SIZE) * TILE_SIZE - TILE_SIZE;
const endX = camX + canvas.width + TILE_SIZE;
const endY = camY + canvas.height + TILE_SIZE;
ctx.fillStyle = COLORS.grassDark;
for (let x = startX; x < endX; x += TILE_SIZE) {
for (let y = startY; y < endY; y += TILE_SIZE) {
const hash = hashCoord(x, y);
if (hash % 1 > 0.6) {
ctx.beginPath(); ctx.ellipse(x + (hash % 50), y + ((hash*3) % 50), 12 + (hash % 10), 8 + (hash % 5), hash, 0, Math.PI*2); ctx.fill();
}
}
}
state.items.forEach(item => {
ctx.save(); ctx.translate(Math.floor(item.x), Math.floor(item.y + item.floatOffset));
const s = item.type === 'meat' && item.heal > 50 ? 1.5 : 1; ctx.scale(s, s);
ctx.fillStyle = COLORS.meat; ctx.beginPath(); ctx.ellipse(0, 0, 16, 10, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = COLORS.bone; ctx.fillRect(-24, -3, 48, 6);
ctx.beginPath(); ctx.arc(-24, -4, 5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(-24, 4, 5, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.arc(24, -4, 5, 0, Math.PI*2); ctx.fill(); ctx.beginPath(); ctx.arc(24, 4, 5, 0, Math.PI*2); ctx.fill();
ctx.restore();
});
const renderables = [ ...state.obstacles.map(o => ({...o, rType: 'obstacle'})), ...state.enemies.map(e => ({...e, rType: 'enemy'})), { ...state.player, rType: 'player' } ].sort((a, b) => a.y - b.y);
renderables.forEach(obj => {
const ex = Math.floor(obj.x); const ey = Math.floor(obj.y);
if (obj.rType === 'obstacle') {
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; ctx.beginPath(); ctx.ellipse(ex + 10, ey + 10, obj.radius, obj.radius * 0.7, 0, 0, Math.PI * 2); ctx.fill();
if (obj.type === 'rock') {
ctx.fillStyle = '#7a7a7a'; ctx.beginPath(); ctx.arc(ex, ey, obj.radius, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#5c5c5c'; ctx.beginPath(); ctx.arc(ex - obj.radius*0.2, ey + obj.radius*0.2, obj.radius * 0.7, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#949494'; ctx.beginPath(); ctx.arc(ex + obj.radius*0.3, ey - obj.radius*0.3, obj.radius * 0.4, 0, Math.PI * 2); ctx.fill();
} else {
ctx.fillStyle = '#4a2e16'; ctx.fillRect(ex - 8, ey - 10, 16, obj.radius + 10);
ctx.fillStyle = '#2d4d22'; ctx.beginPath(); ctx.arc(ex, ey - obj.radius * 0.3, obj.radius, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#3f6b30'; ctx.beginPath(); ctx.arc(ex - obj.radius*0.2, ey - obj.radius * 0.5, obj.radius*0.7, 0, Math.PI * 2); ctx.fill();
}
} else if (obj.rType === 'player' || obj.rType === 'enemy') {
drawDinosaur(ctx, obj, state.time);
}
});
state.particles.forEach(p => {
ctx.fillStyle = p.color; ctx.globalAlpha = Math.max(0, p.life);
ctx.beginPath(); ctx.arc(Math.floor(p.x), Math.floor(p.y), 4 + (1 - p.life)*5, 0, Math.PI * 2); ctx.fill();
ctx.globalAlpha = 1.0;
});
ctx.restore();
}
function drawDinosaur(ctx, entity, time) {
ctx.save();
const isHeavy = entity.type === 'trike' || entity.type === 'spino' || entity.type === 'ankylosaurus' || entity.type === 'brachio';
const walkSpeed = isHeavy ? 0.15 : 0.3; const walkAmp = isHeavy ? 3 : 5;
const breathOffset = entity.isMoving ? 0 : Math.floor(Math.sin(time * 0.05) * 1.5);
const walkBob = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * walkAmp) : 0;
const walkTilt = entity.isMoving ? Math.sin(time * (walkSpeed/2)) * 0.05 : 0;
const ex = Math.floor(entity.x); const ey = Math.floor(entity.y + walkBob + breathOffset);
ctx.fillStyle = 'rgba(0,0,0,0.2)'; ctx.beginPath(); ctx.ellipse(ex + 8, Math.floor(entity.y) + 12, entity.radius * 1.2, entity.radius * 0.6, 0, 0, Math.PI * 2); ctx.fill();
ctx.translate(ex, ey); ctx.scale(entity.dir, 1); ctx.rotate(walkTilt);
if (entity.rType === 'player') {
ctx.fillStyle = COLORS.player;
let tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 10 : Math.sin(time * 0.05) * 3;
if (entity.isTailAttacking) {
const t = 20 - entity.tailAttackTimer;
if (t < 5) tailSwing = -15; else if (t < 12) tailSwing = 40; else tailSwing = 40 - (t - 12) * 5;
if (t > 3 && t < 15) { ctx.save(); ctx.beginPath(); ctx.arc(-35, 0, 50, Math.PI * 0.7, Math.PI * 1.3, false); ctx.lineWidth = 12; ctx.strokeStyle = `rgba(200, 200, 200, ${1 - t/15})`; ctx.stroke(); ctx.restore(); }
}
ctx.beginPath(); ctx.moveTo(-10, -5); ctx.quadraticCurveTo(-45, -5 + tailSwing, -80, -2 + tailSwing * 1.5); ctx.lineWidth = 14; ctx.strokeStyle = COLORS.player; ctx.lineCap = 'round'; ctx.stroke();
ctx.beginPath(); ctx.moveTo(25, -12); ctx.lineTo(10, -18); ctx.lineTo(-20, -15); ctx.lineTo(-35, -8); ctx.lineTo(-25, 5); ctx.lineTo(0, 12); ctx.lineTo(20, 2); ctx.fill();
ctx.strokeStyle = COLORS.playerStripe; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(-15, -15); ctx.lineTo(-10, -2); ctx.stroke(); ctx.beginPath(); ctx.moveTo(-5, -17); ctx.lineTo(0, 0); ctx.stroke(); ctx.beginPath(); ctx.moveTo(5, -15); ctx.lineTo(10, 2); ctx.stroke();
const armBob = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 2) : 0;
ctx.fillStyle = '#3a2e22'; ctx.beginPath(); ctx.moveTo(10, 5 + armBob); ctx.lineTo(13, 12 + armBob); ctx.lineTo(17, 14 + armBob); ctx.lineTo(15, 15 + armBob); ctx.lineTo(11, 12 + armBob); ctx.lineTo(8, 6 + armBob); ctx.fill();
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 14) : 0;
ctx.fillStyle = '#2c2219'; ctx.beginPath(); ctx.moveTo(-10 - legOffset * 0.5, 5); ctx.lineTo(-2 - legOffset, 18); ctx.lineTo(-8 - legOffset, 28); ctx.lineTo(2 - legOffset, 30); ctx.lineTo(-12 - legOffset, 28); ctx.lineTo(-18 - legOffset * 0.5, 5); ctx.fill();
ctx.fillStyle = '#403224'; ctx.beginPath(); ctx.moveTo(6 + legOffset * 0.5, 5); ctx.lineTo(14 + legOffset, 20); ctx.lineTo(8 + legOffset, 30); ctx.lineTo(18 + legOffset, 32); ctx.lineTo(4 + legOffset, 30); ctx.lineTo(-2 + legOffset * 0.5, 5); ctx.fill();
ctx.fillStyle = COLORS.player; const jawDrop = entity.isRoaring ? 25 : (entity.isAttacking ? 15 : 0);
ctx.save(); if (entity.isRoaring) { ctx.translate(20, -5); ctx.rotate(-Math.PI / 4 + Math.sin(time * 2.0) * 0.05); ctx.translate(-20, 5); }
ctx.beginPath(); ctx.moveTo(20, -12); ctx.lineTo(40, -8); ctx.lineTo(38, 2); ctx.lineTo(20, 4); ctx.fill();
ctx.fillStyle = '#f5f5f4'; for(let i=0; i<5; i++) { ctx.beginPath(); ctx.moveTo(23 + i*4, 2); ctx.lineTo(25 + i*4, 10); ctx.lineTo(27 + i*4, 2); ctx.fill(); }
ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(28, -8, 4, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(29, -8, 2, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = '#251d16'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(24, -13); ctx.lineTo(32, -10); ctx.stroke();
ctx.save(); ctx.translate(20, 2); ctx.rotate(jawDrop * 0.05); ctx.fillStyle = COLORS.player; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(18, 0); ctx.lineTo(15, 5); ctx.lineTo(0, 4); ctx.fill();
ctx.fillStyle = '#f5f5f4'; if(entity.isAttacking || entity.isRoaring) { for(let i=0; i<4; i++) { ctx.beginPath(); ctx.moveTo(2 + i*4, 0); ctx.lineTo(4 + i*4, -8); ctx.lineTo(6 + i*4, 0); ctx.fill(); } }
ctx.restore(); ctx.restore();
} else if (entity.type === 'spino') {
ctx.fillStyle = COLORS.bossSpino; const tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 15 : Math.sin(time * 0.1) * 5;
ctx.beginPath(); ctx.moveTo(-20, -5); ctx.quadraticCurveTo(-60, -5 + tailSwing, -100, tailSwing * 1.8); ctx.lineWidth = 18; ctx.strokeStyle = COLORS.bossSpino; ctx.lineCap = 'round'; ctx.stroke();
ctx.beginPath(); ctx.moveTo(35, -10); ctx.lineTo(10, -15); ctx.lineTo(-25, -12); ctx.lineTo(-40, -5); ctx.lineTo(-20, 10); ctx.lineTo(15, 12); ctx.lineTo(30, 0); ctx.fill();
ctx.fillStyle = COLORS.bossSpinoSail; ctx.beginPath(); ctx.moveTo(-25, -12); ctx.quadraticCurveTo(-10, -45, 5, -42); ctx.quadraticCurveTo(15, -35, 20, -12); ctx.fill();
ctx.strokeStyle = '#4a1b1b'; ctx.lineWidth = 2; for(let i=-20; i<=15; i+=6) { const height = -12 - Math.max(0, Math.sin((i+25)/45 * Math.PI)) * 28; ctx.beginPath(); ctx.moveTo(i, -12); ctx.lineTo(i + 2, height); ctx.stroke(); }
const armOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed + Math.PI) * 5) : 0;
ctx.fillStyle = '#222729'; ctx.beginPath(); ctx.moveTo(15 + armOffset, 5); ctx.lineTo(20 + armOffset, 20); ctx.lineTo(30 + armOffset, 25); ctx.lineTo(35 + armOffset, 30); ctx.lineTo(30 + armOffset, 28); ctx.lineTo(37 + armOffset, 33); ctx.lineTo(28 + armOffset, 26); ctx.lineTo(18 + armOffset, 18); ctx.lineTo(10 + armOffset, 5); ctx.fill();
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 16) : 0;
ctx.fillStyle = '#1c2022'; ctx.beginPath(); ctx.moveTo(-10 - legOffset * 0.5, 5); ctx.lineTo(0 - legOffset, 20); ctx.lineTo(-6 - legOffset, 30); ctx.lineTo(6 - legOffset, 32); ctx.lineTo(-12 - legOffset, 30); ctx.lineTo(-20 - legOffset * 0.5, 5); ctx.fill();
ctx.fillStyle = '#272d30'; ctx.beginPath(); ctx.moveTo(5 + legOffset * 0.5, 5); ctx.lineTo(15 + legOffset, 22); ctx.lineTo(6 + legOffset, 32); ctx.lineTo(18 + legOffset, 35); ctx.lineTo(0 + legOffset, 32); ctx.lineTo(-8 + legOffset * 0.5, 5); ctx.fill();
ctx.fillStyle = COLORS.bossSpino; ctx.fillRect(30, -10, 32, 8); ctx.fillRect(30, -1, 30, 6);
ctx.beginPath(); ctx.arc(60, -6, 5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#f5f5f4'; for(let i=0; i<8; i++) { ctx.beginPath(); ctx.moveTo(32 + i*4, -2); ctx.lineTo(34 + i*4, 4); ctx.lineTo(36 + i*4, -2); ctx.fill(); }
ctx.fillStyle = '#fbbf24'; ctx.beginPath(); ctx.arc(40, -8, 3, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(41, -8, 1.5, 0, Math.PI*2); ctx.fill();
} else if (entity.type === 'ankylosaurus') {
ctx.fillStyle = COLORS.bossAnkylo; const tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 12 : Math.sin(time * 0.1) * 3;
ctx.beginPath(); ctx.moveTo(-20, 0); ctx.quadraticCurveTo(-40, tailSwing, -60, tailSwing * 1.5); ctx.lineWidth = 14; ctx.strokeStyle = COLORS.bossAnkylo; ctx.lineCap = 'round'; ctx.stroke();
ctx.fillStyle = COLORS.bossAnkyloArmor; ctx.beginPath(); ctx.arc(-60, tailSwing * 1.5, 12, 0, Math.PI*2); ctx.fill();
ctx.beginPath(); ctx.ellipse(0, 0, 35, 22, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = COLORS.bossAnkyloArmor;
for(let i = -20; i <= 20; i += 12) { ctx.beginPath(); ctx.moveTo(i, -18); ctx.lineTo(i-5, -28); ctx.lineTo(i+5, -28); ctx.fill(); ctx.beginPath(); ctx.moveTo(i, 18); ctx.lineTo(i-5, 28); ctx.lineTo(i+5, 28); ctx.fill(); }
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 10) : 0;
ctx.fillStyle = '#4a3824'; ctx.fillRect(-20 - legOffset * 0.5, 10, 14, 18); ctx.fillRect(10 + legOffset * 0.5, 10, 14, 18);
ctx.fillStyle = '#634e38'; ctx.fillRect(-10 + legOffset * 0.5, 12, 14, 18); ctx.fillRect(20 - legOffset * 0.5, 12, 14, 18);
ctx.fillStyle = COLORS.bossAnkylo; ctx.beginPath(); ctx.ellipse(35, 0, 15, 12, 0, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = COLORS.bossAnkyloArmor; ctx.beginPath(); ctx.moveTo(40, -10); ctx.lineTo(35, -20); ctx.lineTo(45, -10); ctx.fill(); ctx.beginPath(); ctx.moveTo(40, 10); ctx.lineTo(35, 20); ctx.lineTo(45, 10); ctx.fill();
ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(42, -5, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(42.5, -5, 1, 0, Math.PI*2); ctx.fill();
} else if (entity.type === 'carno') {
ctx.fillStyle = COLORS.bossCarno; const tailSwing = entity.isMoving ? Math.sin(time * 0.4) * 12 : Math.sin(time * 0.1) * 3;
ctx.beginPath(); ctx.moveTo(-10, -5); ctx.quadraticCurveTo(-40, -5 + tailSwing, -70, -2 + tailSwing * 1.5); ctx.lineWidth = 12; ctx.strokeStyle = COLORS.bossCarno; ctx.lineCap = 'round'; ctx.stroke();
ctx.beginPath(); ctx.moveTo(20, -10); ctx.lineTo(5, -15); ctx.lineTo(-15, -12); ctx.lineTo(-30, -8); ctx.lineTo(-20, 5); ctx.lineTo(0, 10); ctx.lineTo(15, 2); ctx.fill();
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * 0.4) * 16) : 0;
ctx.fillStyle = '#5c2217'; ctx.beginPath(); ctx.moveTo(-10 - legOffset * 0.5, 5); ctx.lineTo(-2 - legOffset, 18); ctx.lineTo(-8 - legOffset, 28); ctx.lineTo(2 - legOffset, 30); ctx.lineTo(-12 - legOffset, 28); ctx.lineTo(-18 - legOffset * 0.5, 5); ctx.fill();
ctx.fillStyle = '#7a2f20'; ctx.beginPath(); ctx.moveTo(6 + legOffset * 0.5, 5); ctx.lineTo(14 + legOffset, 20); ctx.lineTo(8 + legOffset, 30); ctx.lineTo(18 + legOffset, 32); ctx.lineTo(4 + legOffset, 30); ctx.lineTo(-2 + legOffset * 0.5, 5); ctx.fill();
ctx.fillStyle = COLORS.bossCarno; ctx.beginPath(); ctx.moveTo(15, -10); ctx.lineTo(32, -6); ctx.lineTo(30, 4); ctx.lineTo(15, 4); ctx.fill();
ctx.fillStyle = '#e8dcc5'; ctx.beginPath(); ctx.moveTo(22, -10); ctx.lineTo(28, -22); ctx.lineTo(28, -8); ctx.fill(); ctx.beginPath(); ctx.moveTo(18, -10); ctx.lineTo(24, -20); ctx.lineTo(24, -8); ctx.fill();
ctx.fillStyle = '#f5f5f4'; for(let i=0; i<4; i++) { ctx.beginPath(); ctx.moveTo(20 + i*3, 2); ctx.lineTo(21 + i*3, 8); ctx.lineTo(23 + i*3, 2); ctx.fill(); }
ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(26, -4, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(26.5, -4, 1.5, 0, Math.PI*2); ctx.fill();
} else if (entity.type === 'brachio') {
ctx.fillStyle = COLORS.bossBrachio; const tailSwing = entity.isMoving ? Math.sin(time * walkSpeed) * 8 : Math.sin(time * 0.05) * 2;
ctx.beginPath(); ctx.moveTo(-30, -10); ctx.quadraticCurveTo(-70, -10 + tailSwing, -110, tailSwing); ctx.lineWidth = 16; ctx.strokeStyle = COLORS.bossBrachio; ctx.lineCap = 'round'; ctx.stroke();
const neckBob = entity.isMoving ? Math.sin(time * walkSpeed) * 4 : Math.sin(time * 0.05) * 2;
ctx.beginPath(); ctx.moveTo(30, -15); ctx.quadraticCurveTo(60, -40 + neckBob, 75, -70 + neckBob); ctx.lineWidth = 18; ctx.strokeStyle = COLORS.bossBrachio; ctx.lineCap = 'round'; ctx.stroke();
ctx.beginPath(); ctx.ellipse(80, -75 + neckBob, 12, 8, Math.PI/6, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(84, -78 + neckBob, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(84, -78 + neckBob, 1.5, 0, Math.PI*2); ctx.fill();
ctx.fillStyle = COLORS.bossBrachio; ctx.beginPath(); ctx.ellipse(0, 0, 45, 35, 0, 0, Math.PI * 2); ctx.fill();
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 10) : 0;
ctx.fillStyle = '#3a453c'; ctx.fillRect(-25 - legOffset * 0.5, 15, 18, 30); ctx.fillRect(15 + legOffset * 0.5, 15, 18, 40);
ctx.fillStyle = '#4b574d'; ctx.fillRect(-15 + legOffset * 0.5, 20, 18, 30); ctx.fillRect(25 - legOffset * 0.5, 20, 18, 40);
} else if (entity.type === 'raptor') {
ctx.fillStyle = COLORS.raptor; const tailSwing = entity.isMoving ? Math.sin(time * 0.5) * 12 : 0;
ctx.beginPath(); ctx.moveTo(-5, -2); ctx.lineTo(-45, -2 + tailSwing); ctx.lineWidth = 6; ctx.strokeStyle = COLORS.raptor; ctx.stroke();
ctx.beginPath(); ctx.moveTo(12, -8); ctx.lineTo(-10, -6); ctx.lineTo(-5, 4); ctx.lineTo(10, 2); ctx.fill();
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * 0.6) * 12) : 0;
ctx.fillStyle = '#1e211b'; ctx.beginPath(); ctx.moveTo(-4 - legOffset * 0.5, 2); ctx.lineTo(2 - legOffset, 12); ctx.lineTo(-2 - legOffset, 18); ctx.lineTo(4 - legOffset, 20); ctx.lineTo(-6 - legOffset, 18); ctx.lineTo(-8 - legOffset * 0.5, 2); ctx.fill();
ctx.fillStyle = '#292d25'; ctx.beginPath(); ctx.moveTo(8 + legOffset * 0.5, 2); ctx.lineTo(14 + legOffset, 12); ctx.lineTo(10 + legOffset, 18); ctx.lineTo(16 + legOffset, 20); ctx.lineTo(12 + legOffset, 14); ctx.lineTo(20 + legOffset, 16); ctx.lineTo(10 + legOffset, 18); ctx.lineTo(6 + legOffset, 18); ctx.lineTo(4 + legOffset * 0.5, 2); ctx.fill();
ctx.fillRect(12, -10, 14, 7);
ctx.fillStyle = '#f5f5f4'; for(let i=0; i<3; i++) { ctx.beginPath(); ctx.moveTo(15 + i*4, -3); ctx.lineTo(17 + i*4, 2); ctx.lineTo(19 + i*4, -3); ctx.fill(); }
ctx.fillStyle = '#ef4444'; ctx.beginPath(); ctx.arc(20, -7, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(21, -7, 1, 0, Math.PI*2); ctx.fill();
} else if (entity.type === 'trike') {
ctx.fillStyle = COLORS.trike; ctx.beginPath(); ctx.moveTo(25, -12); ctx.lineTo(0, -20); ctx.lineTo(-30, -5); ctx.lineTo(-35, 8); ctx.lineTo(20, 12); ctx.fill();
ctx.beginPath(); ctx.moveTo(-30, -5); ctx.lineTo(-45, 2); ctx.lineTo(-35, 8); ctx.fill();
const legOffset = entity.isMoving ? Math.floor(Math.sin(time * walkSpeed) * 8) : 0;
ctx.fillStyle = '#2b2b2b'; ctx.beginPath(); ctx.moveTo(-20 + legOffset, 8); ctx.lineTo(-14 + legOffset, 22); ctx.lineTo(-24 + legOffset, 22); ctx.lineTo(-26 + legOffset, 8); ctx.fill(); ctx.beginPath(); ctx.moveTo(15 + legOffset, 8); ctx.lineTo(21 + legOffset, 22); ctx.lineTo(11 + legOffset, 22); ctx.lineTo(9 + legOffset, 8); ctx.fill();
ctx.fillStyle = '#3a3a3a'; ctx.beginPath(); ctx.moveTo(-6 - legOffset, 10); ctx.lineTo(-2 - legOffset, 26); ctx.lineTo(-14 - legOffset, 26); ctx.lineTo(-14 - legOffset, 10); ctx.fill(); ctx.beginPath(); ctx.moveTo(25 - legOffset, 10); ctx.lineTo(29 - legOffset, 26); ctx.lineTo(17 - legOffset, 26); ctx.lineTo(17 - legOffset, 10); ctx.fill();
ctx.fillStyle = COLORS.trike; ctx.beginPath(); ctx.moveTo(20, -8); ctx.quadraticCurveTo(15, -35, 35, -30); ctx.quadraticCurveTo(40, -15, 30, 5); ctx.fill();
ctx.fillStyle = '#8f8c83'; ctx.beginPath(); ctx.moveTo(42, -4); ctx.lineTo(48, 4); ctx.lineTo(38, 6); ctx.fill();
ctx.fillStyle = COLORS.trike; ctx.beginPath(); ctx.moveTo(25, -12); ctx.lineTo(45, -5); ctx.lineTo(30, 8); ctx.fill();
ctx.fillStyle = '#b5b2a8'; ctx.beginPath(); ctx.moveTo(35, -15); ctx.lineTo(60, -20); ctx.lineTo(38, -8); ctx.fill(); ctx.beginPath(); ctx.moveTo(32, -10); ctx.lineTo(50, -12); ctx.lineTo(35, -4); ctx.fill(); ctx.beginPath(); ctx.moveTo(42, 0); ctx.lineTo(52, -2); ctx.lineTo(42, 4); ctx.fill();
ctx.fillStyle = '#eab308'; ctx.beginPath(); ctx.arc(36, -6, 2.5, 0, Math.PI*2); ctx.fill(); ctx.fillStyle = 'black'; ctx.beginPath(); ctx.arc(36, -6, 1.5, 0, Math.PI*2); ctx.fill();
}
if (entity.rType === 'enemy' && entity.hp < entity.maxHp) {
ctx.fillStyle = 'rgba(0,0,0,0.5)'; ctx.fillRect(-15, -35, 30, 4);
ctx.fillStyle = entity.isBoss ? '#b33939' : '#8f8f8f'; ctx.fillRect(-15, -35, 30 * (entity.hp / entity.maxHp), 4);
}
ctx.restore();
}
// --- イベントリスナー設定 ---
window.addEventListener('keydown', (e) => {
engine.keys[e.code] = true;
if (engine.gameState !== 'playing') return;
const p = engine.player;
if (e.code === 'Space' && !p.isAttacking && !p.isTailAttacking && !p.isRoaring) attack(engine);
if ((e.code === 'ShiftLeft' || e.code === 'ShiftRight' || e.code === 'KeyC') && !p.isAttacking && !p.isTailAttacking && !p.isRoaring) tailAttack(engine);
});
window.addEventListener('keyup', (e) => { engine.keys[e.code] = false; });
// UIボタン設定
document.getElementById('btn-start').addEventListener('click', initGame);
document.getElementById('btn-retry').addEventListener('click', initGame);
// スマホタッチ設定
const bindTouch = (id, key, actionFunc) => {
const el = document.getElementById(id);
if(!el) return;
el.addEventListener('touchstart', (e) => {
e.preventDefault();
if(key) engine.isMobileTouch[key] = true;
if(actionFunc && engine.gameState === 'playing') {
const p = engine.player;
if (!p.isAttacking && !p.isTailAttacking && !p.isRoaring) actionFunc(engine);
}
});
el.addEventListener('touchend', (e) => { e.preventDefault(); if(key) engine.isMobileTouch[key] = false; });
// PC用マウスクリック対応
el.addEventListener('mousedown', (e) => {
if(key) engine.isMobileTouch[key] = true;
if(actionFunc && engine.gameState === 'playing') {
const p = engine.player;
if (!p.isAttacking && !p.isTailAttacking && !p.isRoaring) actionFunc(engine);
}
});
el.addEventListener('mouseup', (e) => { if(key) engine.isMobileTouch[key] = false; });
el.addEventListener('mouseleave', (e) => { if(key) engine.isMobileTouch[key] = false; });
};
bindTouch('btn-up', 'up'); bindTouch('btn-down', 'down'); bindTouch('btn-left', 'left'); bindTouch('btn-right', 'right');
bindTouch('btn-bite', null, attack); bindTouch('btn-tail', null, tailAttack);
// 起動
animationFrameId = requestAnimationFrame(gameLoop);
</script>
</body>
</html>
恐竜世界を舞台にしたオリジナル2Dゲーム