Spaces:
Running
Running
| <html lang="fr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Naruto Shippuden - Duel Ninja</title> | |
| <!-- Importation de FontAwesome pour les icônes --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Police Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Bangers&family=Roboto:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --naruto-orange: #ff5e00; | |
| --naruto-yellow: #ffcc00; | |
| --sasuke-blue: #2e3192; | |
| --sasuke-purple: #662d91; | |
| --bg-dark: #1a1a1a; | |
| --ui-panel: rgba(0, 0, 0, 0.8); | |
| --text-main: #ffffff; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| user-select: none; | |
| } | |
| body { | |
| font-family: 'Roboto', sans-serif; | |
| background-color: #050505; | |
| color: var(--text-main); | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| background-image: radial-gradient(circle at center, #2a2a2a 0%, #000000 100%); | |
| } | |
| /* --- Header --- */ | |
| header { | |
| height: 60px; | |
| background: rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0 20px; | |
| border-bottom: 1px solid rgba(255, 255, 255, 0.1); | |
| z-index: 10; | |
| backdrop-filter: blur(5px); | |
| } | |
| .logo { | |
| font-family: 'Bangers', cursive; | |
| font-size: 1.8rem; | |
| letter-spacing: 2px; | |
| color: var(--naruto-orange); | |
| text-shadow: 2px 2px 0px #fff; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .logo i { | |
| color: var(--sasuke-blue); | |
| } | |
| .built-with { | |
| font-size: 0.85rem; | |
| color: #888; | |
| text-decoration: none; | |
| transition: color 0.3s; | |
| } | |
| .built-with:hover { | |
| color: white; | |
| } | |
| /* --- Main Game Area --- */ | |
| main { | |
| flex: 1; | |
| position: relative; | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| } | |
| #game-wrapper { | |
| position: relative; | |
| width: 100%; | |
| height: 100%; | |
| max-width: 1200px; | |
| max-height: 800px; | |
| background: #222; | |
| box-shadow: 0 0 50px rgba(0, 0, 0, 0.5); | |
| overflow: hidden; | |
| border: 2px solid #333; | |
| } | |
| canvas { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* --- UI Overlay (HUD) --- */ | |
| #ui-layer { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: space-between; | |
| } | |
| /* Health Bars */ | |
| .hud-top { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: flex-start; | |
| width: 100%; | |
| } | |
| .player-status { | |
| width: 45%; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 5px; | |
| } | |
| .p1-status { align-items: flex-start; } | |
| .p2-status { align-items: flex-end; } | |
| .name-tag { | |
| font-family: 'Bangers', cursive; | |
| font-size: 1.5rem; | |
| text-transform: uppercase; | |
| margin-bottom: 5px; | |
| } | |
| .p1-name { color: var(--naruto-yellow); } | |
| .p2-name { color: #aaddff; } | |
| .bar-container { | |
| width: 100%; | |
| height: 25px; | |
| background: #333; | |
| border: 2px solid #555; | |
| position: relative; | |
| transform: skewX(-20deg); | |
| } | |
| .hp-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, #ff3333, #ff8888); | |
| width: 100%; | |
| transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .p2-hp { background: linear-gradient(90deg, #8888ff, #3333ff); float: right; } | |
| .chakra-bar-container { | |
| width: 80%; | |
| height: 10px; | |
| background: #222; | |
| border: 1px solid #444; | |
| margin-top: 5px; | |
| position: relative; | |
| transform: skewX(-20deg); | |
| } | |
| .chakra-bar { | |
| height: 100%; | |
| width: 0%; | |
| background: linear-gradient(90deg, #00ffff, #0088ff); | |
| transition: width 0.1s linear; | |
| } | |
| .p2-chakra { background: linear-gradient(90deg, #aa00ff, #ff00ff); float: right; } | |
| /* Timer */ | |
| .timer-box { | |
| background: rgba(0,0,0,0.7); | |
| padding: 10px 20px; | |
| border: 2px solid #555; | |
| border-radius: 5px; | |
| text-align: center; | |
| } | |
| .timer { | |
| font-family: 'Bangers', cursive; | |
| font-size: 2.5rem; | |
| color: #fff; | |
| } | |
| /* Controls Hint */ | |
| .controls-hint { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| gap: 15px; | |
| background: rgba(0, 0, 0, 0.7); | |
| padding: 10px 20px; | |
| border-radius: 30px; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| } | |
| .key-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.8rem; | |
| color: #aaa; | |
| } | |
| .key-icon { | |
| width: 30px; | |
| height: 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 50%; | |
| font-weight: bold; | |
| color: #fff; | |
| font-size: 0.9rem; | |
| box-shadow: 0 2px 5px rgba(0,0,0,0.5); | |
| } | |
| .k-x { background: #e74c3c; } /* Jump */ | |
| .k-square { background: #3498db; } /* Light */ | |
| .k-triangle { background: #2ecc71; } /* Heavy */ | |
| .k-circle { background: #f1c40f; color: #333; } /* Jutsu */ | |
| /* Screens (Start / Game Over) */ | |
| .overlay-screen { | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.85); | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 20; | |
| backdrop-filter: blur(8px); | |
| transition: opacity 0.3s; | |
| } | |
| .hidden { opacity: 0; pointer-events: none; } | |
| .game-title { | |
| font-family: 'Bangers', cursive; | |
| font-size: 4rem; | |
| color: transparent; | |
| -webkit-text-stroke: 2px #fff; | |
| margin-bottom: 20px; | |
| text-transform: uppercase; | |
| letter-spacing: 5px; | |
| } | |
| .game-title span { color: var(--naruto-orange); -webkit-text-stroke: 0; } | |
| .btn-start { | |
| padding: 15px 40px; | |
| font-size: 1.5rem; | |
| font-family: 'Bangers', cursive; | |
| background: var(--naruto-orange); | |
| color: white; | |
| border: none; | |
| cursor: pointer; | |
| transform: skewX(-10deg); | |
| transition: transform 0.2s, background 0.2s; | |
| box-shadow: 0 0 20px rgba(255, 94, 0, 0.5); | |
| } | |
| .btn-start:hover { | |
| transform: skewX(-10deg) scale(1.1); | |
| background: #fff; | |
| color: var(--naruto-orange); | |
| } | |
| /* Toast Messages */ | |
| #toast-container { | |
| position: absolute; | |
| top: 20%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 10px; | |
| pointer-events: none; | |
| } | |
| .combo-text { | |
| font-family: 'Bangers', cursive; | |
| font-size: 2rem; | |
| color: #fff; | |
| text-shadow: 0 0 10px var(--naruto-orange); | |
| animation: popUp 0.8s ease-out forwards; | |
| } | |
| @keyframes popUp { | |
| 0% { transform: scale(0.5); opacity: 0; } | |
| 50% { transform: scale(1.2); opacity: 1; } | |
| 100% { transform: scale(1.0) translateY(-20px); opacity: 0; } | |
| } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .controls-hint { display: none; } /* Hide hints on mobile */ | |
| .game-title { font-size: 2.5rem; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="logo"> | |
| <i class="fa-solid fa-bolt"></i> | |
| <span>SHIPPUDEN <span>DUEL</span></span> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with"> | |
| Built with anycoder <i class="fa-solid fa-external-link-alt"></i> | |
| </a> | |
| </header> | |
| <main> | |
| <div id="game-wrapper"> | |
| <canvas id="gameCanvas"></canvas> | |
| <!-- UI Layer --> | |
| <div id="ui-layer"> | |
| <!-- HUD --> | |
| <div class="hud-top"> | |
| <!-- Player 1 (Naruto) --> | |
| <div class="player-status p1-status"> | |
| <div class="name-tag p1-name">Naruto</div> | |
| <div class="bar-container"> | |
| <div class="hp-bar" id="p1-hp" style="width: 100%;"></div> | |
| </div> | |
| <div class="chakra-bar-container"> | |
| <div class="chakra-bar" id="p1-chakra"></div> | |
| </div> | |
| </div> | |
| <!-- Timer --> | |
| <div class="timer-box"> | |
| <div class="timer" id="game-timer">60</div> | |
| </div> | |
| <!-- Player 2 (Sasuke) --> | |
| <div class="player-status p2-status"> | |
| <div class="name-tag p2-name">Sasuke (AI)</div> | |
| <div class="bar-container"> | |
| <div class="hp-bar p2-hp" id="p2-hp" style="width: 100%;"></div> | |
| </div> | |
| <div class="chakra-bar-container"> | |
| <div class="chakra-bar p2-chakra" id="p2-chakra"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toasts --> | |
| <div id="toast-container"></div> | |
| <!-- Controls Hint --> | |
| <div class="controls-hint"> | |
| <div class="key-group"><div class="key-icon k-x">X</div> Saut</div> | |
| <div class="key-group"><div class="key-icon k-square">■</div> Frappe</div> | |
| <div class="key-group"><div class="key-icon k-triangle">▲</div> Lourd</div> | |
| <div class="key-group"><div class="key-icon k-circle">●</div> Jutsu</div> | |
| </div> | |
| </div> | |
| <!-- Start Screen --> | |
| <div id="start-screen" class="overlay-screen"> | |
| <h1 class="game-title">Ninja <span>Duel</span></h1> | |
| <p style="margin-bottom: 30px; color: #ccc;">Utilisez Z, E, R et Espace</p> | |
| <button class="btn-start" onclick="startGame()">COMBATTRE</button> | |
| </div> | |
| <!-- Game Over Screen --> | |
| <div id="game-over-screen" class="overlay-screen hidden"> | |
| <h1 class="game-title" id="winner-text">VICTOIRE</h1> | |
| <button class="btn-start" onclick="resetGame()">REJOUER</button> | |
| </div> | |
| </div> | |
| </main> | |
| <script> | |
| // --- Configuration --- | |
| const canvas = document.getElementById('gameCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| // Game Constants | |
| const GRAVITY = 0.8; | |
| const GROUND_Y = 550; // Ajusté dynamiquement selon la hauteur | |
| const FRICTION = 0.8; | |
| // Inputs Mapping | |
| // X = Space (Jump) | |
| // Square = Z (Light Hit) | |
| // Triangle = E (Heavy Hit) | |
| // Circle = R (Jutsu) | |
| const KEYS = { | |
| LEFT: 'ArrowLeft', | |
| RIGHT: 'ArrowRight', | |
| JUMP: ' ', | |
| LIGHT: 'z', // Carré | |
| HEAVY: 'e', // Triangle | |
| JUTSU: 'r' // Rond | |
| }; | |
| // --- Game State --- | |
| let gameState = 'MENU'; // MENU, PLAYING, GAMEOVER | |
| let animationId; | |
| let lastTime = 0; | |
| let timer = 60; | |
| let timerInterval; | |
| let cameraShake = 0; | |
| // --- Classes --- | |
| class Sprite { | |
| constructor({ position, velocity, color, isPlayer, name }) { | |
| this.position = position; | |
| this.velocity = velocity; | |
| this.width = 50; | |
| this.height = 100; | |
| this.color = color; | |
| this.isPlayer = isPlayer; | |
| this.name = name; | |
| // Stats | |
| this.hp = 100; | |
| this.maxHp = 100; | |
| this.chakra = 0; | |
| this.maxChakra = 100; | |
| this.isDead = false; | |
| // Combat | |
| this.isAttacking = false; | |
| this.isBlocking = false; | |
| this.attackCooldown = 0; | |
| this.facingRight = isPlayer; // P1 regarde droite, P2 regarde gauche | |
| this.hitbox = { x: 0, y: 0, width: 0, height: 0 }; | |
| } | |
| draw() { | |
| ctx.save(); | |
| // Flip if facing left | |
| if (!this.facingRight) { | |
| ctx.translate(this.position.x + this.width, this.position.y); | |
| ctx.scale(-1, 1); | |
| ctx.translate(-(this.position.x + this.width), -this.position.y); | |
| } | |
| // Body (Jumpsuit) | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(this.position.x, this.position.y, this.width, this.height); | |
| // Head Band | |
| ctx.fillStyle = '#333'; | |
| ctx.fillRect(this.position.x - 5, this.position.y + 10, this.width + 10, 10); | |
| ctx.fillStyle = '#ccc'; // Metal plate | |
| ctx.fillRect(this.position.x + 5, this.position.y + 12, 30, 6); | |
| // Eyes (Simple) | |
| ctx.fillStyle = this.isPlayer ? '#00ccff' : '#6600cc'; | |
| ctx.fillRect(this.position.x + 30, this.position.y + 25, 8, 5); | |
| // Spiky Hair (Procedural) | |
| ctx.fillStyle = this.isPlayer ? '#ffcc00' : '#000000'; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.position.x - 5, this.position.y + 10); | |
| ctx.lineTo(this.position.x + 10, this.position.y - 15); | |
| ctx.lineTo(this.position.x + 25, this.position.y + 5); | |
| ctx.lineTo(this.position.x + 40, this.position.y - 20); | |
| ctx.lineTo(this.position.x + 55, this.position.y + 10); | |
| ctx.fill(); | |
| // Attack Hitbox (Debug visual - optional, usually invisible) | |
| if (this.isAttacking) { | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; | |
| ctx.fillRect(this.hitbox.x, this.hitbox.y, this.hitbox.width, this.hitbox.height); | |
| } | |
| ctx.restore(); | |
| } | |
| update() { | |
| this.draw(); | |
| if (!this.isDead) { | |
| this.position.x += this.velocity.x; | |
| this.position.y += this.velocity.y; | |
| // Gravity | |
| if (this.position.y + this.height + this.velocity.y >= canvas.height - 50) { | |
| this.velocity.y = 0; | |
| this.position.y = canvas.height - 50 - this.height; | |
| } else { | |
| this.velocity.y += GRAVITY; | |
| } | |
| // Boundaries | |
| if (this.position.x < 0) this.position.x = 0; | |
| if (this.position.x + this.width > canvas.width) this.position.x = canvas.width - this.width; | |
| } | |
| // Face opponent | |
| if (this.isPlayer) { | |
| this.facingRight = this.position.x < enemy.position.x; | |
| } else { | |
| this.facingRight = this.position.x > player.position.x; | |
| } | |
| // Cooldowns | |
| if (this.attackCooldown > 0) this.attackCooldown--; | |
| // Passive Chakra Regen | |
| if (this.chakra < this.maxChakra) this.chakra += 0.05; | |
| } | |
| attack(type) { | |
| if (this.attackCooldown > 0 || this.isDead) return; | |
| this.isAttacking = true; | |
| // Define Hitbox | |
| const reach = type === 'heavy' ? 80 : 60; | |
| const damage = type === 'heavy' ? 15 : 5; | |
| const cooldown = type === 'heavy' ? 40 : 20; | |
| this.hitbox = { | |
| x: this.facingRight ? this.position.x + this.width : this.position.x - reach, | |
| y: this.position.y + 20, | |
| width: reach, | |
| height: 40 | |
| }; | |
| this.attackCooldown = cooldown; | |
| setTimeout(() => { | |
| this.isAttacking = false; | |
| }, 100); | |
| return damage; | |
| } | |
| jutsu() { | |
| if (this.chakra < 50 || this.attackCooldown > 0 || this.isDead) return false; | |
| this.chakra -= 50; | |
| this.attackCooldown = 60; // Long cooldown | |
| // Create Projectile | |
| const direction = this.facingRight ? 1 : -1; | |
| const startX = this.facingRight ? this.position.x + this.width : this.position.x - 40; | |
| const startY = this.position.y + 40; | |
| const type = this.isPlayer ? 'rasengan' : 'chidori'; | |
| projectiles.push(new Projectile({ | |
| position: { x: startX, y: startY }, | |
| velocity: { x: 10 * direction, y: 0 }, | |
| owner: this, | |
| type: type | |
| })); | |
| showFloatingText(type === 'rasengan' ? "RASENGAN!" : "CHIDORI!", this.position.x, this.position.y - 20, this.isPlayer ? '#00ffff' : '#aa00ff'); | |
| return true; | |
| } | |
| takeHit(damage) { | |
| this.hp -= damage; | |
| if (this.hp < 0) this.hp = 0; | |
| // Knockback | |
| const dir = this.isPlayer ? -1 : 1; | |
| this.velocity.x = 10 * dir; | |
| this.velocity.y = -5; | |
| if (this.hp <= 0) { | |
| this.isDead = true; | |
| endGame(!this.isPlayer); // Opponent wins | |
| } | |
| } | |
| } | |
| class Projectile { | |
| constructor({ position, velocity, owner, type }) { | |
| this.position = position; | |
| this.velocity = velocity; | |
| this.owner = owner; | |
| this.type = type; | |
| this.radius = 25; | |
| this.active = true; | |
| } | |
| draw() { | |
| ctx.beginPath(); | |
| ctx.arc(this.position.x, this.position.y, this.radius, 0, Math.PI * 2); | |
| if (this.type === 'rasengan') { | |
| ctx.fillStyle = '#00ccff'; | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = '#00ffff'; | |
| } else { | |
| ctx.fillStyle = '#aa00ff'; | |
| ctx.shadowBlur = 20; | |
| ctx.shadowColor = '#ff00ff'; | |
| } | |
| ctx.fill(); | |
| ctx.shadowBlur = 0; | |
| ctx.closePath(); | |
| } | |
| update() { | |
| this.draw(); | |
| this.position.x += this.velocity.x; | |
| // Remove if off screen | |
| if (this.position.x < 0 || this.position.x > canvas.width) { | |
| this.active = false; | |
| } | |
| } | |
| } | |
| class Particle { | |
| constructor({ position, velocity, color }) { | |
| this.position = position; | |
| this.velocity = velocity; | |
| this.color = color; | |
| this.alpha = 1; | |
| } | |
| draw() { | |
| ctx.save(); | |
| ctx.globalAlpha = this.alpha; | |
| ctx.fillStyle = this.color; | |
| ctx.fillRect(this.position.x, this.position.y, 4, 4); | |
| ctx.restore(); | |
| } | |
| update() { | |
| this.draw(); | |
| this.position.x += this.velocity.x; | |
| this.position.y += this.velocity.y; | |
| this.alpha -= 0.02; | |
| } | |
| } | |
| // --- Instances --- | |
| let player; | |
| let enemy; | |
| let projectiles = []; | |
| let particles = []; | |
| // --- Input Handling --- | |
| const keys = { | |
| ArrowRight: { pressed: false }, | |
| ArrowLeft: { pressed: false }, | |
| ArrowUp: { pressed: false } | |
| }; | |
| window.addEventListener('keydown', (e) => { | |
| if (gameState !== 'PLAYING') return; | |
| switch (e.key.toLowerCase()) { | |
| case KEYS.RIGHT: keys.ArrowRight.pressed = true; break; | |
| case KEYS.LEFT: keys.ArrowLeft.pressed = true; break; | |
| case KEYS.JUMP: | |
| if (player.velocity.y === 0) player.velocity.y = -20; | |
| break; | |
| case KEYS.LIGHT: // Square | |
| player.attack('light'); | |
| checkCollision(player, enemy, 5); | |
| break; | |
| case KEYS.HEAVY: // Triangle | |
| player.attack('heavy'); | |
| checkCollision(player, enemy, 15); | |
| break; | |
| case KEYS.JUTSU: // Circle | |
| player.jutsu(); | |
| break; | |
| } | |
| }); | |
| window.addEventListener('keyup', (e) => { | |
| switch (e.key.toLowerCase()) { | |
| case KEYS.RIGHT: keys.ArrowRight.pressed = false; break; | |
| case KEYS.LEFT: keys.ArrowLeft.pressed = false; break; | |
| } | |
| }); | |
| // --- Game Logic --- | |
| function initGame() { | |
| resizeCanvas(); | |
| player = new Sprite({ | |
| position: { x: 100, y: 0 }, | |
| velocity: { x: 0, y: 0 }, | |
| color: '#ff5e00', // Naruto Orange | |
| isPlayer: true, | |
| name: 'Naruto' | |
| }); | |
| enemy = new Sprite({ | |
| position: { x: canvas.width - 150, y: 0 }, | |
| velocity: { x: 0, y: 0 }, | |
| color: '#2e3192', // Sasuke Blue | |
| isPlayer: false, | |
| name: 'Sasuke' | |
| }); | |
| projectiles = []; | |
| particles = []; | |
| timer = 60; | |
| cameraShake = 0; | |
| document.getElementById('p1-hp').style.width = '100%'; | |
| document.getElementById('p2-hp').style.width = '100%'; | |
| document.getElementById('game-timer').textContent = timer; | |
| } | |
| function checkCollision(attacker, receiver, damage) { | |
| if ( | |
| attacker.hitbox.x < receiver.position.x + receiver.width && | |
| attacker.hitbox.x + attacker.hitbox.width > receiver.position.x && | |
| attacker.hitbox.y < receiver.position.y + receiver.height && | |
| attacker.hitbox.y + attacker.hitbox.height > receiver.position.y | |
| ) { | |
| receiver.takeHit(damage); | |
| createParticles(receiver.position.x + receiver.width/2, receiver.position.y + receiver.height/2, 10, '#fff'); | |
| startShake(5); | |
| updateUI(); | |
| } | |
| } | |
| function createParticles(x, y, count, color) { | |
| for (let i = 0; i < count; i++) { | |
| particles.push(new Particle({ | |
| position: { x, y }, | |
| velocity: { | |
| x: (Math.random() - 0.5) * 10, | |
| y: (Math.random() - 0.5) * 10 | |
| }, | |
| color: color | |
| })); | |
| } | |
| } | |
| function startShake(intensity) { | |
| cameraShake = intensity; | |
| } | |
| function updateUI() { | |
| document.getElementById('p1-hp').style.width = player.hp + '%'; | |
| document.getElementById('p2-hp').style.width = enemy.hp + '%'; | |
| document.getElementById('p1-chakra').style.width = player.chakra + '%'; | |
| document.getElementById('p2-chakra').style.width = enemy.chakra + '%'; | |
| } | |
| function showFloatingText(text, x, y, color) { | |
| const toast = document.createElement('div'); | |
| toast.className = 'combo-text'; | |
| toast.textContent = text; | |
| toast.style.color = color; | |
| toast.style.left = x + 'px'; | |
| toast.style.top = y + 'px'; | |
| document.getElementById('toast-container').appendChild(toast); | |
| setTimeout(() => toast.remove(), 1000); | |
| } | |
| // --- AI Logic (Simple) --- | |
| function updateAI() { | |
| if (enemy.isDead || gameState !== 'PLAYING') return; | |
| const distance = Math.abs(player.position.x - enemy.position.x); | |
| const action = Math.random(); | |
| // Movement | |
| if (distance > 80) { | |
| enemy.velocity.x = player.position.x < enemy.position.x ? -3 : 3; | |
| } else { | |
| enemy.velocity.x = 0; | |
| } | |
| // Jump randomly or if attacked | |
| if (action < 0.01 && enemy.velocity.y === 0) enemy.velocity.y = -15; | |
| // Attack | |
| if (distance < 100) { | |
| if (action < 0.05) { | |
| enemy.attack('light'); | |
| checkCollision(enemy, player, 5); | |
| } else if (action < 0.02) { | |
| enemy.attack('heavy'); | |
| checkCollision(enemy, player, 15); | |
| } | |
| } | |
| // Jutsu if far and chakra full | |
| if (distance > 200 && enemy.chakra >= 50 && action < 0.02) { | |
| enemy.jutsu(); | |
| } | |
| } | |
| // --- Main Loop --- | |
| function animate() { | |
| animationId = requestAnimationFrame(animate); | |
| // Clear Screen | |
| ctx.fillStyle = '#1a1a1a'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw Background (Simple Floor) | |
| ctx.fillStyle = '#111'; | |
| ctx.fillRect(0, canvas.height - 50, canvas.width, 50); | |
| // Grid effect on floor | |
| ctx.strokeStyle = '#333'; | |
| ctx.beginPath(); | |
| ctx.moveTo(0, canvas.height - 50); | |
| ctx.lineTo(canvas.width, canvas.height - 50); | |
| ctx.stroke(); | |
| // Camera Shake | |
| if (cameraShake > 0) { | |
| ctx.save(); | |
| const dx = (Math.random() - 0.5) * cameraShake; | |
| const dy = (Math.random() - 0.5) * cameraShake; | |
| ctx.translate(dx, dy); | |
| cameraShake *= 0.9; | |
| if (cameraShake < 0.5) cameraShake = 0; | |
| } | |
| // Player Movement | |
| player.velocity.x = 0; | |
| if (keys.ArrowRight.pressed) player.velocity.x = 5; | |
| else if (keys.ArrowLeft.pressed) player.velocity.x = -5; | |
| // Update Entities | |
| player.update(); | |
| enemy.update(); | |
| updateAI(); | |
| // Projectiles | |
| projectiles.forEach((proj, index) => { | |
| proj.update(); | |
| // Hit detection | |
| const target = proj.owner === player ? enemy : player; | |
| if ( | |
| proj.position.x < target.position.x + target.width && | |
| proj.position.x + proj.radius > target.position.x && | |
| proj.position.y < target.position.y + target.height && | |
| proj.position.y + proj.radius > target.position.y | |
| ) { | |
| target.takeHit(35); | |
| proj.active = false; | |
| createParticles(proj.position.x, proj.position.y, 15, proj.type === 'rasengan' ? '#00ffff' : '#ff00ff'); | |
| startShake(15); | |
| updateUI(); | |
| } | |
| if (!proj.active) projectiles.splice(index, 1); | |
| }); | |
| // Particles | |
| particles.forEach((part, index) => { | |
| if (part.alpha <= 0) particles.splice(index, 1); | |
| else part.update(); | |
| }); | |
| // Restore shake context | |
| if (cameraShake > 0 || ctx.getTransform().e !== 0) ctx.restore(); | |
| } | |
| // --- Game Flow Control --- | |
| function startGame() { | |
| document.getElementById('start-screen').classList.add('hidden'); | |
| document.getElementById('game-over-screen').classList.add('hidden'); | |
| gameState = 'PLAYING'; | |
| initGame(); | |
| clearInterval(timerInterval); | |
| timerInterval = setInterval(() => { | |
| if (gameState === 'PLAYING') { | |
| timer--; | |
| document.getElementById('game-timer').textContent = timer; | |
| if (timer <= 0) { | |
| endGame(player.hp > enemy.hp); // Who has more HP wins | |
| } | |
| } | |
| }, 1000); | |
| } | |
| function endGame(playerWins) { | |
| gameState = 'GAMEOVER'; | |
| clearInterval(timerInterval); | |
| const winnerText = document.getElementById('winner-text'); | |
| if (playerWins) { | |
| winnerText.innerHTML = '<span style="color:#ffcc00">NARUTO</span> GAGNE!'; | |
| } else { | |
| winnerText.innerHTML = '<span style="color:#aaddff">SASUKE</span> GAGNE!'; | |
| } | |
| document.getElementById('game-over-screen').classList.remove('hidden'); | |
| } | |
| function resetGame() { | |
| startGame(); | |
| } | |
| // --- Utils --- | |
| function resizeCanvas() { | |
| const wrapper = document.getElementById('game-wrapper'); | |
| canvas.width = wrapper.clientWidth; | |
| canvas.height = wrapper.clientHeight; | |
| } | |
| window.addEventListener('resize', resizeCanvas); | |
| // Start Loop | |
| resizeCanvas(); | |
| animate(); | |
| </script> | |
| </body> | |
| </html> |