Ninja Chronicles
by SparkWave49300 lines48.4 KB
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Ninja Chronicles</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', -apple-system, sans-serif; user-select: none; touch-action: none; position: fixed; }
#game-container { position: relative; width: 100vw; height: 100vh; overflow: hidden; background: #1a1a2e; }
canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; image-rendering: auto; }
#hud { position: absolute; top: 12px; left: 12px; right: 60px; display: flex; flex-direction: column; gap: 6px; z-index: 10; pointer-events: none; }
.hud-row { display: flex; align-items: center; gap: 10px; }
.bar-container { display: flex; align-items: center; gap: 6px; }
.bar-label { color: #fff; font-size: 10px; font-weight: bold; text-shadow: 2px 2px 0 #000; width: 52px; letter-spacing: 1px; }
.bar-bg { width: 140px; height: 12px; background: rgba(0,0,0,0.65); border: 2px solid #333; border-radius: 6px; overflow: hidden; box-shadow: 0 0 10px rgba(0,0,0,0.8); }
.bar-fill { height: 100%; transition: width 0.15s; }
.health { background: linear-gradient(90deg, #e74c3c, #ff6b5b); width: 100%; }
.chakra { background: linear-gradient(90deg, #3498db, #5dade2); width: 100%; }
#score { color: #ffd700; font-weight: bold; font-size: 12px; text-shadow: 2px 2px 0 #000; margin-top: 4px; }
#levelInfo { color: #ff8c42; font-weight: bold; font-size: 12px; background: rgba(0,0,0,0.6); padding: 3px 10px; border-radius: 10px; text-shadow: 1px 1px 0 #000; }
#levelBanner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #ffd700; font-size: 28px; font-weight: bold; text-shadow: 0 0 20px #ff8c42, 3px 3px 0 #000; z-index: 15; pointer-events: none; animation: bannerAnim 2s ease-out; white-space: nowrap; }
@keyframes bannerAnim { 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.5); } 20% { opacity: 1; transform: translate(-50%, -50%) scale(1.1); } 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); } 100% { opacity: 0; transform: translate(-50%, -50%) scale(1); } }
#btnPause, #btnAudio { position: absolute; top: 12px; width: 42px; height: 42px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.4); background: rgba(0,0,0,0.5); color: #fff; font-size: 18px; z-index: 20; cursor: pointer; }
#btnPause { right: 64px; }
#btnAudio { right: 12px; }
#btnPause:active, #btnAudio:active { transform: scale(0.9); }
#controls { position: absolute; bottom: 0; left: 0; right: 0; height: 200px; z-index: 20; pointer-events: none; }
#joystick { position: absolute; bottom: 30px; left: 30px; width: 130px; height: 130px; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.15), rgba(255,255,255,0.05)); border: 3px solid rgba(255,255,255,0.3); pointer-events: auto; touch-action: none; }
#joystickKnob { position: absolute; top: 50%; left: 50%; width: 60px; height: 60px; border-radius: 50%; background: radial-gradient(circle, #ff8c42, #e67e22); border: 3px solid #fff; transform: translate(-50%, -50%); box-shadow: 0 0 15px rgba(255,140,66,0.6); pointer-events: none; }
#actions { position: absolute; bottom: 30px; right: 20px; display: flex; gap: 12px; align-items: flex-end; pointer-events: auto; }
.action-btn { width: 70px; height: 70px; border-radius: 50%; border: 3px solid #fff; font-size: 28px; background: radial-gradient(circle, #e74c3c, #c0392b); color: #fff; box-shadow: 0 0 15px rgba(231,76,60,0.6); cursor: pointer; touch-action: manipulation; }
.action-btn:active { transform: scale(0.9); }
#btnJutsu { background: radial-gradient(circle, #f39c12, #d68910); }
#btnJump { background: radial-gradient(circle, #27ae60, #1e8449); width: 60px; height: 60px; font-size: 24px; }
#dialogueBox { position: absolute; bottom: 240px; left: 5%; right: 5%; background: linear-gradient(135deg, rgba(20,20,40,0.96), rgba(40,40,70,0.96)); border: 3px solid #ff8c42; border-radius: 12px; padding: 14px; z-index: 30; box-shadow: 0 0 30px rgba(255,140,66,0.4); display: flex; gap: 12px; }
#dialoguePortrait { width: 60px; height: 60px; border-radius: 8px; background: radial-gradient(circle, #f5cba7, #c9a67c); border: 2px solid #ff8c42; flex-shrink: 0; position: relative; overflow: hidden; }
#dialoguePortrait::before { content: ''; position: absolute; top: 5px; left: 50%; transform: translateX(-50%); width: 50px; height: 25px; background: #f1c40f; border-radius: 50% 50% 0 0; }
#dialoguePortrait::after { content: ''; position: absolute; top: 30px; left: 50%; transform: translateX(-50%); width: 3px; height: 15px; background: #2c3e50; box-shadow: -15px 0 0 #2c3e50, 15px 0 0 #2c3e50; }
#dialogueContent { flex: 1; }
#dialogueName { color: #ff8c42; font-weight: bold; font-size: 15px; margin-bottom: 6px; text-shadow: 1px 1px 2px #000; }
#dialogueText { color: #fff; font-size: 13px; line-height: 1.4; min-height: 36px; }
#dialogueContinue { color: #ffd700; font-size: 10px; text-align: right; margin-top: 6px; animation: blink 1s infinite; }
@keyframes blink { 50% { opacity: 0.3; } }
.screen { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 100; color: #fff; text-align: center; padding: 20px; gap: 12px; }
.logo h1 { font-size: 38px; color: #ff8c42; text-shadow: 0 0 20px rgba(255,140,66,0.8), 3px 3px 0 #000; margin-bottom: 8px; letter-spacing: 3px; }
.logo h2 { font-size: 15px; color: #ffd700; margin-bottom: 30px; }
.screen h1 { font-size: 32px; color: #ff8c42; text-shadow: 3px 3px 0 #000; }
.screen p { font-size: 14px; opacity: 0.85; }
.big-btn { padding: 14px 36px; font-size: 16px; font-weight: bold; background: linear-gradient(135deg, #ff8c42, #e67e22); color: #fff; border: 3px solid #fff; border-radius: 30px; cursor: pointer; box-shadow: 0 0 20px rgba(255,140,66,0.6); letter-spacing: 2px; min-width: 220px; }
.big-btn.secondary { background: linear-gradient(135deg, #2c3e50, #1a252f); box-shadow: 0 0 15px rgba(44,62,80,0.5); }
.big-btn:active { transform: scale(0.95); }
#levelGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 10px; max-width: 400px; width: 100%; margin: 20px 0; }
.level-cell { aspect-ratio: 1; border-radius: 12px; background: linear-gradient(135deg, #ff8c42, #e67e22); border: 3px solid #fff; color: #fff; font-size: 22px; font-weight: bold; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 0 15px rgba(255,140,66,0.5); }
.level-cell.locked { background: linear-gradient(135deg, #333, #111); opacity: 0.5; cursor: not-allowed; }
.level-cell:active:not(.locked) { transform: scale(0.92); }
.hidden { display: none !important; }
@media (max-width: 400px) { .logo h1 { font-size: 30px; } .bar-bg { width: 110px; } }
</style>
</head>
<body>
<div id="game-container">
<canvas id="gameCanvas"></canvas>
<div id="hud">
<div class="hud-row"><div class="bar-container"><div class="bar-label">VIDA</div><div class="bar-bg"><div id="healthBar" class="bar-fill health"></div></div></div><div id="levelInfo">Nv.1</div></div>
<div class="bar-container"><div class="bar-label">CHAKRA</div><div class="bar-bg"><div id="chakraBar" class="bar-fill chakra"></div></div></div>
<div id="score">XP: 0 | Nivel: 1</div>
<div id="levelBanner" class="hidden"></div>
</div>
<div id="dialogueBox" class="hidden">
<div id="dialoguePortrait"></div>
<div id="dialogueContent"><div id="dialogueName"></div><div id="dialogueText"></div><div id="dialogueContinue">▶ Toca para continuar</div></div>
</div>
<div id="controls">
<div id="joystick"><div id="joystickKnob"></div></div>
<div id="actions"><button class="action-btn" id="btnAttack">👊</button><button class="action-btn" id="btnJutsu">🔥</button><button class="action-btn" id="btnJump">⬆</button></div>
</div>
<button id="btnPause">⏸</button><button id="btnAudio">🔊</button>
<div id="startScreen" class="screen">
<div class="logo"><h1>NINJA CHRONICLES</h1><h2>La Leyenda del Pájaro de Fuego</h2></div>
<button id="startBtn" class="big-btn">NUEVA AVENTURA</button>
<button id="continueBtn" class="big-btn secondary hidden">CONTINUAR</button>
<button id="selectLevelBtn" class="big-btn secondary hidden">SELECCIONAR NIVEL</button>
<p style="font-size:12px;margin-top:24px;opacity:0.6">Toca para jugar · Usa el joystick</p>
</div>
<div id="levelSelect" class="screen hidden"><h1>SELECCIONA NIVEL</h1><div id="levelGrid"></div><button id="backBtn" class="big-btn secondary">VOLVER</button></div>
<div id="pauseScreen" class="screen hidden"><h1>PAUSA</h1><button id="resumeBtn" class="big-btn">CONTINUAR</button><button id="saveBtn" class="big-btn secondary">GUARDAR</button><button id="quitBtn" class="big-btn secondary">SALIR AL MENÚ</button></div>
<div id="gameOverScreen" class="screen hidden"><h1 style="color:#e74c3c">DERROTADO</h1><p>Has caído en batalla...</p><button id="restartBtn" class="big-btn">REINTENTAR</button><button id="menuBtn" class="big-btn secondary">MENÚ</button></div>
<div id="victoryScreen" class="screen hidden"><h1 style="color:#ffd700">¡VICTORIA!</h1><p id="victoryText">Has completado el nivel</p><button id="nextLevelBtn" class="big-btn">SIGUIENTE NIVEL</button><button id="victoryMenuBtn" class="big-btn secondary">MENÚ</button></div>
</div>
<script>
// ==================== SPRITES PROCEDURALES ====================
const SPR = {
cache: {}, pixelRatio: 1,
init(dpr) { this.pixelRatio = dpr; },
makeCanvas(w, h) { const c = document.createElement('canvas'); c.width = w * this.pixelRatio; c.height = h * this.pixelRatio; const cx = c.getContext('2d'); cx.scale(this.pixelRatio, this.pixelRatio); return { canvas: c, ctx: cx }; },
createCharacter(opts) {
const { skin, hair, outfit, accent, isBoss = false, scarf = false } = opts;
const w = isBoss ? 100 : 70, h = isBoss ? 120 : 90;
const key = `char_${skin}_${hair}_${outfit}_${isBoss}_${scarf}`;
if (this.cache[key]) return this.cache[key];
const { canvas, ctx } = this.makeCanvas(w, h);
ctx.translate(w / 2, h);
ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.roundRect(-14, -32, 12, 30, 4); ctx.fill(); ctx.beginPath(); ctx.roundRect(2, -32, 12, 30, 4); ctx.fill();
ctx.fillStyle = '#2c1810'; ctx.beginPath(); ctx.roundRect(-15, -6, 14, 8, 3); ctx.fill(); ctx.beginPath(); ctx.roundRect(1, -6, 14, 8, 3); ctx.fill();
const bodyGrad = ctx.createLinearGradient(0, -60, 0, -20); bodyGrad.addColorStop(0, outfit); bodyGrad.addColorStop(1, this.darken(outfit, 30)); ctx.fillStyle = bodyGrad; ctx.beginPath(); ctx.roundRect(-18, -62, 36, 42, 6); ctx.fill();
ctx.fillStyle = this.darken(outfit, 40); ctx.fillRect(-18, -30, 36, 4); ctx.fillRect(-18, -46, 36, 3);
ctx.fillStyle = this.darken(outfit, 20); ctx.beginPath(); ctx.roundRect(-14, -64, 28, 10, 4); ctx.fill();
if (scarf) { ctx.fillStyle = '#8e1a1a'; ctx.fillRect(-19, -68, 38, 6); ctx.beginPath(); ctx.moveTo(19, -66); ctx.lineTo(28, -58); ctx.lineTo(26, -68); ctx.closePath(); ctx.fill(); }
else { ctx.fillStyle = '#1a1a2e'; ctx.fillRect(-17, -68, 34, 6); const plateGrad = ctx.createLinearGradient(-8, -66, 8, -62); plateGrad.addColorStop(0, '#c0c0c0'); plateGrad.addColorStop(0.5, '#f0f0f0'); plateGrad.addColorStop(1, '#a0a0a0'); ctx.fillStyle = plateGrad; ctx.fillRect(-8, -67, 16, 4); ctx.strokeStyle = '#333'; ctx.lineWidth = 0.5; ctx.strokeRect(-8, -67, 16, 4); }
const armGrad = ctx.createLinearGradient(0, -60, 0, -30); armGrad.addColorStop(0, outfit); armGrad.addColorStop(1, this.darken(outfit, 30)); ctx.fillStyle = armGrad; ctx.beginPath(); ctx.roundRect(-24, -58, 8, 28, 4); ctx.fill(); ctx.beginPath(); ctx.roundRect(16, -58, 8, 28, 4); ctx.fill();
ctx.fillStyle = skin; ctx.beginPath(); ctx.arc(-20, -30, 5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(20, -30, 5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = skin; ctx.beginPath(); ctx.arc(0, -80, 16, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = 'rgba(0,0,0,0.08)'; ctx.beginPath(); ctx.arc(0, -76, 15, 0, Math.PI); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.ellipse(-6, -82, 4, 5, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(6, -82, 4, 5, 0, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = isBoss ? '#ff0000' : '#2c3e50'; ctx.beginPath(); ctx.arc(-6, -82, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(6, -82, 2.5, 0, Math.PI * 2); ctx.fill();
ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(-5, -83, 1, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(7, -83, 1, 0, Math.PI * 2); ctx.fill();
ctx.strokeStyle = this.darken(hair, 20); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(-10, -90); ctx.lineTo(-3, -89); ctx.stroke(); ctx.beginPath(); ctx.moveTo(3, -89); ctx.lineTo(10, -90); ctx.stroke();
ctx.strokeStyle = '#8b4513'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(0, -74, 4, 0.2, Math.PI - 0.2); ctx.stroke();
ctx.fillStyle = hair; ctx.beginPath(); ctx.arc(0, -86, 17, Math.PI, 0); ctx.fill();
for (let i = -4; i <= 4; i++) { const px = i * 4; const py = -86 - Math.abs(i) * 1; ctx.beginPath(); ctx.moveTo(px - 3, py + 5); ctx.lineTo(px + i * 1.5, py - 12 - Math.random() * 3); ctx.lineTo(px + 3, py + 5); ctx.closePath(); ctx.fill(); }
ctx.beginPath(); ctx.moveTo(-12, -80); ctx.lineTo(-9, -73); ctx.lineTo(-6, -80); ctx.closePath(); ctx.fill(); ctx.beginPath(); ctx.moveTo(12, -80); ctx.lineTo(9, -73); ctx.lineTo(6, -80); ctx.closePath(); ctx.fill();
if (scarf === false && !isBoss) { ctx.strokeStyle = '#a0522d'; ctx.lineWidth = 1.5; for (let i = 0; i < 3; i++) { ctx.beginPath(); ctx.moveTo(3 + i, -78); ctx.lineTo(5 + i, -76); ctx.stroke(); } }
if (isBoss) { ctx.shadowColor = accent; ctx.shadowBlur = 10; ctx.strokeStyle = accent; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(0, -55, 32, 0, Math.PI * 2); ctx.stroke(); ctx.shadowBlur = 0; }
this.cache[key] = { canvas, w, h, cx: w/2, cy: h };
return this.cache[key];
},
darken(hex, amt) { const c = parseInt(hex.slice(1), 16); const r = Math.max(0, (c >> 16) - amt); const g = Math.max(0, ((c >> 8) & 0xff) - amt); const b = Math.max(0, (c & 0xff) - amt); return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, '0')}`; },
preload() { this.createCharacter({ skin: '#f5cba7', hair: '#f1c40f', outfit: '#ff8c00', accent: '#ff8c00', scarf: false }); this.createCharacter({ skin: '#d9b48f', hair: '#1a1a2e', outfit: '#2c3e50', accent: '#e74c3c', scarf: false }); this.createCharacter({ skin: '#d9b48f', hair: '#8e1a1a', outfit: '#8e1a1a', accent: '#ff0000', scarf: true, isBoss: true }); }
};
// ==================== AUDIO ====================
class AudioManager {
constructor() { this.ctx = null; this.enabled = true; this.musicNodes = []; this.initialized = false; }
init() { if (this.initialized) return; try { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); this.initialized = true; } catch (e) {} }
resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); }
toggle() { this.enabled = !this.enabled; if (!this.enabled) this.stopMusic(); else this.startMusic(); return this.enabled; }
playTone(freq, duration, type = 'sine', volume = 0.15) { if (!this.enabled || !this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = type; osc.frequency.value = freq; gain.gain.setValueAtTime(volume, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + duration); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + duration); }
jump() { if (!this.enabled || !this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(300, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(600, this.ctx.currentTime + 0.15); gain.gain.setValueAtTime(0.15, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.15); }
hit() { if (!this.enabled || !this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'square'; osc.frequency.setValueAtTime(200, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.1); gain.gain.setValueAtTime(0.25, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.1); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.1); this.playTone(80, 0.08, 'sawtooth', 0.1); }
jutsu() { if (!this.enabled || !this.ctx) return; const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'sawtooth'; osc.frequency.setValueAtTime(600, this.ctx.currentTime); osc.frequency.exponentialRampToValueAtTime(100, this.ctx.currentTime + 0.3); gain.gain.setValueAtTime(0.2, this.ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(); osc.stop(this.ctx.currentTime + 0.3); setTimeout(() => { this.playTone(150, 0.15, 'sawtooth', 0.15); this.playTone(80, 0.2, 'sine', 0.12); }, 100); }
levelUp() { if (!this.enabled || !this.ctx) return; [523, 659, 784, 1047].forEach((f, i) => setTimeout(() => this.playTone(f, 0.2, 'triangle', 0.15), i * 100)); }
victory() { if (!this.enabled || !this.ctx) return; [523, 659, 784, 1047, 1319].forEach((f, i) => setTimeout(() => this.playTone(f, 0.3, 'triangle', 0.18), i * 120)); }
gameOver() { if (!this.enabled || !this.ctx) return; [392, 349, 294, 220].forEach((f, i) => setTimeout(() => this.playTone(f, 0.3, 'sawtooth', 0.15), i * 150)); }
dialogue() { if (!this.enabled || !this.ctx) return; this.playTone(800, 0.05, 'sine', 0.08); }
startMusic() { if (!this.enabled || !this.ctx || this.musicPlaying) return; this.musicPlaying = true; this.scheduleMusicLoop(); }
stopMusic() { this.musicPlaying = false; if (this.musicLoopTimeout) clearTimeout(this.musicLoopTimeout); this.musicNodes.forEach(n => { try { n.stop(); } catch(e) {} }); this.musicNodes = []; }
scheduleMusicLoop() {
if (!this.musicPlaying) return;
const melody = [ { n: 523, d: 0.4 }, { n: 587, d: 0.2 }, { n: 659, d: 0.4 }, { n: 784, d: 0.4 }, { n: 659, d: 0.2 }, { n: 587, d: 0.4 }, { n: 523, d: 0.4 }, { n: 440, d: 0.4 }, { n: 392, d: 0.6 }, { n: 440, d: 0.4 }, { n: 523, d: 0.4 }, { n: 587, d: 0.6 }, { n: 659, d: 0.4 }, { n: 523, d: 0.4 }, { n: 440, d: 0.8 } ];
let time = this.ctx.currentTime; const bpm = 90; const beat = 60 / bpm;
melody.forEach(note => { const osc = this.ctx.createOscillator(); const gain = this.ctx.createGain(); osc.type = 'triangle'; osc.frequency.value = note.n; gain.gain.setValueAtTime(0, time); gain.gain.linearRampToValueAtTime(0.04, time + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, time + note.d * beat); osc.connect(gain); gain.connect(this.ctx.destination); osc.start(time); osc.stop(time + note.d * beat + 0.1); this.musicNodes.push(osc); time += note.d * beat; });
const totalDur = melody.reduce((a, b) => a + b.d * beat, 0);
this.musicLoopTimeout = setTimeout(() => this.scheduleMusicLoop(), totalDur * 1000);
}
}
// ==================== SAVE ====================
const SaveManager = {
key: 'ninja_chronicles_save',
save(data) { try { localStorage.setItem(this.key, JSON.stringify(data)); return true; } catch(e) { return false; } },
load() { try { const d = localStorage.getItem(this.key); return d ? JSON.parse(d) : null; } catch(e) { return null; } },
clear() { try { localStorage.removeItem(this.key); } catch(e) {} }
};
// ==================== LEVELS ====================
const LEVELS = [
{ id: 1, name: 'Bosque de la Hoja', worldWidth: 4000, bgColor1: '#0a0e27', bgColor2: '#1a1a4e', enemyCount: 6, hasBoss: false },
{ id: 2, name: 'Valle del Río', worldWidth: 4500, bgColor1: '#1a0e27', bgColor2: '#4a1e3e', enemyCount: 8, hasBoss: false },
{ id: 3, name: 'Cueva del Dragón', worldWidth: 5000, bgColor1: '#0a0a0a', bgColor2: '#2a1a1a', enemyCount: 10, hasBoss: true }
];
// ==================== GAME ====================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let W, H, DPR;
function resize() { DPR = window.devicePixelRatio || 1; W = window.innerWidth; H = window.innerHeight; canvas.width = W * DPR; canvas.height = H * DPR; canvas.style.width = W + 'px'; canvas.style.height = H + 'px'; ctx.setTransform(DPR, 0, 0, DPR, 0, 0); ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = 'high'; }
window.addEventListener('resize', resize); resize();
SPR.init(DPR); SPR.preload();
const audio = new AudioManager();
const game = { running: false, camera: { x: 0 }, worldWidth: 4000, groundY: 0, score: 0, level: 1, enemies: [], particles: [], projectiles: [], platforms: [], decorations: [], dialogueActive: false, dialogueQueue: [], bossDefeated: false };
game.groundY = () => H - 140;
const player = { x: 200, y: 0, w: 44, h: 64, vx: 0, vy: 0, speed: 5.5, jumpForce: -15, onGround: false, facing: 1, health: 100, maxHealth: 100, chakra: 100, maxChakra: 100, attackTimer: 0, attackCooldown: 0, jutsuCooldown: 0, invulnerable: 0, animTime: 0, state: 'idle', level: 1, xp: 0 };
const input = { left: false, right: false, jump: false, attack: false, jutsu: false };
// Joystick
const joystick = document.getElementById('joystick'); const knob = document.getElementById('joystickKnob');
let joystickActive = false; let joystickCenter = { x: 0, y: 0 }; let joystickVector = { x: 0, y: 0 };
function getJoystickCenter() { const r = joystick.getBoundingClientRect(); return { x: r.left + r.width / 2, y: r.top + r.height / 2 }; }
function handleJoystickStart(e) { e.preventDefault(); joystickActive = true; joystickCenter = getJoystickCenter(); handleJoystickMove(e); }
function handleJoystickMove(e) { if (!joystickActive) return; e.preventDefault(); const touch = e.touches ? e.touches[0] : e; const dx = touch.clientX - joystickCenter.x; const dy = touch.clientY - joystickCenter.y; const dist = Math.min(Math.hypot(dx, dy), 50); const angle = Math.atan2(dy, dx); const kx = Math.cos(angle) * dist; const ky = Math.sin(angle) * dist; knob.style.transform = `translate(calc(-50% + ${kx}px), calc(-50% + ${ky}px))`; const norm = dist / 50; joystickVector.x = Math.cos(angle) * norm; joystickVector.y = Math.sin(angle) * norm; input.left = joystickVector.x < -0.2; input.right = joystickVector.x > 0.2; }
function handleJoystickEnd(e) { e.preventDefault(); joystickActive = false; knob.style.transform = 'translate(-50%, -50%)'; joystickVector = { x: 0, y: 0 }; input.left = input.right = false; }
joystick.addEventListener('touchstart', handleJoystickStart, { passive: false }); joystick.addEventListener('touchmove', handleJoystickMove, { passive: false }); joystick.addEventListener('touchend', handleJoystickEnd, { passive: false });
joystick.addEventListener('mousedown', handleJoystickStart); window.addEventListener('mousemove', handleJoystickMove); window.addEventListener('mouseup', handleJoystickEnd);
// Buttons
document.getElementById('btnJump').addEventListener('touchstart', (e) => { e.preventDefault(); tryJump(); }, { passive: false }); document.getElementById('btnJump').addEventListener('mousedown', tryJump);
document.getElementById('btnAttack').addEventListener('touchstart', (e) => { e.preventDefault(); tryAttack(); }, { passive: false }); document.getElementById('btnAttack').addEventListener('mousedown', tryAttack);
document.getElementById('btnJutsu').addEventListener('touchstart', (e) => { e.preventDefault(); tryJutsu(); }, { passive: false }); document.getElementById('btnJutsu').addEventListener('mousedown', tryJutsu);
document.addEventListener('keydown', (e) => { if (e.key === 'ArrowLeft' || e.key === 'a') input.left = true; if (e.key === 'ArrowRight' || e.key === 'd') input.right = true; if (e.key === 'ArrowUp' || e.key === 'w' || e.key === ' ') tryJump(); if (e.key === 'j') tryAttack(); if (e.key === 'k') tryJutsu(); });
document.addEventListener('keyup', (e) => { if (e.key === 'ArrowLeft' || e.key === 'a') input.left = false; if (e.key === 'ArrowRight' || e.key === 'd') input.right = false; });
function tryJump() { if (player.onGround && !game.dialogueActive) { player.vy = player.jumpForce; player.onGround = false; audio.jump(); spawnParticles(player.x + player.w/2, player.y + player.h, '#3498db', 8); } }
function tryAttack() { if (game.dialogueActive) { advanceDialogue(); return; } if (player.attackCooldown <= 0) { player.attackCooldown = 25; player.attackTimer = 12; audio.hit(); const range = 70; const ax = player.x + (player.facing > 0 ? player.w : -range); const ay = player.y + 10; const ah = player.h - 20; game.enemies.forEach(enemy => { if (enemy.dead) return; if (rectCollide({x: ax, y: ay, w: range, h: ah}, enemy)) { enemy.health -= 25; enemy.hitFlash = 10; enemy.vx += player.facing * 8; spawnParticles(enemy.x + enemy.w/2, enemy.y + enemy.h/2, '#ff4757', 10); if (enemy.health <= 0) killEnemy(enemy); } }); } }
function tryJutsu() { if (game.dialogueActive) { advanceDialogue(); return; } if (player.jutsuCooldown <= 0 && player.chakra >= 30) { player.jutsuCooldown = 45; player.chakra -= 30; player.attackTimer = 15; audio.jutsu(); game.projectiles.push({ x: player.x + player.w/2 + player.facing * 30, y: player.y + player.h/2, vx: player.facing * 12, vy: 0, w: 40, h: 40, life: 90, damage: 50, type: 'fireball', animTime: 0 }); spawnParticles(player.x + player.w/2, player.y + player.h/2, '#ff8c42', 15); } }
// Utils
function rectCollide(a, b) { return a.x < b.x + b.w && a.x + a.w > b.x && a.y < b.y + b.h && a.y + a.h > b.y; }
function spawnParticles(x, y, color, count) { for (let i = 0; i < count; i++) { game.particles.push({ x, y, vx: (Math.random() - 0.5) * 8, vy: (Math.random() - 0.5) * 8 - 2, life: 30 + Math.random() * 20, maxLife: 50, color, size: 3 + Math.random() * 5 }); } }
function killEnemy(enemy) { enemy.dead = true; spawnParticles(enemy.x + enemy.w/2, enemy.y + enemy.h/2, '#ffd700', 20); game.score += enemy.xp; player.xp += enemy.xp; player.chakra = Math.min(player.maxChakra, player.chakra + 20); if (player.xp >= player.level * 100) { player.level++; player.maxHealth += 20; player.health = player.maxHealth; player.maxChakra += 20; audio.levelUp(); showDialogue('Sistema', `¡Subiste de nivel! Ahora eres nivel ${player.level}`); } }
// Dialogue
const dialogueBox = document.getElementById('dialogueBox'); const dialogueName = document.getElementById('dialogueName'); const dialogueText = document.getElementById('dialogueText');
function showDialogue(name, text) { game.dialogueActive = true; dialogueName.textContent = name; dialogueText.textContent = text; dialogueBox.classList.remove('hidden'); input.left = input.right = false; audio.dialogue(); }
function advanceDialogue() { game.dialogueActive = false; dialogueBox.classList.add('hidden'); if (game.dialogueQueue.length > 0) { const next = game.dialogueQueue.shift(); setTimeout(() => showDialogue(next.name, next.text), 100); } }
dialogueBox.addEventListener('click', advanceDialogue); dialogueBox.addEventListener('touchstart', (e) => { e.preventDefault(); advanceDialogue(); }, { passive: false });
// Level Setup
function setupLevel(levelIndex = 0) {
const levelData = LEVELS[levelIndex] || LEVELS[0];
game.worldWidth = levelData.worldWidth;
game.enemies = []; game.platforms = []; game.decorations = []; game.projectiles = []; game.particles = []; game.camera.x = 0; game.bossDefeated = false;
game.groundY = H - 140;
const platformData = [ { x: 600, y: H - 280, w: 180 }, { x: 900, y: H - 380, w: 150 }, { x: 1300, y: H - 300, w: 200 }, { x: 1800, y: H - 420, w: 160 }, { x: 2200, y: H - 320, w: 180 }, { x: 2600, y: H - 400, w: 200 }, { x: 3100, y: H - 300, w: 180 }, { x: 3600, y: H - 350, w: 180 } ];
platformData.forEach(p => game.platforms.push(p));
for (let i = 0; i < levelData.enemyCount; i++) { game.enemies.push(createEnemy(700 + i * 400, 'ninja')); }
if (levelData.hasBoss) { game.enemies.push(createEnemy(game.worldWidth - 400, 'boss')); }
for (let i = 0; i < 30; i++) { game.decorations.push({ x: 200 + i * 130 + Math.random() * 60, type: Math.random() > 0.5 ? 'tree' : 'rock', size: 0.7 + Math.random() * 0.6 }); }
player.x = 200; player.y = game.groundY - player.h; player.vx = player.vy = 0; player.health = player.maxHealth; player.chakra = player.maxChakra;
setTimeout(() => { showDialogue('Naruto', `Nivel ${levelData.id}: ${levelData.name}. ¡Vamos a por ellos!`); game.dialogueQueue.push({ name: 'Kakashi', text: 'Naruto, confío en ti. ¡Adelante!' }); }, 300);
}
function createEnemy(x, type) { if (type === 'boss') { return { x, y: game.groundY - 90, w: 70, h: 90, vx: 0, vy: 0, health: 300, maxHealth: 300, facing: -1, type: 'boss', xp: 150, attackCooldown: 0, hitFlash: 0, animTime: 0, speed: 2, onGround: true, dead: false }; } return { x, y: game.groundY - 60, w: 40, h: 60, vx: 0, vy: 0, health: 50, maxHealth: 50, facing: -1, type: 'ninja', xp: 25, attackCooldown: 0, hitFlash: 0, animTime: Math.random() * 100, speed: 1.5, onGround: true, dead: false }; }
// Update
function update() {
if (!game.running) return; if (game.dialogueActive) return;
const accel = player.onGround ? 1.5 : 0.8; const maxSpeed = player.speed;
if (input.left) { player.vx = Math.max(player.vx - accel, -maxSpeed); player.facing = -1; } else if (input.right) { player.vx = Math.min(player.vx + accel, maxSpeed); player.facing = 1; } else { player.vx *= 0.85; if (Math.abs(player.vx) < 0.1) player.vx = 0; }
player.vy += 0.8; player.x += player.vx; player.y += player.vy;
player.onGround = false;
game.platforms.forEach(p => { if (player.vy >= 0 && player.x + player.w > p.x && player.x < p.x + p.w && player.y + player.h >= p.y && player.y + player.h <= p.y + 20) { player.y = p.y - player.h; player.vy = 0; player.onGround = true; } });
if (player.y + player.h >= game.groundY) { player.y = game.groundY - player.h; player.vy = 0; player.onGround = true; }
player.x = Math.max(0, Math.min(player.x, game.worldWidth - player.w));
player.animTime++;
if (player.attackTimer > 0) player.state = 'attack'; else if (!player.onGround) player.state = 'jump'; else if (Math.abs(player.vx) > 0.5) player.state = 'run'; else player.state = 'idle';
if (player.attackTimer > 0) player.attackTimer--; if (player.attackCooldown > 0) player.attackCooldown--; if (player.jutsuCooldown > 0) player.jutsuCooldown--; if (player.invulnerable > 0) player.invulnerable--;
if (player.chakra < player.maxChakra) player.chakra = Math.min(player.maxChakra, player.chakra + 0.15);
const targetCamX = player.x - W / 2; game.camera.x += (targetCamX - game.camera.x) * 0.1; game.camera.x = Math.max(0, Math.min(game.camera.x, game.worldWidth - W));
game.enemies.forEach(enemy => { if (enemy.dead) return; enemy.animTime++; if (enemy.hitFlash > 0) enemy.hitFlash--; const dx = (player.x + player.w/2) - (enemy.x + enemy.w/2); const dist = Math.abs(dx); if (dist < 400) { enemy.facing = dx > 0 ? 1 : -1; if (dist > 60) { enemy.vx = enemy.facing * enemy.speed; } else { enemy.vx *= 0.8; if (enemy.attackCooldown <= 0) { enemy.attackCooldown = 60; if (Math.abs(dx) < 80 && Math.abs(player.y - enemy.y) < 80) { if (player.invulnerable <= 0) { player.health -= enemy.type === 'boss' ? 25 : 12; player.invulnerable = 60; player.vx = -enemy.facing * 10; player.vy = -6; spawnParticles(player.x + player.w/2, player.y + player.h/2, '#e74c3c', 12); audio.hit(); if (player.health <= 0) gameOver(); } } } } } else { enemy.vx *= 0.9; } if (enemy.attackCooldown > 0) enemy.attackCooldown--; enemy.x += enemy.vx; enemy.vy += 0.8; enemy.y += enemy.vy; if (enemy.y + enemy.h >= game.groundY) { enemy.y = game.groundY - enemy.h; enemy.vy = 0; } enemy.x = Math.max(0, Math.min(enemy.x, game.worldWidth - enemy.w)); });
game.projectiles.forEach(p => { p.x += p.vx; p.y += p.vy; p.life--; p.animTime++; game.enemies.forEach(enemy => { if (enemy.dead) return; if (rectCollide(p, enemy)) { enemy.health -= p.damage; enemy.hitFlash = 10; enemy.vx += p.vx * 0.3; spawnParticles(p.x, p.y, '#ff8c42', 15); p.life = 0; audio.hit(); if (enemy.health <= 0) killEnemy(enemy); } }); });
game.projectiles = game.projectiles.filter(p => p.life > 0);
game.particles.forEach(p => { p.x += p.vx; p.y += p.vy; p.vy += 0.3; p.vx *= 0.97; p.life--; }); game.particles = game.particles.filter(p => p.life > 0);
const boss = game.enemies.find(e => e.type === 'boss'); if (boss && boss.dead && !game.bossDefeated) { game.bossDefeated = true; setTimeout(() => { showDialogue('Naruto', '¡He derrotado al enemigo final!'); game.dialogueQueue.push({ name: 'Hokage', text: 'Bien hecho, ninja. El Pájaro de Fuego te lo agradece.' }); setTimeout(() => showVictory(), 3000); }, 500); }
document.getElementById('healthBar').style.width = (player.health / player.maxHealth * 100) + '%'; document.getElementById('chakraBar').style.width = (player.chakra / player.maxChakra * 100) + '%'; document.getElementById('score').textContent = `XP: ${player.xp} | Nivel: ${player.level}`; document.getElementById('levelInfo').textContent = `Nv.${player.level}`;
}
function gameOver() { game.running = false; audio.gameOver(); document.getElementById('gameOverScreen').classList.remove('hidden'); }
function showVictory() { game.running = false; audio.victory(); document.getElementById('victoryText').textContent = `Nivel ${game.level} completado`; document.getElementById('victoryScreen').classList.remove('hidden'); }
// Render
function drawBackground() { const skyGrad = ctx.createLinearGradient(0, 0, 0, H); skyGrad.addColorStop(0, '#0a0e27'); skyGrad.addColorStop(0.4, '#1a1a4e'); skyGrad.addColorStop(0.7, '#4a2c5a'); skyGrad.addColorStop(1, '#8b4513'); ctx.fillStyle = skyGrad; ctx.fillRect(0, 0, W, H); ctx.save(); const moonX = W * 0.8 - game.camera.x * 0.05; const moonY = 120; const moonGrad = ctx.createRadialGradient(moonX, moonY, 20, moonX, moonY, 100); moonGrad.addColorStop(0, 'rgba(255,250,220,1)'); moonGrad.addColorStop(0.5, 'rgba(255,240,200,0.6)'); moonGrad.addColorStop(1, 'rgba(255,240,200,0)'); ctx.fillStyle = moonGrad; ctx.beginPath(); ctx.arc(moonX, moonY, 100, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff8dc'; ctx.beginPath(); ctx.arc(moonX, moonY, 45, 0, Math.PI * 2); ctx.fill(); ctx.restore(); ctx.save(); ctx.translate(-game.camera.x * 0.2, 0); ctx.fillStyle = '#1a1a3e'; for (let i = 0; i < 15; i++) { const mx = i * 400; ctx.beginPath(); ctx.moveTo(mx, H - 200); ctx.lineTo(mx + 200, H - 500); ctx.lineTo(mx + 400, H - 200); ctx.closePath(); ctx.fill(); } ctx.restore(); ctx.save(); ctx.translate(-game.camera.x * 0.4, 0); ctx.fillStyle = '#0f1530'; for (let i = 0; i < 20; i++) { const mx = i * 300; ctx.beginPath(); ctx.moveTo(mx, H - 140); ctx.lineTo(mx + 150, H - 380); ctx.lineTo(mx + 300, H - 140); ctx.closePath(); ctx.fill(); } ctx.restore(); ctx.fillStyle = 'rgba(255,255,255,0.7)'; for (let i = 0; i < 80; i++) { const sx = (i * 137.5) % W; const sy = (i * 73.3) % (H * 0.5); const twinkle = Math.sin(Date.now() * 0.002 + i) * 0.5 + 0.5; ctx.globalAlpha = twinkle * 0.7; ctx.beginPath(); ctx.arc(sx, sy, 1.5, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; }
function drawGround() { const gy = game.groundY; const groundGrad = ctx.createLinearGradient(0, gy, 0, H); groundGrad.addColorStop(0, '#3d2817'); groundGrad.addColorStop(0.3, '#2a1a0f'); groundGrad.addColorStop(1, '#1a0f08'); ctx.fillStyle = groundGrad; ctx.fillRect(0, gy, W, H - gy); ctx.strokeStyle = '#5d3a1f'; ctx.lineWidth = 3; ctx.beginPath(); ctx.moveTo(0, gy); ctx.lineTo(W, gy); ctx.stroke(); ctx.save(); ctx.translate(-game.camera.x, 0); for (let i = 0; i < 60; i++) { const gx = i * 70 + (game.camera.x % 70); ctx.fillStyle = i % 2 === 0 ? '#4a7c2f' : '#3a6425'; ctx.beginPath(); ctx.ellipse(gx, gy - 5, 15, 8, 0, 0, Math.PI * 2); ctx.fill(); } ctx.restore(); }
function drawDecorations() { ctx.save(); ctx.translate(-game.camera.x, 0); game.decorations.forEach(d => { if (d.x < game.camera.x - 200 || d.x > game.camera.x + W + 200) return; const baseY = game.groundY; if (d.type === 'tree') { ctx.fillStyle = '#3d2817'; ctx.fillRect(d.x - 8 * d.size, baseY - 120 * d.size, 16 * d.size, 120 * d.size); const treeGrad = ctx.createRadialGradient(d.x, baseY - 150 * d.size, 10, d.x, baseY - 150 * d.size, 70 * d.size); treeGrad.addColorStop(0, '#2d5a1e'); treeGrad.addColorStop(1, '#1a3a12'); ctx.fillStyle = treeGrad; ctx.beginPath(); ctx.arc(d.x, baseY - 150 * d.size, 60 * d.size, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(d.x - 40 * d.size, baseY - 130 * d.size, 40 * d.size, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(d.x + 40 * d.size, baseY - 130 * d.size, 40 * d.size, 0, Math.PI * 2); ctx.fill(); } else { const rockGrad = ctx.createRadialGradient(d.x, baseY - 20 * d.size, 5, d.x, baseY - 20 * d.size, 40 * d.size); rockGrad.addColorStop(0, '#5a5a6a'); rockGrad.addColorStop(1, '#2a2a3a'); ctx.fillStyle = rockGrad; ctx.beginPath(); ctx.ellipse(d.x, baseY - 15 * d.size, 35 * d.size, 22 * d.size, 0, 0, Math.PI * 2); ctx.fill(); } }); ctx.restore(); }
function drawPlatforms() { ctx.save(); ctx.translate(-game.camera.x, 0); game.platforms.forEach(p => { ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.beginPath(); ctx.ellipse(p.x + p.w/2, game.groundY - 2, p.w/2, 10, 0, 0, Math.PI * 2); ctx.fill(); const platGrad = ctx.createLinearGradient(0, p.y, 0, p.y + 30); platGrad.addColorStop(0, '#4a7c2f'); platGrad.addColorStop(0.3, '#3d2817'); platGrad.addColorStop(1, '#1a0f08'); ctx.fillStyle = platGrad; ctx.fillRect(p.x, p.y, p.w, 30); ctx.fillStyle = '#5d9a3f'; ctx.fillRect(p.x, p.y, p.w, 4); ctx.fillStyle = 'rgba(0,0,0,0.3)'; for (let i = 0; i < p.w; i += 20) { ctx.fillRect(p.x + i, p.y + 10, 2, 18); } }); ctx.restore(); }
function drawPlayer() { ctx.save(); const px = player.x - game.camera.x; const py = player.y; ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.beginPath(); ctx.ellipse(px + player.w/2, game.groundY - 2, player.w/2, 8, 0, 0, Math.PI * 2); ctx.fill(); if (player.invulnerable > 0 && Math.floor(player.invulnerable / 4) % 2 === 0) { ctx.globalAlpha = 0.5; } ctx.translate(px + player.w/2, py); ctx.scale(player.facing, 1); const bob = player.state === 'idle' ? Math.sin(player.animTime * 0.1) * 2 : 0; const runBob = player.state === 'run' ? Math.abs(Math.sin(player.animTime * 0.3)) * 3 : 0; const legSwing = player.state === 'run' ? Math.sin(player.animTime * 0.3) * 12 : 0; ctx.fillStyle = '#e67e22'; ctx.save(); ctx.translate(-8, 30); ctx.rotate(legSwing * 0.05); ctx.fillRect(-5, 0, 10, 30); ctx.restore(); ctx.save(); ctx.translate(8, 30); ctx.rotate(-legSwing * 0.05); ctx.fillRect(-5, 0, 10, 30); ctx.restore(); ctx.fillStyle = '#ff8c00'; ctx.beginPath(); ctx.roundRect(-16, -10 + bob, 32, 42, 6); ctx.fill(); ctx.fillStyle = '#1a1a1a'; ctx.fillRect(-16, 18 + bob, 32, 6); ctx.strokeStyle = '#1a1a1a'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, -8 + bob); ctx.lineTo(0, 28 + bob); ctx.stroke(); ctx.fillStyle = '#f5cba7'; ctx.beginPath(); ctx.arc(0, -24 + bob, 16, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#f1c40f'; ctx.beginPath(); ctx.arc(0, -30 + bob, 17, Math.PI, 0); ctx.fill(); for (let i = -3; i <= 3; i++) { ctx.beginPath(); ctx.moveTo(i * 5, -30 + bob); ctx.lineTo(i * 5 + 3, -46 + bob); ctx.lineTo(i * 5 + 6, -30 + bob); ctx.closePath(); ctx.fill(); } ctx.fillStyle = '#2c3e50'; ctx.beginPath(); ctx.arc(5, -22 + bob, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(-5, -22 + bob, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = '#a0522d'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(7, -26 + bob); ctx.lineTo(7, -18 + bob); ctx.stroke(); ctx.fillStyle = '#2c3e50'; ctx.fillRect(-17, -32 + bob, 34, 6); ctx.fillStyle = '#3498db'; ctx.beginPath(); ctx.arc(0, -29 + bob, 3, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#f5cba7'; const armSwing = player.state === 'run' ? Math.sin(player.animTime * 0.3) * 15 : 0; if (player.state === 'attack') { ctx.save(); ctx.translate(14, 5 + bob); ctx.fillStyle = '#f5cba7'; ctx.beginPath(); ctx.roundRect(-4, -3, 35, 8, 4); ctx.fill(); ctx.beginPath(); ctx.arc(32, 1, 7, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } else { ctx.save(); ctx.translate(-18, 5 + bob); ctx.rotate(-armSwing * 0.04); ctx.fillRect(-4, 0, 8, 25); ctx.restore(); ctx.save(); ctx.translate(18, 5 + bob); ctx.rotate(armSwing * 0.04); ctx.fillRect(-4, 0, 8, 25); ctx.restore(); } ctx.restore(); if (player.attackTimer > 0) { ctx.save(); const progress = 1 - player.attackTimer / 12; ctx.strokeStyle = `rgba(255,255,255,${1 - progress})`; ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(px + player.w/2 + player.facing * 40, py + player.h/2, 20 + progress * 30, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); } }
function drawEnemies() { ctx.save(); ctx.translate(-game.camera.x, 0); game.enemies.forEach(enemy => { if (enemy.dead) return; if (enemy.x < game.camera.x - 100 || enemy.x > game.camera.x + W + 100) return; ctx.fillStyle = 'rgba(0,0,0,0.4)'; ctx.beginPath(); ctx.ellipse(enemy.x + enemy.w/2, game.groundY - 2, enemy.w/2, 8, 0, 0, Math.PI * 2); ctx.fill(); ctx.save(); ctx.translate(enemy.x + enemy.w/2, enemy.y); ctx.scale(enemy.facing, 1); if (enemy.hitFlash > 0) { ctx.globalAlpha = 0.5 + Math.sin(enemy.hitFlash * 0.5) * 0.5; } const bob = Math.sin(enemy.animTime * 0.08) * 2; const isBoss = enemy.type === 'boss'; const scale = isBoss ? 1.5 : 1; ctx.scale(scale, scale); const bodyColor = isBoss ? '#8e1a1a' : '#2c3e50'; ctx.fillStyle = bodyColor; ctx.beginPath(); ctx.roundRect(-enemy.w/2/scale, -10 + bob, enemy.w/scale, 42, 5); ctx.fill(); ctx.fillStyle = '#1a1a1a'; const legSwing = Math.sin(enemy.animTime * 0.2) * 10; ctx.save(); ctx.translate(-6, 30); ctx.rotate(legSwing * 0.05); ctx.fillRect(-4, 0, 8, 25); ctx.restore(); ctx.save(); ctx.translate(6, 30); ctx.rotate(-legSwing * 0.05); ctx.fillRect(-4, 0, 8, 25); ctx.restore(); ctx.fillStyle = '#f5cba7'; ctx.beginPath(); ctx.arc(0, -24 + bob, 14, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#1a1a2e'; ctx.beginPath(); ctx.arc(0, -24 + bob, 14, Math.PI, 0); ctx.fill(); ctx.fillRect(-14, -26 + bob, 28, 10); ctx.fillStyle = isBoss ? '#ff0000' : '#e74c3c'; ctx.beginPath(); ctx.arc(5, -23 + bob, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(-5, -23 + bob, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = isBoss ? '#8e1a1a' : '#2c3e50'; ctx.fillRect(-15, -32 + bob, 30, 5); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(0, -30 + bob, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#f5cba7'; const armSwing = Math.sin(enemy.animTime * 0.2) * 12; ctx.save(); ctx.translate(-enemy.w/2/scale, 5 + bob); ctx.rotate(-armSwing * 0.04); ctx.fillRect(-3, 0, 6, 22); ctx.restore(); ctx.save(); ctx.translate(enemy.w/2/scale, 5 + bob); ctx.rotate(armSwing * 0.04); ctx.fillRect(-3, 0, 6, 22); ctx.restore(); ctx.restore(); if (enemy.health < enemy.maxHealth) { const barW = enemy.w; const barH = 5; const barX = enemy.x; const barY = enemy.y - 15; ctx.fillStyle = 'rgba(0,0,0,0.7)'; ctx.fillRect(barX, barY, barW, barH); const healthPct = enemy.health / enemy.maxHealth; ctx.fillStyle = healthPct > 0.5 ? '#e74c3c' : healthPct > 0.25 ? '#f39c12' : '#c0392b'; ctx.fillRect(barX, barY, barW * healthPct, barH); ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.strokeRect(barX, barY, barW, barH); } if (isBoss) { ctx.fillStyle = '#ffd700'; ctx.font = 'bold 12px sans-serif'; ctx.textAlign = 'center'; ctx.fillText('JEFE', enemy.x + enemy.w/2, enemy.y - 22); } }); ctx.restore(); }
function drawProjectiles() { ctx.save(); ctx.translate(-game.camera.x, 0); game.projectiles.forEach(p => { if (p.type === 'fireball') { const grad = ctx.createRadialGradient(p.x, p.y, 5, p.x, p.y, 25); grad.addColorStop(0, 'rgba(255,255,150,1)'); grad.addColorStop(0.3, 'rgba(255,150,50,0.9)'); grad.addColorStop(0.7, 'rgba(255,80,20,0.6)'); grad.addColorStop(1, 'rgba(200,30,0,0)'); ctx.fillStyle = grad; ctx.beginPath(); ctx.arc(p.x, p.y, 25 + Math.sin(p.animTime * 0.5) * 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#fff'; ctx.beginPath(); ctx.arc(p.x, p.y, 8, 0, Math.PI * 2); ctx.fill(); } }); ctx.restore(); }
function drawParticles() { ctx.save(); ctx.translate(-game.camera.x, 0); game.particles.forEach(p => { const alpha = p.life / p.maxLife; ctx.globalAlpha = alpha; ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.size * alpha, 0, Math.PI * 2); ctx.fill(); }); ctx.globalAlpha = 1; ctx.restore(); }
if (!CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; this.beginPath(); this.moveTo(x + r, y); this.arcTo(x + w, y, x + w, y + h, r); this.arcTo(x + w, y + h, x, y + h, r); this.arcTo(x, y + h, x, y, r); this.arcTo(x, y, x + w, y, r); this.closePath(); return this; }; }
function render() { ctx.clearRect(0, 0, W, H); drawBackground(); drawDecorations(); drawPlatforms(); drawEnemies(); drawProjectiles(); drawPlayer(); drawParticles(); const vignette = ctx.createRadialGradient(W/2, H/2, H*0.3, W/2, H/2, H*0.8); vignette.addColorStop(0, 'rgba(0,0,0,0)'); vignette.addColorStop(1, 'rgba(0,0,0,0.5)'); ctx.fillStyle = vignette; ctx.fillRect(0, 0, W, H); }
let lastTime = 0;
function gameLoop(timestamp) { requestAnimationFrame(gameLoop); update(); render(); }
// Start
document.getElementById('startBtn').addEventListener('click', () => { audio.init(); audio.resume(); audio.startMusic(); document.getElementById('startScreen').classList.add('hidden'); game.running = true; game.level = 1; setupLevel(0); });
document.getElementById('continueBtn').addEventListener('click', () => { const s = SaveManager.load(); if (s) { audio.init(); audio.resume(); document.getElementById('startScreen').classList.add('hidden'); game.running = true; game.level = s.level || 1; player.level = s.playerLevel || 1; player.xp = s.xp || 0; setupLevel((s.levelIndex || 0)); } });
document.getElementById('selectLevelBtn').addEventListener('click', () => { document.getElementById('startScreen').classList.add('hidden'); document.getElementById('levelSelect').classList.remove('hidden'); const grid = document.getElementById('levelGrid'); grid.innerHTML = ''; LEVELS.forEach((lv, i) => { const cell = document.createElement('div'); cell.className = 'level-cell'; cell.textContent = lv.id; cell.onclick = () => { audio.init(); audio.resume(); document.getElementById('levelSelect').classList.add('hidden'); game.running = true; game.level = lv.id; setupLevel(i); }; grid.appendChild(cell); }); });
document.getElementById('backBtn').addEventListener('click', () => { document.getElementById('levelSelect').classList.add('hidden'); document.getElementById('startScreen').classList.remove('hidden'); });
document.getElementById('restartBtn').addEventListener('click', () => { document.getElementById('gameOverScreen').classList.add('hidden'); player.level = 1; player.xp = 0; player.maxHealth = 100; player.maxChakra = 100; game.score = 0; game.running = true; audio.init(); audio.resume(); setupLevel(0); });
document.getElementById('menuBtn').addEventListener('click', () => { document.getElementById('gameOverScreen').classList.add('hidden'); document.getElementById('startScreen').classList.remove('hidden'); });
document.getElementById('nextLevelBtn').addEventListener('click', () => { document.getElementById('victoryScreen').classList.add('hidden'); game.level++; if (game.level > LEVELS.length) { alert('¡Has completado el juego!'); document.getElementById('startScreen').classList.remove('hidden'); } else { game.running = true; setupLevel(game.level - 1); } });
document.getElementById('victoryMenuBtn').addEventListener('click', () => { document.getElementById('victoryScreen').classList.add('hidden'); document.getElementById('startScreen').classList.remove('hidden'); });
document.getElementById('btnPause').addEventListener('click', () => { if (game.running) { game.running = false; document.getElementById('pauseScreen').classList.remove('hidden'); } });
document.getElementById('resumeBtn').addEventListener('click', () => { document.getElementById('pauseScreen').classList.add('hidden'); game.running = true; });
document.getElementById('saveBtn').addEventListener('click', () => { const ok = SaveManager.save({ level: game.level, levelIndex: game.level - 1, playerLevel: player.level, xp: player.xp }); if (ok) { alert('Partida guardada'); } else { alert('No se pudo guardar'); } });
document.getElementById('quitBtn').addEventListener('click', () => { document.getElementById('pauseScreen').classList.add('hidden'); document.getElementById('startScreen').classList.remove('hidden'); });
document.getElementById('btnAudio').addEventListener('click', () => { const on = audio.toggle(); document.getElementById('btnAudio').textContent = on ? '🔊' : '🔇'; });
const saved = SaveManager.load();
if (saved) { document.getElementById('continueBtn').classList.remove('hidden'); }
document.getElementById('selectLevelBtn').classList.remove('hidden');
requestAnimationFrame(gameLoop);
</script>
</body>
</html>Game Source: Ninja Chronicles
Creator: SparkWave49
Libraries: none
Complexity: complex (300 lines, 48.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: ninja-chronicles-sparkwave49" to link back to the original. Then publish at arcadelab.ai/publish.