Shopno AI
লগইন ৩ দিনের Pro ট্রায়াল
admin / Fruit Ninja public
Download ZIP

Inspired From Fruit Ninja Games I Made it on Html 5

কোড (Code) কমিটস (Commits) 2 রিলিজ (Releases) 1 ইস্যুজ (Issues) 0
ব্যাক টু ফাইল ট্রি
index.html 36,863 bytes
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>🍉 Fruit Ninja — Slice Master</title>
<style>
  /* === DESIGN SYSTEM === */
  /* Font: Outfit — premium geometric sans for game UI */
  @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;500;700;900&display=swap');

  :root {
    --accent: #ff5e3a;
    --accent-glow: rgba(255,94,58,0.4);
    --freeze: #4fc3f7;
    --super: #ffd93d;
    --rain: #6ddb4f;
    --bg-dark: #1a0a14;
    --bg-mid: #2d1b1e;
    --text: #fff5e6;
    --danger: #ff3b3b;
  }

  * { margin: 0; padding: 0; box-sizing: border-box; user-select: none; -webkit-user-select: none; }
  html, body { overflow: hidden; height: 100%; width: 100%; }
  body {
    font-family: 'Outfit', sans-serif;
    background: radial-gradient(ellipse at center, var(--bg-mid) 0%, var(--bg-dark) 100%);
    color: var(--text);
    touch-action: none;
  }

  canvas { display: block; position: absolute; top: 0; left: 0; z-index: 1; }

  /* === HUD === */
  #hud {
    position: absolute;
    top: 0; left: 0; right: 0;
    z-index: 10;
    padding: 16px 24px;
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    pointer-events: none;
  }

  .hud-panel {
    background: rgba(0,0,0,0.45);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 14px;
    padding: 10px 18px;
    display: flex;
    align-items: center;
    gap: 10px;
  }

  .hud-panel .label {
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 1.5px;
    text-transform: uppercase;
    opacity: 0.6;
  }

  .hud-panel .value {
    font-size: 28px;
    font-weight: 900;
    line-height: 1;
  }

  #livesPanel { gap: 6px; }
  .life-icon {
    width: 22px; height: 22px;
    border-radius: 50%;
    background: var(--accent);
    box-shadow: 0 0 10px var(--accent-glow);
    transition: all 0.3s ease;
  }
  .life-icon.lost {
    background: rgba(255,255,255,0.1);
    box-shadow: none;
    transform: scale(0.7);
  }

  /* === POWER-UP BAR === */
  #powerBar {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 10;
    display: flex;
    gap: 12px;
    pointer-events: none;
  }
  .power-badge {
    display: flex;
    align-items: center;
    gap: 8px;
    background: rgba(0,0,0,0.5);
    backdrop-filter: blur(8px);
    border: 1px solid rgba(255,255,255,0.1);
    border-radius: 30px;
    padding: 6px 16px;
    font-size: 13px;
    font-weight: 700;
    opacity: 0;
    transform: translateY(20px);
    transition: opacity 0.3s, transform 0.3s;
  }
  .power-badge.active { opacity: 1; transform: translateY(0); }
  .power-badge .pdot {
    width: 10px; height: 10px;
    border-radius: 50%;
    animation: pulse 1s infinite;
  }
  .power-badge.freeze .pdot { background: var(--freeze); box-shadow: 0 0 8px var(--freeze); }
  .power-badge.super .pdot { background: var(--super); box-shadow: 0 0 8px var(--super); }
  .power-badge.rain .pdot { background: var(--rain); box-shadow: 0 0 8px var(--rain); }

  @keyframes pulse {
    0%,100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.4); opacity: 0.7; }
  }

  /* === COMBO POPUP === */
  #comboPopup {
    position: absolute;
    z-index: 15;
    font-size: 36px;
    font-weight: 900;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.3s;
    text-shadow: 0 0 20px currentColor;
  }

  /* === SCREENS === */
  #screen {
    position: absolute;
    top: 0; left: 0; width: 100%; height: 100%;
    z-index: 20;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 20px;
    background: radial-gradient(ellipse at center, rgba(45,27,30,0.85) 0%, rgba(26,10,20,0.95) 100%);
    backdrop-filter: blur(6px);
    transition: opacity 0.5s, visibility 0.5s;
  }
  #screen.hidden { opacity: 0; visibility: hidden; }

  #screen h1 {
    font-size: 56px;
    font-weight: 900;
    letter-spacing: -2px;
    text-align: center;
    line-height: 1.05;
    background: linear-gradient(135deg, #ff5e3a, #ff9f43, #ffd93d);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    filter: drop-shadow(0 4px 18px rgba(255,94,58,0.25));
  }
  #screen h1 .sub {
    display: block;
    font-size: 18px;
    font-weight: 500;
    letter-spacing: 4px;
    text-transform: uppercase;
    -webkit-text-fill-color: var(--text);
    color: var(--text);
    opacity: 0.7;
    margin-top: 6px;
  }

  #screen .tagline {
    font-size: 16px;
    opacity: 0.6;
    max-width: 400px;
    text-align: center;
    line-height: 1.5;
  }

  .features-row {
    display: flex;
    gap: 14px;
    flex-wrap: wrap;
    justify-content: center;
    max-width: 460px;
  }
  .feat-chip {
    display: flex;
    align-items: center;
    gap: 6px;
    background: rgba(255,255,255,0.05);
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 20px;
    padding: 6px 14px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.5px;
  }
  .feat-chip .dot { width: 8px; height: 8px; border-radius: 50%; }

  .btn {
    font-family: inherit;
    font-size: 18px;
    font-weight: 700;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: #fff;
    background: var(--accent);
    border: none;
    border-radius: 14px;
    padding: 14px 42px;
    cursor: pointer;
    box-shadow: 0 8px 28px var(--accent-glow);
    transition: transform 0.2s, box-shadow 0.2s;
  }
  .btn:hover { transform: translateY(-2px); box-shadow: 0 12px 34px var(--accent-glow); }
  .btn:active { transform: translateY(0); }

  .final-score {
    font-size: 64px;
    font-weight: 900;
    line-height: 1;
  }
  .final-label {
    font-size: 14px;
    font-weight: 500;
    letter-spacing: 2px;
    text-transform: uppercase;
    opacity: 0.5;
    margin-bottom: -10px;
  }

  .score-row {
    display: flex;
    gap: 30px;
  }
  .score-row .stat { text-align: center; }
  .score-row .stat .num { font-size: 28px; font-weight: 900; }
  .score-row .stat .lbl { font-size: 11px; opacity: 0.5; text-transform: uppercase; letter-spacing: 1.5px; }

  /* How to play */
  .howto {
    display: flex;
    gap: 20px;
    flex-wrap: wrap;
    justify-content: center;
    max-width: 500px;
  }
  .howto-item {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    opacity: 0.75;
  }
  .howto-item .ico {
    width: 36px; height: 36px;
    border-radius: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    background: rgba(255,255,255,0.06);
    border: 1px solid rgba(255,255,255,0.08);
  }
</style>
</head>
<body>

<!-- Game Canvas -->
<canvas id="canvas"></canvas>

<!-- HUD -->
<div id="hud">
  <div class="hud-panel" id="scorePanel">
    <span class="label">Score</span>
    <span class="value" id="scoreEl">0</span>
  </div>
  <div class="hud-panel" id="livesPanel">
    <span class="label" style="margin-right:4px">Lives</span>
    <div class="life-icon" data-life="1"></div>
    <div class="life-icon" data-life="2"></div>
    <div class="life-icon" data-life="3"></div>
  </div>
  <div class="hud-panel" id="bestPanel">
    <span class="label">Best</span>
    <span class="value" id="bestEl">0</span>
  </div>
</div>

<!-- Power-up active bar -->
<div id="powerBar">
  <div class="power-badge freeze" id="pb-freeze"><div class="pdot"></div> FREEZE</div>
  <div class="power-badge super" id="pb-super"><div class="pdot"></div> SUPER SLICE</div>
  <div class="power-badge rain" id="pb-rain"><div class="pdot"></div> FRUIT RAIN</div>
</div>

<!-- Combo popup -->
<div id="comboPopup"></div>

<!-- Start / Game Over Screen -->
<div id="screen">
  <h1 id="screenTitle">🍉 Fruit Ninja<span class="sub">Slice Master</span></h1>
  <p class="tagline" id="screenTagline">Swipe to slice fruits, chain combos, grab power-ups, and dodge the bombs!</p>
  <div class="features-row" id="featRow">
    <div class="feat-chip"><span class="dot" style="background:var(--accent)"></span> Combos</div>
    <div class="feat-chip"><span class="dot" style="background:var(--freeze)"></span> Freeze</div>
    <div class="feat-chip"><span class="dot" style="background:var(--super)"></span> Super Power</div>
    <div class="feat-chip"><span class="dot" style="background:var(--rain)"></span> Fruit Rain</div>
    <div class="feat-chip"><span class="dot" style="background:var(--danger)"></span> Traps</div>
  </div>
  <div class="howto" id="howto">
    <div class="howto-item"><div class="ico">🍉</div> <span>Slice fruits<br>for points</span></div>
    <div class="howto-item"><div class="ico">💣</div> <span>Avoid bombs<br>or lose a life</span></div>
    <div class="howto-item"><div class="ico">❄️</div> <span>Freeze slows<br>everything</span></div>
    <div class="howto-item"><div class="ico">⚡</div> <span>Super = double<br>score slices</span></div>
  </div>
  <button class="btn" id="startBtn">Play</button>
</div>

<script>
"use strict";
/* =================================================================
   FRUIT NINJA — SLICE MASTER
   Single-file HTML5 Canvas Game
   Features: Realistic fruits, slice physics, combo system,
   power-ups (Freeze, Super, Fruit Rain), traps/bombs, particles
   ================================================================= */

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d', { alpha: false });
const DPR = Math.max(1, window.devicePixelRatio || 1);
let W, H;

function resize() {
  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);
}
window.addEventListener('resize', resize);
resize();

/* ===== GAME STATE ===== */
const State = { MENU: 'menu', PLAYING: 'playing', OVER: 'over' };
let gameState = State.MENU;
let score = 0;
let best = parseInt(localStorage.getItem('fruitNinjaBest') || '0');
let lives = 3;
let timeAccumulator = 0;
let spawnTimer = 0;
let spawnInterval = 1400; // ms
let difficulty = 1;
let shakeAmount = 0;
let freezeTimer = 0;
let superTimer = 0;
let rainTimer = 0;
let comboCount = 0;
let comboTimer = 0;
let comboHits = [];
let slicesThisSwipe = []; // for combo detection
let lastSwipeTime = 0;

document.getElementById('bestEl').textContent = best;

/* ===== UTILITIES ===== */
const rand = (a, b) => a + Math.random() * (b - a);
const randInt = (a, b) => Math.floor(rand(a, b + 1));
const choose = arr => arr[Math.floor(Math.random() * arr.length)];
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
const lerp = (a, b, t) => a + (b - a) * t;

/* ===== COLORS / TEXTURES for fruits ===== */
const FRUIT_TYPES = [
  { name: 'watermelon',  rCol: '#ff5e5e', rCol2: '#ff8a8a', skinColor1: '#2d8f3e', skinColor2: '#1a6b2e', innerColor: '#ff7a7a', seeds: true,  seedColor: '#3d2b1f', radius: 48 },
  { name: 'orange',      rCol: '#ff9f43', rCol2: '#ffb96b', skinColor1: '#ff8c1a', skinColor2: '#e07a0a', innerColor: '#ffb15c', seeds: false, radius: 38 },
  { name: 'apple',       rCol: '#e84855', rCol2: '#ff7070', skinColor1: '#d63447', skinColor2: '#a52a35', innerColor: '#fff5e6', seeds: true,  seedColor: '#5a3a1a', radius: 40 },
  { name: 'lime',        rCol: '#6ddb4f', rCol2: '#9af07d', skinColor1: '#4caf50', skinColor2: '#357a38', innerColor: '#d4f5c4', seeds: false, radius: 34 },
  { name: 'grape',       rCol: '#9b59b6', rCol2: '#b370cc', skinColor1: '#8e44ad', skinColor2: '#6c3492', innerColor: '#c8a4d4', seeds: false, radius: 32 },
  { name: 'pineapple',   rCol: '#f1c40f', rCol2: '#ffe066', skinColor1: '#e6b800', skinColor2: '#c79a00', innerColor: '#ffe88a', seeds: false, radius: 42 },
  { name: 'strawberry',  rCol: '#e84855', rCol2: '#ff6870', skinColor1: '#e84855', skinColor2: '#c0392b', innerColor: '#ffb3c1', seeds: true,  seedColor: '#ffe066', radius: 36 },
];

const SPECIAL_TYPES = [
  { name: 'freeze',  glow: '#4fc3f7', radius: 38 },  // ❄️ Freeze powerup
  { name: 'super',   glow: '#ffd93d', radius: 38 },   // ⚡ Super powerup
  { name: 'rain',    glow: '#6ddb4f', radius: 38 },   // 🌧️ Fruit Rain
];

// Trap / Bomb
const BOMB = { radius: 42, glow: '#ff3b3b' };

/* ===== FRUIT / OBJECT ===== */
class GameObject {
  constructor(typeData, isFruit, isSpecial, isBomb) {
    this.type = typeData;
    this.isFruit = isFruit;
    this.isSpecial = isSpecial;
    this.isBomb = isBomb;
    this.radius = typeData.radius || 40;
    this.x = rand(this.radius * 2, W - this.radius * 2);
    this.y = H + this.radius + 10;
    // Launch upward with angle
    const angle = rand(-Math.PI * 0.75, -Math.PI * 0.25);
    const power = rand(980, 1350) * difficulty * 0.001;
    // Velocity (tilt toward center)
    const targetX = W / 2;
    const dirX = (targetX - this.x) / W;
    this.vx = dirX * rand(150, 350);
    this.vy = -rand(750, 1100);
    this.rotation = rand(0, Math.PI * 2);
    this.rotSpeed = rand(-3, 3);
    this.sliced = false;
    this.alive = true;
    this.sliceProgress = 0;
    this.angleSlice = 0;
    this.halfA = null; // leftover half (canvas rendering)
    this.halfB = null;
    this.popOffset = 0;
  }

  update(dt, slowed) {
    const factor = slowed ? 0.28 : 1;
    this.x += this.vx * dt * factor;
    this.y += this.vy * dt * factor;
    this.vy += 980 * dt * factor; // gravity
    this.rotation += this.rotSpeed * dt * factor;
    if (this.sliced) this.sliceProgress = Math.min(1, this.sliceProgress + dt * 2);
    // Remove if off screen
    if (this.y > H + 200 && this.vy > 0) {
      this.alive = false;
      // Missed fruit = lose life (only for real fruits)
      if (this.isFruit && !this.sliced && gameState === State.PLAYING) {
        loseLife();
      }
    }
  }

  removePeaceful() {
    this.alive = false;
  }

  /* Slicinq */
  slice(angle) {
    if (this.sliced) return false;
    this.sliced = true;
    this.angleSlice = angle;
    // Not removing here; update handles removal
    return true;
  }

  /* Check if point (global) near fruit */
  hitTest(x1, y1, x2, y2) {
    // Distance from point to segment
    const dx = x2 - x1, dy = y2 - y1;
    const len = Math.sqrt(dx * dx + dy * dy);
    if (len === 0) return false;
    const t = clamp(((this.x - x1) * dx + (this.y - y1) * dy) / (len * len), 0, 1);
    const px = x1 + t * dx;
    const py = y1 + t * dy;
    const dist = Math.sqrt((this.x - px) ** 2 + (this.y - py) ** 2);
    return dist < this.radius + 8;
  }

  /* === Render fruit / bomb / special === */
  draw(c) {
    c.save();
    c.translate(this.x, this.y);
    if (this.isBomb) this.drawBomb(c);
    else if (this.isSpecial) this.drawSpecial(c);
    else this.drawFruit(c);
    c.restore();
  }

  drawFruit(c) {
    const r = this.radius;
    c.rotate(this.rotation);
    if (!this.sliced) {
      this.drawWholeFruit(c, r);
    } else {
      // Draw splitting fruit: two halves sliding apart
      const off = r * 1.5 * this.sliceProgress;
      const ang = this.angleSlice;
      // Half A
      c.save();
      c.rotate(ang - this.rotation); // undo self-rotation for slice line
      c.beginPath();
      c.arc(0, 0, r, -Math.PI / 2, Math.PI / 2);
      c.closePath();
      c.fillStyle = this.type.innerColor;
      c.fill();
      // inner gradient
      const ig = c.createRadialGradient(0, 0, r * 0.2, 0, 0, r);
      ig.addColorStop(0, this.type.innerColor);
      ig.addColorStop(0.8, this.type.rCol2);
      ig.addColorStop(1, this.type.skinColor1);
      c.fillStyle = ig;
      c.fill();
      if (this.type.seeds) this.drawSeeds(c, r, 0);
      // skin
      c.lineWidth = 4;
      c.strokeStyle = this.type.skinColor2;
      c.stroke();
      c.restore();
      // Half B (rotate other side)
      c.save();
      c.rotate(ang - this.rotation + Math.PI);
      c.beginPath();
      c.arc(0, 0, r, -Math.PI / 2, Math.PI / 2);
      c.closePath();
      c.fillStyle = this.type.innerColor;
      c.fill();
      const ig2 = c.createRadialGradient(0, 0, r * 0.2, 0, 0, r);
      ig2.addColorStop(0, this.type.innerColor);
      ig2.addColorStop(0.8, this.type.rCol2);
      ig2.addColorStop(1, this.type.skinColor1);
      c.fillStyle = ig2;
      c.fill();
      if (this.type.seeds) this.drawSeeds(c, r, 0);
      c.lineWidth = 4;
      c.strokeStyle = this.type.skinColor2;
      c.stroke();
      c.restore();
    }
  }

  drawSeeds(c, r, seedColor) {
    c.save();
    c.fillStyle = this.type.seedColor || '#3d2b1f';
    const positions = [
      [0, -r*0.3], [0, r*0.3], [-r*0.25, 0], [r*0.25, 0],
      [-r*0.15, -r*0.18], [r*0.15, -r*0.18], [-r*0.15, r*0.18], [r*0.15, r*0.18]
    ];
    positions.forEach(([sx, sy]) => {
      c.beginPath();
      c.ellipse(sx, sy, 2.5, 4, 0, 0, Math.PI * 2);
      c.fill();
    });
    c.restore();
  }

  drawWholeFruit(c, r) {
    // Outer glow
    c.shadowColor = this.type.rCol;
    c.shadowBlur = 12;
    c.beginPath();
    c.arc(0, 0, r, 0, Math.PI * 2);
    c.closePath();
    // gradient fill
    const g = c.createRadialGradient(-r * 0.3, -r * 0.3, r * 0.15, 0, 0, r);
    g.addColorStop(0, this.type.rCol2);
    g.addColorStop(0.6, this.type.rCol);
    g.addColorStop(1, this.type.skinColor2);
    c.fillStyle = g;
    c.fill();
    c.shadowBlur = 0;
    // Highlight
    c.beginPath();
    c.ellipse(-r * 0.35, -r * 0.35, r * 0.2, r * 0.12, -0.6, 0, Math.PI * 2);
    c.fillStyle = 'rgba(255,255,255,0.35)';
    c.fill();
    // Stem
    c.strokeStyle = '#3d2b1f';
    c.lineWidth = 3;
    c.beginPath();
    c.moveTo(0, -r);
    c.lineTo(4, -r - 12);
    c.stroke();
    // Leaf
    c.save();
    c.translate(4, -r - 10);
    c.fillStyle = '#5db840';
    c.beginPath();
    c.ellipse(-4, 0, 8, 3, 1.2, 0, Math.PI * 2);
    c.fill();
    c.restore();
  }

  drawBomb(c) {
    const r = this.radius;
    c.rotate(this.rotation);
    c.shadowColor = BOMB.glow;
    c.shadowBlur = 18 + Math.sin(timeAccumulator * 8) * 6;
    // Body
    const g = c.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r);
    g.addColorStop(0, '#555');
    g.addColorStop(0.7, '#2a2a2a');
    g.addColorStop(1, '#0a0a0a');
    c.fillStyle = g;
    c.beginPath();
    c.arc(0, 0, r, 0, Math.PI * 2);
    c.fill();
    c.shadowBlur = 0;
    // Fuse top
    c.strokeStyle = '#8B6F47';
    c.lineWidth = 4;
    c.beginPath();
    c.moveTo(0, -r);
    c.quadraticCurveTo(14, -r - 15, 8, -r - 24);
    c.stroke();
    // Spark
    c.fillStyle = '#ffd93d';
    c.shadowColor = '#ffd93d';
    c.shadowBlur = 14;
    c.beginPath();
    c.arc(8, -r - 24, 5, 0, Math.PI * 2);
    c.fill();
    // Inner spark ring
    c.fillStyle = '#ff6b3b';
    c.beginPath();
    c.arc(8, -r - 24, 2.5, 0, Math.PI * 2);
    c.fill();
    c.shadowBlur = 0;
    // Highlight ball
    c.beginPath();
    c.ellipse(-r*0.35, -r*0.35, r*0.18, r*0.1, -0.5, 0, Math.PI*2);
    c.fillStyle = 'rgba(255,255,255,0.18)';
    c.fill();
  }

  drawSpecial(c) {
    const r = this.radius;
    c.rotate(this.rotation);
    c.shadowColor = this.type.glow;
    c.shadowBlur = 20 + Math.sin(timeAccumulator * 6) * 8;
    // Core sphere
    const g = c.createRadialGradient(-r*0.3, -r*0.3, r*0.1, 0, 0, r);
    g.addColorStop(0, '#fff');
    g.addColorStop(0.4, this.type.glow);
    g.addColorStop(1, this.type.glow);
    c.fillStyle = g;
    c.beginPath();
    c.arc(0, 0, r, 0, Math.PI * 2);
    c.fill();
    c.shadowBlur = 0;
    // Inner symbol
    c.fillStyle = '#fff';
    c.font = 'bold 28px Outfit, sans-serif';
    c.textAlign = 'center';
    c.textBaseline = 'middle';
    if (this.type.name === 'freeze') c.fillText('❄', 0, 1);
    else if (this.type.name === 'super') c.fillText('⚡', 0, 1);
    else if (this.type.name === 'rain') c.fillText('🌧', 0, 1);
  }
}

/* ===== PARTICLES ===== */
const particles = [];
class JuiceParticle {
  constructor(x, y, color, vx, vy, size, life) {
    this.x = x; this.y = y;
    this.vx = vx; this.vy = vy;
    this.size = size;
    this.color = color;
    this.life = life;
    this.maxLife = life;
    this.gravity = rand(300, 600);
  }
  update(dt, slowed) {
    const f = slowed ? 0.3 : 1;
    this.x += this.vx * dt * f;
    this.y += this.vy * dt * f;
    this.vy += this.gravity * dt * f;
    this.life -= dt;
  }
  draw(c) {
    const alpha = clamp(this.life / this.maxLife, 0, 1);
    c.globalAlpha = alpha;
    c.fillStyle = this.color;
    c.beginPath();
    c.arc(this.x, this.y, this.size * alpha, 0, Math.PI * 2);
    c.fill();
    c.globalAlpha = 1;
  }
  get alive() { return this.life > 0; }
}

class SparkParticle {
  constructor(x, y, color, vx, vy, life) {
    this.x = x; this.y = y;
    this.vx = vx; this.vy = vy;
    this.color = color;
    this.life = life;
    this.maxLife = life;
    this.size = rand(2, 5);
  }
  update(dt) {
    this.x += this.vx * dt;
    this.y += this.vy * dt;
    this.vx *= 0.95;
    this.vy *= 0.95;
    this.life -= dt;
  }
  draw(c) {
    const a = clamp(this.life / this.maxLife, 0, 1);
    c.globalAlpha = a;
    c.fillStyle = this.color;
    c.beginPath();
    c.arc(this.x, this.y, this.size * a, 0, Math.PI * 2);
    c.fill();
    c.globalAlpha = 1;
  }
  get alive() { return this.life > 0; }
}

class ScorePopup {
  constructor(x, y, text, color, big) {
    this.x = x; this.y = y;
    this.text = text;
    this.color = color;
    this.life = 1.2;
    this.maxLife = 1.2;
    this.vy = -120;
    this.big = big;
  }
  update(dt) {
    this.x += 0;
    this.y += this.vy * dt;
    this.vy *= 0.93;
    this.life -= dt;
  }
  draw(c) {
    const a = clamp(this.life / this.maxLife, 0, 1);
    c.globalAlpha = a;
    c.font = this.big ? 'bold 28px Outfit' : 'bold 20px Outfit';
    c.textAlign = 'center';
    c.fillStyle = this.color;
    c.shadowColor = this.color;
    c.shadowBlur = 12;
    c.fillText(this.text, this.x, this.y);
    c.shadowBlur = 0;
    c.globalAlpha = 1;
  }
  get alive() { return this.life > 0; }
}

const scorePopups = [];

/* ===== GAME OBJECTS POOL ===== */
let objects = [];

/* ===== SLICE TRAIL ===== */
const trailPoints = [];
const MAX_TRAIL = 28;

/* ===== INPUT HANDLING ===== */
let mouseDown = false;
let lastMouseX = 0, lastMouseY = 0;

function getPointerPos(e) {
  const rect = canvas.getBoundingClientRect();
  if (e.touches && e.touches.length > 0) {
    return { x: e.touches[0].clientX - rect.left, y: e.touches[0].clientY - rect.top };
  }
  return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}

function onPointerDown(e) {
  e.preventDefault();
  const p = getPointerPos(e);
  mouseDown = true;
  lastMouseX = p.x;
  lastMouseY = p.y;
  trailPoints.length = 0;
}

function onPointerMove(e) {
  if (gameState !== State.PLAYING) return;
  e.preventDefault();
  const p = getPointerPos(e);
  // Also track for mouse (mouseDown check) but allow hover-slice for touch
  if (e.touches) {
    // Touch: always slice on move
    handleSlice(lastMouseX, lastMouseY, p.x, p.y);
    lastMouseX = p.x;
    lastMouseY = p.y;
    addTrail(p.x, p.y);
  } else if (mouseDown) {
    handleSlice(lastMouseX, lastMouseY, p.x, p.y);
    lastMouseX = p.x;
    lastMouseY = p.y;
    addTrail(p.x, p.y);
  }
}

function onPointerUp(e) {
  mouseDown = false;
  finishSwipeCombo();
}

canvas.addEventListener('mousedown', onPointerDown);
canvas.addEventListener('mousemove', onPointerMove);
canvas.addEventListener('mouseup', onPointerUp);
canvas.addEventListener('mouseleave', onPointerUp);
canvas.addEventListener('touchstart', onPointerDown, { passive: false });
canvas.addEventListener('touchmove', onPointerMove, { passive: false });
canvas.addEventListener('touchend', onPointerUp, { passive: false });

function addTrail(x, y) {
  trailPoints.push({ x, y, life: 0.4 });
  if (trailPoints.length > MAX_TRAIL) trailPoints.shift();
}

/* ===== SLICE LOGIC ===== */
function handleSlice(x1, y1, x2, y2) {
  const now = performance.now();
  if (now - lastSwipeTime > 500) {
    slicesThisSwipe = [];
  }
  lastSwipeTime = now;

  const angle = Math.atan2(y2 - y1, x2 - x1);
  for (let i = objects.length - 1; i >= 0; i--) {
    const obj = objects[i];
    if (!obj.alive || obj.sliced) continue;
    if (obj.hitTest(x1, y1, x2, y2)) {
      sliceObject(obj, angle, x2, y2);
      slicesThisSwipe.push(obj);
    }
  }
}

function finishSwipeCombo() {
  // If multiple fruits sliced rapidly, give combo bonus
  if (slicesThisSwipe.length >= 2) {
    comboCount = slicesThisSwipe.length;
    showCombo(comboCount);
  }
  slicesThisSwipe = [];
}

function sliceObject(obj, angle, px, py) {
  if (obj.isBomb) {
    // Bomb sliced! Lose life
    obj.sliced = true;
    obj.alive = false;
    // Big explosion
    for (let i = 0; i < 30; i++) {
      const ang = rand(0, Math.PI*2);
      const sp = rand(200, 600);
      particles.push(new SparkParticle(obj.x, obj.y, '#ff6b3b',
        Math.cos(ang) * sp, Math.sin(ang) * sp, rand(0.5, 1.0)));
    }
    for (let i = 0; i < 15; i++) {
      const ang = rand(0, Math.PI*2);
      const sp = rand(100, 300);
      particles.push(new SparkParticle(obj.x, obj.y, '#ffd93d',
        Math.cos(ang) * sp, Math.sin(ang) * sp, rand(0.4, 0.8)));
    }
    shakeAmount = 25;
    loseLife();
    scorePopups.push(new ScorePopup(obj.x, obj.y - 20, '💥 BOOM!', '#ff3b3b', true));
    return;
  }

  if (obj.isFruit) {
    obj.sliced = true;
    setTimeout(() => { if (obj._remove) obj.alive = false; }, 100);
    obj._remove = true;
    // Juice particles
    const color = obj.type.rCol2;
    for (let i = 0; i < 12; i++) {
      const ang = angle + rand(-1, 1);
      const sp = rand(80, 350);
      particles.push(new JuiceParticle(obj.x, obj.y, color,
        Math.cos(ang) * sp, Math.sin(ang) * sp - 60, rand(3, 8), rand(0.5, 1.2)));
    }
    // Score bump
    let pts = 10;
    if (superTimer > 0) pts *= 2;
    score += pts;
    comboHits.push({ time: performance.now(), pts });
    scorePopups.push(new ScorePopup(obj.x, obj.y, '+' + pts,
      superTimer > 0 ? '#ffd93d' : '#fff5e6', false));
    updateHUD();
    return;
  }

  // Specials
  if (obj.isSpecial) {
    obj.alive = false;
    if (obj.type.name === 'freeze') { freezeTimer = 4.0; }
    else if (obj.type.name === 'super') { superTimer = 6.0; }
    else if (obj.type.name === 'rain') { rainTimer = 5.0; }
    // Spark particles
    for (let i = 0; i < 25; i++) {
      const ang = rand(0, Math.PI*2);
      const sp = rand(100, 400);
      particles.push(new SparkParticle(obj.x, obj.y, obj.type.glow,
        Math.cos(ang) * sp, Math.sin(ang) * sp, rand(0.5, 1.0)));
    }
    scorePopups.push(new ScorePopup(obj.x, obj.y, obj.type.name.toUpperCase() + '!',
      obj.type.glow, true));
    updatePowerBadges();
    return;
  }
}

/* ===== COMBO ===== */
function showCombo(n) {
  const el = document.getElementById('comboPopup');
  let label = n + 'x COMBO!';
  let color = '#ffd93d';
  if (n >= 4) { label = '🔥 ' + n + 'x BLADE MASTER!'; color = '#ff5e3a'; }
  else if (n >= 3) { label = n + 'x COMBO!'; color = '#ff9f43'; }
  el.textContent = label;
  el.style.color = color;
  el.style.left = (W/2 - 80) + 'px';
  el.style.top = '40%';
  el.style.transform = 'scale(1.3)';
  el.style.opacity = '1';
  // bonus
  const bonus = n * 5;
  score += bonus;
  updateHUD();
  scorePopups.push(new ScorePopup(W/2, H * 0.45, '+' + bonus + ' BONUS', color, true));
  setTimeout(() => {
    el.style.opacity = '0';
    el.style.transform = 'scale(1)';
  }, 800);
}

/* ===== LIFE ===== */
function loseLife() {
  lives--;
  shakeAmount = 20;
  updateHUD();
  if (lives <= 0) {
    gameOver();
  }
}

/* ===== HUD ===== */
function updateHUD() {
  document.getElementById('scoreEl').textContent = score;
  for (let i = 1; i <= 3; i++) {
    const icon = document.querySelector(`.life-icon[data-life="${i}"]`);
    if (i <= lives) icon.classList.remove('lost');
    else icon.classList.add('lost');
  }
  if (score > best) {
    best = score;
    localStorage.setItem('fruitNinjaBest', best);
    document.getElementById('bestEl').textContent = best;
  }
}

function updatePowerBadges() {
  document.getElementById('pb-freeze').classList.toggle('active', freezeTimer > 0);
  document.getElementById('pb-super').classList.toggle('active', superTimer > 0);
  document.getElementById('pb-rain').classList.toggle('active', rainTimer > 0);
}

/* ===== SPAWNING ===== */
let bombChance = 0.10; // 10% chance per spawn
let specialChance = 0.06;

function spawnWave(dt) {
  spawnTimer += dt * 1000;
  let interval = spawnInterval / difficulty;
  if (rainTimer > 0) interval *= 0.4; // fruit rain spawns faster
  if (spawnTimer >= interval) {
    spawnTimer = 0;
    const batch = rainTimer > 0 ? randInt(2, 4) : (Math.random() < 0.25 ? 2 : 1);
    for (let i = 0; i < batch; i++) spawnObject();
  }
}

function spawnObject() {
  const roll = Math.random();
  if (roll < bombChance * difficulty * 0.5) {
    // Bomb
    objects.push(new GameObject(BOMB, false, false, true));
  } else if (roll < bombChance + specialChance) {
    // Special
    const st = choose(SPECIAL_TYPES);
    objects.push(new GameObject(st, false, true, false));
  } else {
    // Regular fruit
    const ft = choose(FRUIT_TYPES);
    objects.push(new GameObject(ft, true, false, false));
  }
}

/* ===== GAME LOOP ===== */
let lastT = 0;
function loop(ts) {
  if (!lastT) lastT = ts;
  const dt = Math.min(0.05, (ts - lastT) / 1000);
  lastT = ts;
  timeAccumulator += dt;

  // Clear
  ctx.fillStyle = '#1a0a14';
  ctx.fillRect(0, 0, W, H);

  // Dojo bg pattern
  drawBackground();

  if (gameState === State.PLAYING) {
    // Decrease power-up timers
    if (freezeTimer > 0) freezeTimer -= dt;
    if (superTimer > 0) superTimer -= dt;
    if (rainTimer > 0) rainTimer -= dt;
    if (freezeTimer < 0) freezeTimer = 0;
    if (superTimer < 0) superTimer = 0;
    if (rainTimer < 0) rainTimer = 0;
    updatePowerBadges();

    // Difficulty ramps with time
    difficulty = 1 + timeAccumulator / 80;

    // Update combo timer
    if (performance.now() - lastSwipeTime > 1500) {
      // reset combo
    }

    // Spawn
    spawnWave(dt);

    // Update objects
    for (let i = objects.length - 1; i >= 0; i--) {
      objects[i].update(dt, freezeTimer > 0);
      if (!objects[i].alive) objects.splice(i, 1);
    }

    // Update particles
    for (let i = particles.length - 1; i >= 0; i--) {
      particles[i].update(dt, freezeTimer > 0);
      if (!particles[i].alive) particles.splice(i, 1);
    }

    // Update score popups
    for (let i = scorePopups.length - 1; i >= 0; i--) {
      scorePopups[i].update(dt);
      if (!scorePopups[i].alive) scorePopups.splice(i, 1);
    }

    // Update trail
    for (let i = trailPoints.length - 1; i >= 0; i--) {
      trailPoints[i].life -= dt;
      if (trailPoints[i].life <= 0) trailPoints.splice(i, 1);
    }
  }

  // Shake offset
  let sx = 0, sy = 0;
  if (shakeAmount > 0) {
    sx = rand(-shakeAmount, shakeAmount) * 0.5;
    sy = rand(-shakeAmount, shakeAmount) * 0.5;
    shakeAmount *= 0.88;
    if (shakeAmount < 0.3) shakeAmount = 0;
  }

  ctx.save();
  ctx.translate(sx, sy);

  // Draw objects
  for (const obj of objects) obj.draw(ctx);
  // Draw particles
  for (const p of particles) p.draw(ctx);
  // Score popups
  for (const p of scorePopups) p.draw(ctx);
  // Slice trail
  if (gameState === State.PLAYING) drawTrail();

  ctx.restore();

  // Freeze overlay
  if (freezeTimer > 0) {
    ctx.fillStyle = `rgba(79,195,247,${0.08 * Math.min(1, freezeTimer)})`;
    ctx.fillRect(0, 0, W, H);
  }

  requestAnimationFrame(loop);
}

/* ===== BACKGROUND ===== */
function drawBackground() {
  // Vertical gradient
  const g = ctx.createLinearGradient(0, 0, 0, H);
  g.addColorStop(0, '#231019');
  g.addColorStop(0.5, '#1e0d14');
  g.addColorStop(1, '#15080f');
  ctx.fillStyle = g;
  ctx.fillRect(0, 0, W, H);

  // Subtle radial glow
  const rg = ctx.createRadialGradient(W * 0.5, H * 0.4, 80, W * 0.5, H * 0.4, Math.max(W, H) * 0.7);
  rg.addColorStop(0, 'rgba(255,94,58,0.04)');
  rg.addColorStop(1, 'rgba(255,94,58,0)');
  ctx.fillStyle = rg;
  ctx.fillRect(0, 0, W, H);

  // Decorative concentric arcs
  ctx.strokeStyle = 'rgba(255,94,58,0.03)';
  ctx.lineWidth = 1;
  for (let i = 0; i < 6; i++) {
    ctx.beginPath();
    ctx.arc(W * 0.5, H * 0.9, 120 + i * 90, Math.PI, 0);
    ctx.stroke();
  }

  // Lantern particles (floating background dots)
  for (let i = 0; i < 25; i++) {
    const x = (i * 137.5) % W;
    const y = (i * 89.3 + timeAccumulator * 15) % (H + 40) - 20;
    const a = 0.04 + Math.sin(timeAccumulator + i) * 0.02;
    ctx.fillStyle = `rgba(255,159,67,${a})`;
    ctx.beginPath();
    ctx.arc(x, y, 1.5, 0, Math.PI * 2);
    ctx.fill();
  }
}

/* ===== SLICE TRAIL ===== */
function drawTrail() {
  if (trailPoints.length < 2) return;
  ctx.save();
  // Outer glow trail
  ctx.strokeStyle = 'rgba(255,255,255,0.25)';
  ctx.lineWidth = 20;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  ctx.beginPath();
  ctx.moveTo(trailPoints[0].x, trailPoints[0].y);
  for (let i = 1; i < trailPoints.length; i++) ctx.lineTo(trailPoints[i].x, trailPoints[i].y);
  ctx.stroke();
  // Core white trail
  ctx.strokeStyle = '#fff';
  ctx.lineWidth = 5;
  ctx.shadowColor = '#ff5e3a';
  ctx.shadowBlur = 16;
  ctx.beginPath();
  ctx.moveTo(trailPoints[0].x, trailPoints[0].y);
  for (let i = 1; i < trailPoints.length; i++) ctx.lineTo(trailPoints[i].x, trailPoints[i].y);
  ctx.stroke();
  ctx.restore();
}

/* ===== SCREEN MANAGEMENT ===== */
function startGame() {
  gameState = State.PLAYING;
  score = 0;
  lives = 3;
  difficulty = 1;
  spawnTimer = 0;
  timeAccumulator = 0;
  objects = [];
  particles.length = 0;
  scorePopups.length = 0;
  trailPoints.length = 0;
  freezeTimer = 0;
  superTimer = 0;
  rainTimer = 0;
  comboCount = 0;
  slicesThisSwipe = [];
  shakeAmount = 0;
  document.getElementById('screen').classList.add('hidden');
  updateHUD();
  updatePowerBadges();
}

function gameOver() {
  gameState = State.OVER;
  if (score > best) {
    best = score;
    localStorage.setItem('fruitNinjaBest', best);
  }
  // Show game over screen
  setTimeout(() => {
    const screen = document.getElementById('screen');
    screen.classList.remove('hidden');
    document.getElementById('screenTitle').innerHTML =
      'Game Over<span class="sub">You scored ' + score + '</span>';
    document.getElementById('screenTagline').style.display = 'none';
    document.getElementById('featRow').style.display = 'none';
    document.getElementById('howto').style.display = 'none';
    document.getElementById('startBtn').textContent = 'Play Again';
    // Add score display
    let scoreDisplay = document.getElementById('scoreDisplay');
    if (!scoreDisplay) {
      scoreDisplay = document.createElement('div');
      scoreDisplay.id = 'scoreDisplay';
      scoreDisplay.className = 'score-row';
      screen.insertBefore(scoreDisplay, document.getElementById('startBtn'));
    }
    scoreDisplay.innerHTML = `
      <div class="stat"><div class="num">${score}</div><div class="lbl">Score</div></div>
      <div class="stat"><div class="num">${best}</div><div class="lbl">Best</div></div>
    `;
  }, 600);
}

document.getElementById('startBtn').addEventListener('click', () => {
  document.getElementById('screenTagline').style.display = '';
  document.getElementById('featRow').style.display = '';
  document.getElementById('howto').style.display = '';
  const sd = document.getElementById('scoreDisplay');
  if (sd) sd.remove();
  startGame();
});

// Start the loop
requestAnimationFrame(loop);
</script>
</body>
</html>