آهنگ کهکشان
by RapidKnight54683 lines18.7 KB🛠️ matter.js (physics engine)
<!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.