🎮ArcadeLab

آهنگ کهکشان

by RapidKnight54
683 lines18.7 KB🛠️ matter.js (physics engine)
▶ Play
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>آهنگ کهکشان 🎵</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    background: #000308;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    font-family: Tahoma, sans-serif;
    overflow: hidden;
  }
  #game-container {
    position: relative;
    width: 900px;
    height: 600px;
    border-radius: 14px;
    overflow: hidden;
    box-shadow: 0 0 80px rgba(120, 80, 255, 0.5);
  }
  canvas { display: block; width: 100%; height: 100%; }

  #hud {
    position: absolute;
    top: 12px; left: 12px; right: 12px;
    display: flex;
    justify-content: space-between;
    pointer-events: none;
    direction: ltr;
  }
  .panel {
    background: rgba(10, 5, 30, 0.9);
    border: 1px solid rgba(150, 100, 255, 0.5);
    border-radius: 10px;
    padding: 10px 16px;
    backdrop-filter: blur(8px);
    color: #c8b0ff;
    font-size: 13px;
    line-height: 1.9;
    box-shadow: 0 0 20px rgba(120, 80, 255, 0.3);
  }
  .panel b { color: #d8b8ff; }

  #notes-collected {
    position: absolute;
    top: 100px;
    left: 12px;
    display: flex;
    gap: 6px;
    pointer-events: none;
  }
  .note-slot {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    background: rgba(30, 15, 60, 0.8);
    border: 1px solid rgba(150, 100, 255, 0.4);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    color: #444;
    transition: all 0.3s;
  }
  .note-slot.filled {
    background: linear-gradient(135deg, #60f, #b0f);
    color: #fff;
    box-shadow: 0 0 20px rgba(180, 100, 255, 0.7);
    animation: pop 0.4s;
  }
  @keyframes pop {
    0% { transform: scale(0.5); }
    50% { transform: scale(1.3); }
    100% { transform: scale(1); }
  }

  #controls {
    position: absolute;
    bottom: 12px;
    left: 12px; right: 12px;
    display: flex;
    gap: 10px;
    justify-content: center;
    direction: ltr;
  }
  .ctrl-group {
    background: rgba(10, 5, 30, 0.92);
    border: 1px solid rgba(150, 100, 255, 0.5);
    border-radius: 10px;
    padding: 8px 14px;
    color: #c8b0ff;
    font-size: 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 150px;
  }
  .ctrl-group label { color: #a88fff; font-size: 11px; }
  input[type=range] { width: 100%; accent-color: #a0f; cursor: pointer; }
  .val { color: #d8b8ff; font-weight: bold; }

  #overlay {
    position: absolute;
    inset: 0;
    background: rgba(0, 3, 8, 0.96);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 18px;
    color: #fff;
    text-align: center;
    z-index: 10;
    padding: 30px;
  }
  #overlay h1 {
    font-size: 32px;
    color: #d8b8ff;
    text-shadow: 0 0 30px #a0f, 0 0 60px #60f;
    letter-spacing: 2px;
  }
  #overlay p {
    font-size: 15px;
    color: #c8b0ff;
    max-width: 580px;
    line-height: 2.1;
  }
  #overlay b { color: #e8c8ff; }
  #overlay button {
    background: linear-gradient(135deg, #60f, #b0f);
    border: none;
    color: #fff;
    padding: 14px 42px;
    font-size: 18px;
    border-radius: 10px;
    cursor: pointer;
    font-family: Tahoma;
    font-weight: bold;
    box-shadow: 0 0 30px rgba(160, 100, 255, 0.6);
    transition: transform 0.15s;
  }
  #overlay button:hover { transform: scale(1.06); }
  #overlay .hint-text {
    color: #8070a0;
    font-size: 13px;
    margin-top: 8px;
  }
  #overlay .melody {
    display: flex;
    gap: 8px;
    margin: 10px 0;
  }
  #overlay .melody span {
    font-size: 28px;
    opacity: 0.3;
  }
  #overlay .melody span.on { opacity: 1; text-shadow: 0 0 20px #a0f; }

  #toast {
    position: absolute;
    top: 180px;
    left: 50%;
    transform: translateX(-50%) translateY(-20px);
    background: rgba(80, 30, 160, 0.95);
    border: 1px solid rgba(200, 150, 255, 0.8);
    border-radius: 12px;
    padding: 12px 28px;
    color: #fff;
    font-size: 16px;
    font-weight: bold;
    box-shadow: 0 0 40px rgba(160, 100, 255, 0.8);
    opacity: 0;
    transition: all 0.4s;
    pointer-events: none;
    z-index: 5;
  }
  #toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
</style>
</head>
<body>
<div id="game-container">
  <canvas id="c"></canvas>

  <div id="hud">
    <div class="panel">
      <b>نت‌ها:</b> <span id="score">0</span>/5<br>
      <b>مرحله:</b> <span id="level">1</span>
    </div>
    <div class="panel" style="text-align:right;">
      <b>هماهنگی:</b> <span id="tune">0</span>%<br>
      <b>زمان دروازه:</b> <span id="gate-timer">--</span>s
    </div>
  </div>

  <div id="notes-collected">
    <div class="note-slot" id="n1">♪</div>
    <div class="note-slot" id="n2">♫</div>
    <div class="note-slot" id="n3">♪</div>
    <div class="note-slot" id="n4">♫</div>
    <div class="note-slot" id="n5">♪</div>
  </div>

  <div id="controls">
    <div class="ctrl-group">
      <label>فرکانس چشمه آبی: <span class="val" id="f1v">3.0</span></label>
      <input type="range" id="f1" min="1.5" max="6" step="0.1" value="3">
    </div>
    <div class="ctrl-group">
      <label>فرکانس چشمه بنفش: <span class="val" id="f2v">3.0</span></label>
      <input type="range" id="f2" min="1.5" max="6" step="0.1" value="3">
    </div>
    <div class="ctrl-group">
      <label>فاصله چشمه‌ها: <span class="val" id="dv">80</span></label>
      <input type="range" id="d" min="20" max="200" step="5" value="80">
    </div>
  </div>

  <div id="toast"></div>

  <div id="overlay">
    <h1>🎵 آهنگ کهکشان</h1>
    <p>
      تو یک <b>نوازنده‌ی فضایی</b> در ایستگاه «اکو» هستی.<br>
      ستاره‌های دوردست با <b>تداخل موج‌های صوتی</b> با هم حرف می‌زنن.<br>
      هر بار یک <b>دروازه‌ی ستاره‌ای</b> باز می‌شه و باید با تنظیم فرکانس دو چشمه،
      یک <b>نت کهکشانی</b> ازش بیرون بکشی.<br>
      <b>۵ نت</b> = راز سیاره‌ی گمشده کشف می‌شه!<br>
      <span class="hint-text">دروازه‌ها فقط چند ثانیه بازن — سریع باش!</span>
    </p>
    <div class="melody">
      <span>♪</span><span>♫</span><span>♪</span><span>♫</span><span>♪</span>
    </div>
    <button onclick="startGame()">شروع سفر</button>
  </div>
</div>

<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const W = 900, H = 600;
canvas.width = W; canvas.height = H;

// ============ تنظیمات ============
const SOURCE_X = 140;
const SOURCE_CENTER_Y = 300;
const BASE_K = 0.09;

// ============ وضعیت ============
let gameState = 'menu';
let score = 0;
let level = 1;
let freq1 = 3.0, freq2 = 3.0, sourceDistance = 80;
let animationId = null;
let time = 0;
let lastTime = 0;

// دروازه ستاره‌ای
let gateActive = false;
let gateX = 600, gateY = 300;
let gateRadius = 40;
let gateTimeLeft = 0;
let gateMaxTime = 8;
let gateProgress = 0; // 0..1 چقدر توی دروازه امتیاز گرفتیم
let gateCharge = 0;   // 0..1 شارژ نهایی

// افکت‌ها
let hitFlash = 0;
let shakeAmount = 0;
let particles = [];
let toastTimer = 0;

// نت‌ها
const noteSymbols = ['♪', '♫', '♪', '♫', '♪'];

// ============ DOM ============
const f1S = document.getElementById('f1');
const f2S = document.getElementById('f2');
const dS = document.getElementById('d');
const f1v = document.getElementById('f1v');
const f2v = document.getElementById('f2v');
const dv = document.getElementById('dv');
const scoreEl = document.getElementById('score');
const levelEl = document.getElementById('level');
const tuneEl = document.getElementById('tune');
const gateTimerEl = document.getElementById('gate-timer');
const overlay = document.getElementById('overlay');
const toast = document.getElementById('toast');

f1S.addEventListener('input', () => { freq1 = +f1S.value; f1v.textContent = freq1.toFixed(1); });
f2S.addEventListener('input', () => { freq2 = +f2S.value; f2v.textContent = freq2.toFixed(1); });
dS.addEventListener('input', () => { sourceDistance = +dS.value; dv.textContent = sourceDistance; });

// ============ فیزیک ============
function getSources() {
  const half = sourceDistance / 2;
  return [
    { x: SOURCE_X, y: SOURCE_CENTER_Y - half },
    { x: SOURCE_X, y: SOURCE_CENTER_Y + half }
  ];
}

function waveAt(px, py, t) {
  const srcs = getSources();
  const k1 = freq1 * BASE_K;
  const k2 = freq2 * BASE_K;
  const om1 = freq1 * 2.0;
  const om2 = freq2 * 2.0;
  const d1 = Math.hypot(px - srcs[0].x, py - srcs[0].y);
  const d2 = Math.hypot(px - srcs[1].x, py - srcs[1].y);
  return Math.sin(k1 * d1 - om1 * t) + Math.sin(k2 * d2 - om2 * t);
}

// هماهنگی موج روی دروازه (0..1)
function tuneOnGate(t) {
  if (!gateActive) return 0;
  const srcs = getSources();
  const k1 = freq1 * BASE_K;
  const k2 = freq2 * BASE_K;
  const d1 = Math.hypot(gateX - srcs[0].x, gateY - srcs[0].y);
  const d2 = Math.hypot(gateX - srcs[1].x, gateY - srcs[1].y);
  // اختلاف فاز: اگر مضرب 2π باشد، تداخل سازنده
  const phaseDiff = k1 * d1 - k2 * d2;
  const mod = ((phaseDiff % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
  const dist = Math.min(mod, Math.PI * 2 - mod); // 0..π
  const tune = 1 - (dist / Math.PI); // 1 = کامل هماهنگ، 0 = ناهماهنگ
  return tune;
}

// ============ رندر ============
function draw(t) {
  // پس‌زمینه
  const bg = ctx.createRadialGradient(W/2, H/2, 100, W/2, H/2, 700);
  bg.addColorStop(0, '#0a0520');
  bg.addColorStop(1, '#000308');
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, W, H);

  ctx.save();
  if (shakeAmount > 0.1) {
    ctx.translate((Math.random() - 0.5) * shakeAmount, (Math.random() - 0.5) * shakeAmount);
    shakeAmount *= 0.9;
  }

  // ستاره‌های پس‌زمینه
  ctx.fillStyle = 'rgba(255,255,255,0.4)';
  for (let i = 0; i < 60; i++) {
    const sx = ((i * 137.5) % W);
    const sy = ((i * 71.3) % H);
    const tw = 0.5 + 0.5 * Math.sin(t * 2 + i);
    ctx.globalAlpha = tw * 0.5;
    ctx.fillRect(sx, sy, 1.5, 1.5);
  }
  ctx.globalAlpha = 1;

  // الگوی موج (خطوط سازنده)
  const step = 6;
  for (let x = 0; x < W; x += step) {
    for (let y = 0; y < H; y += step) {
      const v = waveAt(x, y, t);
      const b = Math.abs(v) / 2;
      if (b > 0.75) {
        const alpha = Math.min(1, (b - 0.75) * 4);
        ctx.fillStyle = `rgba(140, 200, 255, ${alpha * 0.55})`;
        ctx.fillRect(x, y, step, step);
      } else if (b > 0.45) {
        ctx.fillStyle = `rgba(80, 40, 160, ${(b - 0.45) * 0.7})`;
        ctx.fillRect(x, y, step, step);
      }
    }
  }

  // چشمه‌ها
  const srcs = getSources();
  srcs.forEach((s, i) => {
    const pulse = Math.sin(t * 4 + i * Math.PI) * 5 + 16;
    const grad = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, pulse + 25);
    grad.addColorStop(0, i === 0 ? 'rgba(80,180,255,0.8)' : 'rgba(180,80,255,0.8)');
    grad.addColorStop(1, 'transparent');
    ctx.fillStyle = grad;
    ctx.beginPath();
    ctx.arc(s.x, s.y, pulse + 25, 0, Math.PI * 2);
    ctx.fill();

    ctx.fillStyle = i === 0 ? '#5af' : '#b5f';
    ctx.beginPath();
    ctx.arc(s.x, s.y, 8, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#fff';
    ctx.lineWidth = 2;
    ctx.stroke();

    // برچسب
    ctx.fillStyle = 'rgba(255,255,255,0.5)';
    ctx.font = '10px Tahoma';
    ctx.textAlign = 'center';
    ctx.fillText(i === 0 ? 'آبی' : 'بنفش', s.x, s.y - 20);
  });

  // خط چشمه‌ها
  ctx.setLineDash([4, 6]);
  ctx.strokeStyle = 'rgba(255,255,255,0.15)';
  ctx.beginPath();
  ctx.moveTo(srcs[0].x, srcs[0].y);
  ctx.lineTo(srcs[1].x, srcs[1].y);
  ctx.stroke();
  ctx.setLineDash([]);

  // دروازه ستاره‌ای
  if (gateActive && gameState === 'playing') {
    drawGate(t);
  }

  // ذرات
  particles = particles.filter(p => p.life > 0);
  particles.forEach(p => {
    p.x += p.vx;
    p.y += p.vy;
    p.vy += 0.1;
    p.life -= 0.018;
    ctx.fillStyle = `rgba(${p.r},${p.g},${p.b},${p.life})`;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
    ctx.fill();
  });

  // فلاش
  if (hitFlash > 0) {
    ctx.fillStyle = `rgba(200, 150, 255, ${hitFlash * 0.5})`;
    ctx.fillRect(0, 0, W, H);
    hitFlash *= 0.9;
  }

  ctx.restore();
}

function drawGate(t) {
  const tune = tuneOnGate(t);
  const pulse = Math.sin(t * 3) * 3;
  const r = gateRadius + pulse;

  // هاله بیرونی
  const grad = ctx.createRadialGradient(gateX, gateY, r * 0.3, gateX, gateY, r * 2);
  const hue = 260 + tune * 40;
  grad.addColorStop(0, `hsla(${hue}, 100%, 70%, ${0.4 + tune * 0.5})`);
  grad.addColorStop(1, 'transparent');
  ctx.fillStyle = grad;
  ctx.beginPath();
  ctx.arc(gateX, gateY, r * 2, 0, Math.PI * 2);
  ctx.fill();

  // حلقه چرخان
  ctx.save();
  ctx.translate(gateX, gateY);
  ctx.rotate(t * 1.5);
  for (let i = 0; i < 6; i++) {
    ctx.rotate(Math.PI / 3);
    ctx.strokeStyle = `hsla(${hue}, 100%, 80%, ${0.3 + tune * 0.6})`;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(0, 0, r, 0, Math.PI / 4);
    ctx.stroke();
  }
  ctx.restore();

  // هسته
  ctx.fillStyle = `hsla(${hue}, 100%, ${60 + tune * 30}%, ${0.5 + tune * 0.5})`;
  ctx.beginPath();
  ctx.arc(gateX, gateY, r * 0.5, 0, Math.PI * 2);
  ctx.fill();

  // نماد نت داخل دروازه
  ctx.fillStyle = '#fff';
  ctx.font = 'bold 24px Tahoma';
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.globalAlpha = 0.5 + tune * 0.5;
  ctx.fillText('♪', gateX, gateY);
  ctx.globalAlpha = 1;
  ctx.textBaseline = 'alphabetic';

  // نوار پیشرفت شارژ
  if (gateCharge > 0.02) {
    ctx.strokeStyle = `hsla(${hue}, 100%, 80%, 0.9)`;
    ctx.lineWidth = 4;
    ctx.beginPath();
    ctx.arc(gateX, gateY, r + 12, -Math.PI/2, -Math.PI/2 + Math.PI * 2 * gateCharge);
    ctx.stroke();
  }

  // خط اتصال چشمه‌ها به دروازه
  const srcs = getSources();
  srcs.forEach((s, i) => {
    ctx.setLineDash([3, 8]);
    ctx.strokeStyle = `rgba(${i === 0 ? '80,180,255' : '180,80,255'}, ${0.1 + tune * 0.3})`;
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(s.x, s.y);
    ctx.lineTo(gateX, gateY);
    ctx.stroke();
    ctx.setLineDash([]);
  });
}

// ============ حلقه بازی ============
function gameLoop(ts) {
  if (gameState !== 'playing') return;
  const dt = Math.min((ts - lastTime) / 1000, 0.05);
  lastTime = ts;
  time += dt;

  if (gateActive) {
    gateTimeLeft -= dt;
    gateTimerEl.textContent = Math.max(0, gateTimeLeft).toFixed(1);

    const tune = tuneOnGate(time);

    // اگر تیون بالای 0.85 باشه، شارژ می‌شه
    if (tune > 0.82) {
      gateCharge += dt * 0.6 * (0.5 + tune);
      // ذرات جذب
      if (Math.random() < 0.4) {
        const ang = Math.random() * Math.PI * 2;
        const dist = 80 + Math.random() * 40;
        particles.push({
          x: gateX + Math.cos(ang) * dist,
          y: gateY + Math.sin(ang) * dist,
          vx: -Math.cos(ang) * 2,
          vy: -Math.sin(ang) * 2,
          size: 2 + Math.random() * 2,
          r: 200, g: 150, b: 255,
          life: 1
        });
      }
    } else {
      gateCharge -= dt * 0.3;
      if (gateCharge < 0) gateCharge = 0;
    }

    // تکمیل نت
    if (gateCharge >= 1) {
      collectNote();
    }

    // اتمام زمان
    if (gateTimeLeft <= 0) {
      gateActive = false;
      gateCharge = 0;
      showToast('⏱ دروازه بسته شد!');
      setTimeout(spawnGate, 1200);
    }

    tuneEl.textContent = Math.round(tune * 100);
  }

  // فلاش‌ها
  hitFlash *= 0.9;
  shakeAmount *= 0.9;

  draw(time);
  animationId = requestAnimationFrame(gameLoop);
}

// ============ توابع گیم‌پلی ============
function spawnGate() {
  if (gameState !== 'playing') return;
  if (score >= 5) return;

  // موقعیت تصادفی در نیمه راست
  gateX = 500 + Math.random() * 300;
  gateY = 120 + Math.random() * (H - 240);
  gateRadius = 35 + Math.random() * 15;
  gateMaxTime = Math.max(4, 8 - level * 0.7);
  gateTimeLeft = gateMaxTime;
  gateCharge = 0;
  gateActive = true;
}

function collectNote() {
  gateActive = false;
  gateCharge = 0;
  score++;
  scoreEl.textContent = score;

  // پر کردن اسلات نت
  const slot = document.getElementById('n' + score);
  if (slot) {
    slot.classList.add('filled');
    slot.textContent = noteSymbols[score - 1];
  }

  hitFlash = 1;
  shakeAmount = 20;

  // انفجار ذرات
  for (let i = 0; i < 40; i++) {
    const ang = Math.random() * Math.PI * 2;
    const sp = 2 + Math.random() * 6;
    particles.push({
      x: gateX, y: gateY,
      vx: Math.cos(ang) * sp,
      vy: Math.sin(ang) * sp,
      size: 2 + Math.random() * 3,
      r: 180, g: 120, b: 255,
      life: 1
    });
  }

  showToast(`✨ نت ${score} از 5 کشف شد!`);

  if (score >= 5) {
    setTimeout(winGame, 800);
  } else {
    level++;
    levelEl.textContent = level;
    setTimeout(spawnGate, 1500);
  }
}

function showToast(msg) {
  toast.textContent = msg;
  toast.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => toast.classList.remove('show'), 1800);
}

function winGame() {
  gameState = 'won';
  cancelAnimationFrame(animationId);
  overlay.innerHTML = `
    <h1>🌟 سیاره‌ی گمشده پیدا شد!</h1>
    <p>
      تو موفق شدی <b>۵ نت کهکشانی</b> رو کشف کنی.<br>
      ملودی کامل، راه رو به <b>سیاره‌ی گمشده</b> نشون داد.<br>
      حالا نسل‌های بعدی نوازندگان فضایی داستان تو رو تعریف می‌کنن. 🎵
    </p>
    <div class="melody">
      ${noteSymbols.map(n => `<span class="on">${n}</span>`).join('')}
    </div>
    <button onclick="location.reload()">سفر دوباره</button>
  `;
  overlay.style.display = 'flex';
}

function startGame() {
  overlay.style.display = 'none';
  gameState = 'playing';
  score = 0;
  level = 1;
  time = 0;
  gateActive = false;
  gateCharge = 0;
  particles = [];
  scoreEl.textContent = '0';
  levelEl.textContent = '1';
  gateTimerEl.textContent = '--';
  document.querySelectorAll('.note-slot').forEach(s => {
    s.classList.remove('filled');
    s.textContent = noteSymbols[parseInt(s.id.slice(1)) - 1];
  });
  document.getElementById('n1').textContent = '♪';
  document.getElementById('n2').textContent = '♫';
  document.getElementById('n3').textContent = '♪';
  document.getElementById('n4').textContent = '♫';
  document.getElementById('n5').textContent = '♪';

  lastTime = performance.now();
  setTimeout(spawnGate, 800);
  animationId = requestAnimationFrame(gameLoop);
}

// ============ رندر اولیه ============
draw(0);
</script>
</body>
</html>

Game Source: آهنگ کهکشان

Creator: RapidKnight54

Libraries: matter

Complexity: complex (683 lines, 18.7 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: game-rapidknight54" to link back to the original. Then publish at arcadelab.ai/publish.