گمشده در امواج
by RapidKnight54795 lines20.9 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: #000814;
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(0, 150, 255, 0.4);
}
canvas { display: block; width: 100%; height: 100%; }
/* ==== نوار بالا ==== */
#top-bar {
position: absolute;
top: 12px; left: 12px; right: 12px;
display: flex;
justify-content: space-between;
align-items: flex-start;
pointer-events: none;
direction: ltr;
}
.panel {
background: rgba(0, 20, 45, 0.9);
border: 1px solid rgba(0, 180, 255, 0.5);
border-radius: 12px;
padding: 10px 18px;
backdrop-filter: blur(8px);
color: #9df;
font-size: 14px;
line-height: 1.9;
box-shadow: 0 0 25px rgba(0, 150, 255, 0.25);
}
.panel b { color: #0ff; font-size: 16px; }
/* ==== نوار هدف (بالای صفحه) ==== */
#goal-bar {
position: absolute;
top: 90px;
left: 50%;
transform: translateX(-50%);
width: 500px;
pointer-events: none;
text-align: center;
}
#goal-label {
color: #ffd966;
font-size: 13px;
margin-bottom: 6px;
text-shadow: 0 0 10px rgba(255, 200, 0, 0.6);
}
#goal-track {
position: relative;
height: 14px;
background: rgba(0, 20, 40, 0.85);
border: 1px solid rgba(0, 180, 255, 0.4);
border-radius: 8px;
overflow: hidden;
}
#goal-fill {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #06f, #0ff, #0f8);
transition: width 0.15s;
box-shadow: 0 0 20px #0ff inset;
}
#goal-marker {
position: absolute;
top: -4px;
right: 6px;
font-size: 18px;
}
/* ==== راهنمای زنده ==== */
#live-hint {
position: absolute;
top: 145px;
left: 50%;
transform: translateX(-50%);
color: #fff;
font-size: 15px;
background: rgba(0, 30, 60, 0.85);
border: 1px solid rgba(0, 200, 255, 0.5);
border-radius: 20px;
padding: 8px 22px;
pointer-events: none;
text-align: center;
box-shadow: 0 0 25px rgba(0, 200, 255, 0.3);
transition: all 0.25s;
min-width: 280px;
}
/* ==== کنترلها ==== */
#controls {
position: absolute;
bottom: 14px;
left: 14px; right: 14px;
display: flex;
gap: 12px;
justify-content: center;
direction: ltr;
}
.ctrl {
background: rgba(0, 20, 45, 0.92);
border: 1px solid rgba(0, 180, 255, 0.5);
border-radius: 12px;
padding: 10px 16px;
color: #9df;
font-size: 12px;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 170px;
box-shadow: 0 0 20px rgba(0, 150, 255, 0.2);
}
.ctrl label { color: #6cf; font-size: 12px; font-weight: bold; }
.ctrl .sub { color: #4a7; font-size: 10px; }
input[type=range] {
width: 100%;
accent-color: #0af;
cursor: pointer;
height: 22px;
}
.val { color: #0ff; font-weight: bold; font-size: 14px; }
/* ==== دکمههای جهتی (بزرگ و واضح) ==== */
#direction-hint {
display: flex;
gap: 6px;
justify-content: center;
margin-top: 4px;
}
.dir-arrow {
font-size: 22px;
color: #2a4a6a;
transition: all 0.15s;
padding: 2px 8px;
}
.dir-arrow.active {
color: #0ff;
text-shadow: 0 0 15px #0ff;
transform: scale(1.3);
}
/* ==== Overlay شروع ==== */
#overlay {
position: absolute;
inset: 0;
background: rgba(0, 8, 20, 0.96);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 16px;
color: #fff;
text-align: center;
z-index: 10;
padding: 30px;
}
#overlay h1 {
font-size: 34px;
color: #0ff;
text-shadow: 0 0 30px #0af, 0 0 60px #06f;
letter-spacing: 2px;
}
#overlay .story {
font-size: 15px;
color: #9df;
max-width: 540px;
line-height: 2.2;
}
#overlay .story b { color: #ffd966; }
#overlay .how-to {
background: rgba(0, 30, 60, 0.7);
border: 1px solid rgba(0, 180, 255, 0.4);
border-radius: 12px;
padding: 16px 24px;
max-width: 540px;
text-align: right;
color: #cfe;
font-size: 14px;
line-height: 2.2;
}
#overlay .how-to h3 { color: #0ff; margin-bottom: 8px; text-align: center; }
#overlay button {
background: linear-gradient(135deg, #06f, #0af);
border: none;
color: #fff;
padding: 16px 50px;
font-size: 20px;
border-radius: 12px;
cursor: pointer;
font-family: Tahoma;
font-weight: bold;
box-shadow: 0 0 35px rgba(0, 170, 255, 0.7);
transition: transform 0.15s;
}
#overlay button:hover { transform: scale(1.06); }
/* ==== پیام شناور ==== */
#toast {
position: absolute;
top: 220px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: rgba(0, 80, 140, 0.95);
border: 2px solid #0ff;
border-radius: 14px;
padding: 14px 32px;
color: #fff;
font-size: 18px;
font-weight: bold;
box-shadow: 0 0 50px rgba(0, 200, 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="top-bar">
<div class="panel">
<b>مرحله:</b> <span id="level">1</span><br>
<b>امتیاز:</b> <span id="score">0</span>
</div>
<div class="panel" style="text-align:right;">
<b>سرعت قایق:</b> <span id="speed-display">0</span>
</div>
</div>
<div id="goal-bar">
<div id="goal-label">🏝️ فاصله تا ساحل امن</div>
<div id="goal-track">
<div id="goal-fill"></div>
<div id="goal-marker">🏝️</div>
</div>
</div>
<div id="live-hint">اسلایدرها را تکان بده تا قایق حرکت کند</div>
<div id="controls">
<div class="ctrl">
<label>🌊 موج بالا-پایین</label>
<input type="range" id="f1" min="0" max="100" value="50">
<div class="sub">حرکت عمودی قایق</div>
</div>
<div class="ctrl">
<label>💨 موج چپ-راست</label>
<input type="range" id="f2" min="0" max="100" value="50">
<div class="sub">حرکت افقی قایق</div>
<div id="direction-hint">
<span class="dir-arrow" id="arrow-left">◀</span>
<span class="dir-arrow" id="arrow-right">▶</span>
</div>
</div>
<div class="ctrl">
<label>🎯 قدرت موج</label>
<input type="range" id="d" min="0" max="100" value="50">
<div class="sub">سرعت حرکت</div>
</div>
</div>
<div id="toast"></div>
<div id="overlay">
<h1>⛵ گمشده در امواج</h1>
<p class="story">
تو <b>خدای دریاها</b> هستی. یک قایق کوچک در طوفان گیر افتاده.<br>
با کنترل <b>امواج</b>، آب را طوری به حرکت دربیار که قایق به <b>ساحل امن</b> برسه.
</p>
<div class="how-to">
<h3>🎮 چطور بازی کنم؟</h3>
<b>۱.</b> سه اسلایدر پایین صفحه هست<br>
<b>۲.</b> اسلایدر اول: قایق را بالا/پایین میبرد<br>
<b>۳.</b> اسلایدر دوم: قایق را چپ/راست میبرد<br>
<b>۴.</b> اسلایدر سوم: قدرت موج را بیشتر میکند<br>
<b>۵.</b> قایق را به 🏝️ ساحل برسان تا برنده شوی!
</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;
// ============ وضعیت ============
let gameState = 'menu';
let level = 1;
let score = 0;
let time = 0;
let lastTime = 0;
let animationId = null;
// کنترلها (0..1)
let ctrlVertical = 0.5;
let ctrlHorizontal = 0.5;
let ctrlPower = 0.5;
// قایق
let boatX = 150;
let boatY = 300;
let boatVX = 0;
let boatVY = 0;
// ساحل
const SHORE_X = 800;
// افکتها
let particles = [];
let shakeAmount = 0;
let flashAmount = 0;
let trailPoints = [];
// ============ DOM ============
const f1S = document.getElementById('f1');
const f2S = document.getElementById('f2');
const dS = document.getElementById('d');
const levelEl = document.getElementById('level');
const scoreEl = document.getElementById('score');
const speedEl = document.getElementById('speed-display');
const goalFill = document.getElementById('goal-fill');
const hintEl = document.getElementById('live-hint');
const overlay = document.getElementById('overlay');
const toast = document.getElementById('toast');
const arrowL = document.getElementById('arrow-left');
const arrowR = document.getElementById('arrow-right');
f1S.addEventListener('input', () => { ctrlVertical = f1S.value / 100; });
f2S.addEventListener('input', () => { ctrlHorizontal = f2S.value / 100; });
dS.addEventListener('input', () => { ctrlPower = dS.value / 100; });
// ============ توابع کمکی ============
function showToast(msg, color = '#0ff') {
toast.textContent = msg;
toast.style.borderColor = color;
toast.style.boxShadow = `0 0 50px ${color}cc`;
toast.classList.add('show');
clearTimeout(toast._t);
toast._t = setTimeout(() => toast.classList.remove('show'), 1800);
}
// ============ فیزیک ساده و واضح ============
function updateBoat(dt) {
// نیروی عمودی: از ctrlVertical
const forceY = (ctrlVertical - 0.5) * 2; // -1..1
// نیروی افقی: از ctrlHorizontal
const forceX = (ctrlHorizontal - 0.5) * 2; // -1..1
// قدرت موج: سرعت واکنش
const power = 0.3 + ctrlPower * 1.5;
// شتاب
boatVY += forceY * power * dt * 8;
boatVX += forceX * power * dt * 8;
// اصطکاک آب
boatVY *= 0.94;
boatVX *= 0.94;
// محدودیت سرعت
const maxSpeed = 4 + ctrlPower * 4;
boatVX = Math.max(-maxSpeed, Math.min(maxSpeed, boatVX));
boatVY = Math.max(-maxSpeed, Math.min(maxSpeed, boatVY));
// حرکت
boatX += boatVX;
boatY += boatVY;
// برخورد با لبهها
if (boatY < 80) { boatY = 80; boatVY *= -0.3; }
if (boatY > H - 80) { boatY = H - 80; boatVY *= -0.3; }
if (boatX < 60) { boatX = 60; boatVX *= -0.3; }
if (boatX > W - 60) { boatX = W - 60; boatVX *= -0.3; }
// دنباله
if (Math.abs(boatVX) + Math.abs(boatVY) > 0.5) {
trailPoints.push({ x: boatX, y: boatY, life: 1 });
if (trailPoints.length > 40) trailPoints.shift();
}
trailPoints.forEach(p => p.life -= dt * 0.8);
trailPoints = trailPoints.filter(p => p.life > 0);
// ذرات پاشش آب
if (Math.abs(boatVX) + Math.abs(boatVY) > 2 && Math.random() < 0.4) {
particles.push({
x: boatX, y: boatY + 10,
vx: -boatVX * 0.3 + (Math.random() - 0.5) * 2,
vy: -Math.random() * 2 - 1,
size: 2 + Math.random() * 2,
life: 1
});
}
// بهروزرسانی نمایش سرعت
const speed = Math.hypot(boatVX, boatVY);
speedEl.textContent = speed.toFixed(1);
// جهت افقی (فلشها)
if (boatVX > 0.3) { arrowR.classList.add('active'); arrowL.classList.remove('active'); }
else if (boatVX < -0.3) { arrowL.classList.add('active'); arrowR.classList.remove('active'); }
else { arrowL.classList.remove('active'); arrowR.classList.remove('active'); }
// راهنمای زنده
updateLiveHint();
// پیشرفت به ساحل
const progress = Math.max(0, Math.min(1, (boatX - 60) / (SHORE_X - 60)));
goalFill.style.width = (progress * 100) + '%';
// برنده شدن
if (boatX >= SHORE_X - 30) {
winLevel();
}
// باخت: بیرون رفتن از محدوده
if (boatY > H + 50 || boatY < -50 || boatX < -50) {
loseLevel();
}
}
function updateLiveHint() {
if (boatX >= SHORE_X - 100) {
hintEl.textContent = '🏝️ نزدیک ساحل! ادامه بده!';
hintEl.style.borderColor = '#0f8';
hintEl.style.boxShadow = '0 0 30px rgba(0,255,150,0.6)';
return;
}
const sp = Math.hypot(boatVX, boatVY);
if (sp < 0.3) {
hintEl.textContent = '👆 اسلایدرها را تکان بده تا قایق حرکت کند';
hintEl.style.borderColor = 'rgba(0,200,255,0.5)';
hintEl.style.boxShadow = '0 0 25px rgba(0,200,255,0.3)';
} else if (boatVX > 1.5) {
hintEl.textContent = '✅ آفرین! قایق به سمت ساحل میرود';
hintEl.style.borderColor = '#0f8';
hintEl.style.boxShadow = '0 0 30px rgba(0,255,150,0.5)';
} else if (boatVX < -0.5) {
hintEl.textContent = '⚠️ قایق دارد عقب میرود!';
hintEl.style.borderColor = '#f80';
hintEl.style.boxShadow = '0 0 30px rgba(255,150,0,0.5)';
} else {
hintEl.textContent = '🌊 قایق در حال حرکت است...';
hintEl.style.borderColor = 'rgba(0,200,255,0.5)';
hintEl.style.boxShadow = '0 0 25px rgba(0,200,255,0.3)';
}
}
// ============ رندر ============
function draw(t) {
// پسزمینه دریا
const bg = ctx.createLinearGradient(0, 0, W, H);
bg.addColorStop(0, '#001428');
bg.addColorStop(0.5, '#002a4a');
bg.addColorStop(1, '#00406a');
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;
}
// ==== خطوط موج دریا (پسزمینه) ====
drawSeaWaves(t);
// ==== ساحل ====
drawShore(t);
// ==== دنباله قایق ====
trailPoints.forEach((p, i) => {
const alpha = p.life * 0.5;
ctx.fillStyle = `rgba(100, 200, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(p.x, p.y, 3 + (1 - p.life) * 3, 0, Math.PI * 2);
ctx.fill();
});
// ==== ذرات ====
particles = particles.filter(p => p.life > 0);
particles.forEach(p => {
p.x += p.vx;
p.y += p.vy;
p.vy += 0.08;
p.life -= 0.02;
ctx.fillStyle = `rgba(150, 220, 255, ${p.life})`;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
});
// ==== قایق ====
drawBoat(t);
// ==== فلش راهنما بالای قایق ====
drawBoatArrow();
// ==== فلاش ====
if (flashAmount > 0) {
ctx.fillStyle = `rgba(0, 255, 200, ${flashAmount * 0.4})`;
ctx.fillRect(0, 0, W, H);
flashAmount *= 0.9;
}
ctx.restore();
}
function drawSeaWaves(t) {
// موجهای افقی متحرک برای حس دریا
ctx.strokeStyle = 'rgba(100, 200, 255, 0.08)';
ctx.lineWidth = 1;
for (let i = 0; i < 15; i++) {
const y = 60 + i * 40;
ctx.beginPath();
for (let x = 0; x < W; x += 8) {
const wave = Math.sin(x * 0.02 + t * 1.5 + i * 0.5) * 6;
if (x === 0) ctx.moveTo(x, y + wave);
else ctx.lineTo(x, y + wave);
}
ctx.stroke();
}
// موجهای عمودی از چشمهها
const srcs = getSources();
srcs.forEach((s, idx) => {
const color = idx === 0 ? '80,200,255' : '120,180,255';
for (let r = 20; r < 400; r += 40) {
const radius = (r + t * 60) % 400;
const alpha = Math.max(0, 0.15 - radius / 2000);
if (alpha <= 0) continue;
ctx.strokeStyle = `rgba(${color}, ${alpha})`;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(s.x, s.y, radius, 0, Math.PI * 2);
ctx.stroke();
}
});
}
function getSources() {
// دو چشمه موج در سمت چپ صفحه
return [
{ x: 40, y: 150 },
{ x: 40, y: 450 }
];
}
function drawShore(t) {
// ساحل در سمت راست
const grad = ctx.createLinearGradient(SHORE_X - 40, 0, W, 0);
grad.addColorStop(0, 'rgba(255, 220, 120, 0)');
grad.addColorStop(0.3, 'rgba(255, 220, 120, 0.15)');
grad.addColorStop(1, 'rgba(255, 220, 120, 0.4)');
ctx.fillStyle = grad;
ctx.fillRect(SHORE_X - 40, 0, W - SHORE_X + 40, H);
// نخلهای کوچک
ctx.fillStyle = 'rgba(80, 180, 80, 0.6)';
for (let i = 0; i < 5; i++) {
const px = SHORE_X + 20 + i * 18;
const py = 100 + (i % 3) * 180;
ctx.beginPath();
ctx.arc(px, py, 8, 0, Math.PI * 2);
ctx.fill();
}
// برچسب ساحل
ctx.fillStyle = 'rgba(255, 220, 120, 0.9)';
ctx.font = 'bold 22px Tahoma';
ctx.textAlign = 'center';
ctx.fillText('🏝️', SHORE_X + 40, H / 2);
ctx.font = 'bold 16px Tahoma';
ctx.fillText('ساحل امن', SHORE_X + 40, H / 2 + 30);
}
function drawBoat(t) {
ctx.save();
ctx.translate(boatX, boatY);
// چرخش قایق بر اساس سرعت عمودی
const tilt = Math.max(-0.4, Math.min(0.4, boatVY * 0.08));
ctx.rotate(tilt);
// سایه زیر قایق
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.beginPath();
ctx.ellipse(0, 18, 32, 8, 0, 0, Math.PI * 2);
ctx.fill();
// بدنه قایق
ctx.fillStyle = '#c84a1a';
ctx.beginPath();
ctx.moveTo(-30, -8);
ctx.lineTo(30, -8);
ctx.lineTo(22, 14);
ctx.lineTo(-22, 14);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
// خط بدنه
ctx.fillStyle = '#fff';
ctx.fillRect(-28, -2, 56, 3);
// بادبان
ctx.fillStyle = '#fff';
ctx.beginPath();
ctx.moveTo(0, -35);
ctx.lineTo(0, -8);
ctx.lineTo(22, -8);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = '#0af';
ctx.lineWidth = 1.5;
ctx.stroke();
// دکل
ctx.strokeStyle = '#333';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, -38);
ctx.lineTo(0, -8);
ctx.stroke();
// پرچم کوچک
const flagWave = Math.sin(t * 5) * 2;
ctx.fillStyle = '#0ff';
ctx.beginPath();
ctx.moveTo(0, -38);
ctx.lineTo(10 + flagWave, -35);
ctx.lineTo(0, -32);
ctx.closePath();
ctx.fill();
ctx.restore();
}
function drawBoatArrow() {
// فلش بزرگ بالای قایق که جهت حرکت را نشان میدهد
const speed = Math.hypot(boatVX, boatVY);
if (speed < 0.2) return;
const arrowLen = 15 + speed * 6;
const angle = Math.atan2(boatVY, boatVX);
ctx.save();
ctx.translate(boatX + Math.cos(angle) * 50, boatY + Math.sin(angle) * 50);
ctx.rotate(angle);
ctx.fillStyle = `rgba(0, 255, 200, ${0.4 + speed * 0.1})`;
ctx.beginPath();
ctx.moveTo(arrowLen, 0);
ctx.lineTo(-arrowLen * 0.5, -arrowLen * 0.5);
ctx.lineTo(-arrowLen * 0.5, arrowLen * 0.5);
ctx.closePath();
ctx.fill();
ctx.restore();
}
// ============ حلقه بازی ============
function gameLoop(ts) {
if (gameState !== 'playing') return;
const dt = Math.min((ts - lastTime) / 1000, 0.05);
lastTime = ts;
time += dt;
updateBoat(dt);
flashAmount *= 0.9;
shakeAmount *= 0.9;
draw(time);
animationId = requestAnimationFrame(gameLoop);
}
// ============ برد و باخت ============
function winLevel() {
gameState = 'won';
cancelAnimationFrame(animationId);
score += 100 * level;
scoreEl.textContent = score;
flashAmount = 1;
shakeAmount = 15;
if (level < 3) {
showToast(`🎉 مرحله ${level} کامل شد!`, '#0f8');
level++;
levelEl.textContent = level;
setTimeout(() => {
resetLevel();
gameState = 'playing';
lastTime = performance.now();
animationId = requestAnimationFrame(gameLoop);
}, 1500);
} else {
showToast('🏆 برنده شدی!', '#ffd966');
setTimeout(showWinScreen, 1200);
}
}
function loseLevel() {
gameState = 'lost';
cancelAnimationFrame(animationId);
shakeAmount = 25;
showToast('💥 قایق غرق شد!', '#f44');
setTimeout(() => {
resetLevel();
gameState = 'playing';
lastTime = performance.now();
animationId = requestAnimationFrame(gameLoop);
}, 1500);
}
function resetLevel() {
boatX = 150;
boatY = 300;
boatVX = 0;
boatVY = 0;
trailPoints = [];
particles = [];
goalFill.style.width = '0%';
}
function showWinScreen() {
overlay.innerHTML = `
<h1>🏆 برنده شدی!</h1>
<p class="story">
تو موفق شدی قایق را از میان طوفان به <b>ساحل امن</b> برسانی.<br>
امتیاز نهایی: <b>${score}</b><br>
خدای دریاها ازت راضی است. 🌊
</p>
<button onclick="location.reload()">بازی دوباره</button>
`;
overlay.style.display = 'flex';
}
function startGame() {
overlay.style.display = 'none';
gameState = 'playing';
level = 1;
score = 0;
time = 0;
levelEl.textContent = '1';
scoreEl.textContent = '0';
resetLevel();
// شروع اسلایدرها در وسط
f1S.value = 50;
f2S.value = 50;
dS.value = 50;
ctrlVertical = 0.5;
ctrlHorizontal = 0.5;
ctrlPower = 0.5;
showToast('⛵ قایق را به ساحل برسان!', '#0ff');
lastTime = performance.now();
animationId = requestAnimationFrame(gameLoop);
}
// ============ رندر اولیه ============
draw(0);
</script>
</body>
</html>Game Source: گمشده در امواج
Creator: RapidKnight54
Libraries: matter
Complexity: complex (795 lines, 20.9 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-mv6ygoh2" to link back to the original. Then publish at arcadelab.ai/publish.