Initial AI-generated site
This commit is contained in:
+188
@@ -0,0 +1,188 @@
|
||||
<!doctype html>
|
||||
<html lang="he" dir="rtl">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>משחק מירוץ סירות בנמל | משחק דפדפן חינמי</title>
|
||||
<meta name="description" content="משחק מירוץ סירות מרהיב בדפדפן! אספו מתנות, קבלו בוסטים והגיעו הכי רחוק שאפשר. חינם, בלי התקנה.">
|
||||
<meta property="og:title" content="משחק מירוץ סירות בנמל">
|
||||
<meta property="og:description" content="מירוץ סירות עם מתנות ובוסטים!">
|
||||
<meta property="og:type" content="website">
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⛵</text></svg>">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Heebo:wght@400;700;900&family=Rubik+Mono+One&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">דלג לתוכן הראשי</a>
|
||||
<div class="scanlines" aria-hidden="true"></div>
|
||||
|
||||
<header class="site-header">
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<span class="logo-icon" aria-hidden="true">⛵</span>
|
||||
<span class="logo-text">מירוץ סירות</span>
|
||||
</div>
|
||||
<nav class="main-nav" aria-label="ניווט ראשי">
|
||||
<button class="nav-btn" data-action="instructions">הוראות</button>
|
||||
<button class="nav-btn" data-action="highscores">שיאים</button>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main id="main-content">
|
||||
<section class="hero" aria-labelledby="hero-title">
|
||||
<div class="hero-content">
|
||||
<h1 id="hero-title" class="hero-title">מירוץ סירות בנמל</h1>
|
||||
<p class="hero-subtitle">אספו מתנות · קבלו בוסטים · שברו שיאים!</p>
|
||||
<button class="start-btn pixel-btn" id="startGame">
|
||||
<span>התחל משחק</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="hero-decoration" aria-hidden="true">
|
||||
<div class="harbor-elements">
|
||||
<div class="lighthouse"></div>
|
||||
<div class="buoy buoy-1"></div>
|
||||
<div class="buoy buoy-2"></div>
|
||||
</div>
|
||||
<div class="wave wave-1"></div>
|
||||
<div class="wave wave-2"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="game-container" id="gameContainer" aria-label="אזור המשחק">
|
||||
<div class="game-hud">
|
||||
<div class="hud-item score">
|
||||
<span class="hud-label">ניקוד</span>
|
||||
<span class="hud-value" id="scoreDisplay">0</span>
|
||||
</div>
|
||||
<div class="hud-item distance">
|
||||
<span class="hud-label">מרחק</span>
|
||||
<span class="hud-value" id="distanceDisplay">0 מ׳</span>
|
||||
</div>
|
||||
<div class="hud-item boost">
|
||||
<span class="hud-label" id="boostLabel">בוסט</span>
|
||||
<div class="boost-bar" role="progressbar" aria-labelledby="boostLabel" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||
<div class="boost-fill" id="boostBar"></div>
|
||||
<span class="boost-value" id="boostValue">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hud-item gifts">
|
||||
<span class="hud-label">מתנות</span>
|
||||
<span class="hud-value" id="giftsDisplay">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sr-only" role="status" aria-live="polite" aria-atomic="true" id="gameStatus"></div>
|
||||
|
||||
<canvas id="gameCanvas" aria-label="משחק מירוץ סירות פעיל"></canvas>
|
||||
|
||||
<div class="game-overlay" id="gameOverlay" role="dialog" aria-labelledby="overlayTitle" aria-modal="true">
|
||||
<div class="overlay-content">
|
||||
<h2 id="overlayTitle">מוכנים למירוץ?</h2>
|
||||
<p id="overlayMessage">לחצו על ״התחל״ או על רווח</p>
|
||||
<div class="final-stats" id="finalStats" hidden>
|
||||
<div class="stat-row">
|
||||
<span>ניקוד סופי:</span>
|
||||
<span id="finalScore">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>מתנות שנאספו:</span>
|
||||
<span id="finalGifts">0</span>
|
||||
</div>
|
||||
<div class="stat-row">
|
||||
<span>מרחק:</span>
|
||||
<span id="finalDistance">0 מ׳</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="pixel-btn" id="overlayBtn">התחל</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="features" aria-labelledby="features-title">
|
||||
<h2 id="features-title" class="section-title">איך משחקים?</h2>
|
||||
<div class="features-grid">
|
||||
<article class="feature-card">
|
||||
<div class="feature-icon" aria-hidden="true">????</div>
|
||||
<h3>שליטה פשוטה</h3>
|
||||
<p>חיצים למעלה ולמטה להזזת הסירה, או החלקה במובייל</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<div class="feature-icon" aria-hidden="true">????</div>
|
||||
<h3>אספו מתנות</h3>
|
||||
<p>מתנות צפות על המים — אספו לניקוד בונוס ולמילוי מד הבוסט</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<div class="feature-icon" aria-hidden="true">⚡</div>
|
||||
<h3>בוסט מהירות</h3>
|
||||
<p>כשהמד מלא, לחצו רווח או הקשה כפולה לזינוק קדימה!</p>
|
||||
</article>
|
||||
<article class="feature-card">
|
||||
<div class="feature-icon" aria-hidden="true">????</div>
|
||||
<h3>שברו שיאים</h3>
|
||||
<p>התחרו בעצמכם ושפרו את השיא האישי שוב ושוב</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="highscores-section" id="highscoresSection" aria-labelledby="highscores-title">
|
||||
<h2 id="highscores-title" class="section-title">טבלת שיאים</h2>
|
||||
<div class="highscores-table" role="table" aria-label="טבלת שיאים">
|
||||
<div class="table-header" role="row">
|
||||
<span role="columnheader">מקום</span>
|
||||
<span role="columnheader">ניקוד</span>
|
||||
<span role="columnheader">מתנות</span>
|
||||
</div>
|
||||
<div class="table-body" id="highscoresBody" role="rowgroup">
|
||||
<div class="table-row table-row--empty" role="row">
|
||||
<span role="cell">עדיין אין שיאים — היו הראשונים!</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-content">
|
||||
<div class="footer-logo" aria-hidden="true">⛵ מירוץ סירות בנמל</div>
|
||||
<p class="footer-credits">נבנה באהבה לסגנון הרטרו</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<div class="instructions-modal" id="instructionsModal" role="dialog" aria-labelledby="instructions-title" aria-modal="true" hidden>
|
||||
<div class="modal-content">
|
||||
<button class="modal-close" id="closeInstructions" aria-label="סגור חלון הוראות">✕</button>
|
||||
<h2 id="instructions-title">הוראות המשחק</h2>
|
||||
<div class="instructions-list">
|
||||
<div class="instruction-item">
|
||||
<span class="key">↑ ↓</span>
|
||||
<span>הזזת הסירה למעלה ולמטה</span>
|
||||
</div>
|
||||
<div class="instruction-item">
|
||||
<span class="key">רווח</span>
|
||||
<span>הפעלת בוסט (כשהמד מלא)</span>
|
||||
</div>
|
||||
<div class="instruction-item">
|
||||
<span class="key" lang="en">P</span>
|
||||
<span>השהיית המשחק</span>
|
||||
</div>
|
||||
<div class="instruction-item">
|
||||
<span class="key">????</span>
|
||||
<span>במובייל: החלקה למעלה/למטה, הקשה כפולה לבוסט</span>
|
||||
</div>
|
||||
</div>
|
||||
<h3>טיפים</h3>
|
||||
<ul class="tips-list">
|
||||
<li>אספו מתנות למילוי מד הבוסט</li>
|
||||
<li>השתמשו בבוסט בזמן הנכון</li>
|
||||
<li>הימנעו מסלעים ומכשולים</li>
|
||||
<li>המהירות עולה עם הזמן!</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,740 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Game State
|
||||
const state = {
|
||||
isPlaying: false,
|
||||
isPaused: false,
|
||||
score: 0,
|
||||
distance: 0,
|
||||
gifts: 0,
|
||||
boost: 0,
|
||||
maxBoost: 100,
|
||||
isBoostActive: false,
|
||||
speed: 3,
|
||||
baseSpeed: 3,
|
||||
boostSpeed: 7,
|
||||
playerY: 0,
|
||||
playerVelocity: 0,
|
||||
obstacles: [],
|
||||
giftItems: [],
|
||||
particles: [],
|
||||
highscores: [],
|
||||
lastBoostAnnounce: false
|
||||
};
|
||||
|
||||
// DOM Elements
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const startBtn = document.getElementById('startGame');
|
||||
const overlayBtn = document.getElementById('overlayBtn');
|
||||
const gameOverlay = document.getElementById('gameOverlay');
|
||||
const overlayTitle = document.getElementById('overlayTitle');
|
||||
const overlayMessage = document.getElementById('overlayMessage');
|
||||
const finalStats = document.getElementById('finalStats');
|
||||
const scoreDisplay = document.getElementById('scoreDisplay');
|
||||
const distanceDisplay = document.getElementById('distanceDisplay');
|
||||
const giftsDisplay = document.getElementById('giftsDisplay');
|
||||
const boostBar = document.getElementById('boostBar');
|
||||
const boostValue = document.getElementById('boostValue');
|
||||
const boostBarContainer = boostBar.parentElement;
|
||||
const instructionsModal = document.getElementById('instructionsModal');
|
||||
const closeInstructions = document.getElementById('closeInstructions');
|
||||
const highscoresBody = document.getElementById('highscoresBody');
|
||||
const gameStatus = document.getElementById('gameStatus');
|
||||
|
||||
// Check for reduced motion preference
|
||||
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Focus trap for modal
|
||||
let focusableElements = [];
|
||||
let firstFocusable = null;
|
||||
let lastFocusable = null;
|
||||
|
||||
function setupFocusTrap(modal) {
|
||||
focusableElements = modal.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
firstFocusable = focusableElements[0];
|
||||
lastFocusable = focusableElements[focusableElements.length - 1];
|
||||
}
|
||||
|
||||
function handleFocusTrap(e) {
|
||||
if (e.key !== 'Tab') return;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === firstFocusable) {
|
||||
e.preventDefault();
|
||||
lastFocusable.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastFocusable) {
|
||||
e.preventDefault();
|
||||
firstFocusable.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Canvas setup
|
||||
function resizeCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
state.playerY = rect.height / 2;
|
||||
}
|
||||
|
||||
function getCanvasSize() {
|
||||
return {
|
||||
width: canvas.getBoundingClientRect().width,
|
||||
height: canvas.getBoundingClientRect().height
|
||||
};
|
||||
}
|
||||
|
||||
// Boat drawing
|
||||
const boatSprite = {
|
||||
width: 48,
|
||||
height: 32,
|
||||
draw(ctx, x, y, isBoost) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
// Boat body
|
||||
ctx.fillStyle = isBoost ? '#ff6b6b' : '#8b4513';
|
||||
ctx.fillRect(0, 8, 40, 16);
|
||||
ctx.fillRect(-4, 12, 8, 8);
|
||||
|
||||
// Boat front (pointed)
|
||||
ctx.fillStyle = isBoost ? '#ff6b6b' : '#a0522d';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(40, 8);
|
||||
ctx.lineTo(48, 16);
|
||||
ctx.lineTo(40, 24);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Sail
|
||||
ctx.fillStyle = isBoost ? '#ffd60a' : '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(20, 8);
|
||||
ctx.lineTo(20, -12);
|
||||
ctx.lineTo(35, 8);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Mast
|
||||
ctx.fillStyle = '#654321';
|
||||
ctx.fillRect(18, -12, 4, 20);
|
||||
|
||||
// Flag
|
||||
ctx.fillStyle = '#f72585';
|
||||
ctx.fillRect(18, -16, 8, 6);
|
||||
|
||||
// Wake effect when boosting
|
||||
if (isBoost && !prefersReducedMotion) {
|
||||
ctx.fillStyle = 'rgba(0, 245, 212, 0.6)';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const yOffset = Math.sin(Date.now() / 100 + i) * 2;
|
||||
ctx.fillRect(-8 - i * 8, 14 + yOffset, 4, 4);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
};
|
||||
|
||||
// Gift item
|
||||
function drawGift(ctx, x, y, type) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
const bounce = prefersReducedMotion ? 0 : Math.sin(Date.now() / 200 + x) * 3;
|
||||
ctx.translate(0, bounce);
|
||||
|
||||
if (type === 'boost') {
|
||||
// Lightning bolt
|
||||
ctx.fillStyle = '#ffd60a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -12);
|
||||
ctx.lineTo(8, -4);
|
||||
ctx.lineTo(2, -4);
|
||||
ctx.lineTo(8, 12);
|
||||
ctx.lineTo(-4, 0);
|
||||
ctx.lineTo(2, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
} else {
|
||||
// Gift box
|
||||
ctx.fillStyle = '#f72585';
|
||||
ctx.fillRect(-8, -4, 16, 12);
|
||||
ctx.fillStyle = '#ffd60a';
|
||||
ctx.fillRect(-2, -4, 4, 12);
|
||||
ctx.fillRect(-8, 0, 16, 4);
|
||||
// Bow
|
||||
ctx.fillRect(-6, -10, 4, 6);
|
||||
ctx.fillRect(2, -10, 4, 6);
|
||||
ctx.fillRect(-2, -8, 4, 4);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Obstacle (rock)
|
||||
function drawObstacle(ctx, x, y, size) {
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
|
||||
ctx.fillStyle = '#4a4a4a';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-size/2, size/2);
|
||||
ctx.lineTo(-size/3, -size/3);
|
||||
ctx.lineTo(0, -size/2);
|
||||
ctx.lineTo(size/3, -size/4);
|
||||
ctx.lineTo(size/2, size/2);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
// Highlights
|
||||
ctx.fillStyle = '#5a5a5a';
|
||||
ctx.fillRect(-size/4, -size/4, size/4, size/4);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Water background
|
||||
function drawWater(ctx, canvasSize) {
|
||||
const { width, height } = canvasSize;
|
||||
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, height);
|
||||
gradient.addColorStop(0, '#0d1b2a');
|
||||
gradient.addColorStop(0.5, '#1b263b');
|
||||
gradient.addColorStop(1, '#0d1b2a');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Animated waves (skip if reduced motion or paused)
|
||||
if (!prefersReducedMotion && state.isPlaying && !state.isPaused) {
|
||||
ctx.strokeStyle = 'rgba(0, 245, 212, 0.1)';
|
||||
ctx.lineWidth = 2;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
ctx.beginPath();
|
||||
for (let x = 0; x < width; x += 20) {
|
||||
const waveY = (height / 5) * (i + 1) + Math.sin((x + state.distance * 2 + i * 50) / 50) * 8;
|
||||
if (x === 0) {
|
||||
ctx.moveTo(x, waveY);
|
||||
} else {
|
||||
ctx.lineTo(x, waveY);
|
||||
}
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Particle system
|
||||
function createParticle(x, y, color) {
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
state.particles.push({
|
||||
x,
|
||||
y,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: (Math.random() - 0.5) * 4,
|
||||
life: 1,
|
||||
color,
|
||||
size: Math.random() * 4 + 2
|
||||
});
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
for (let i = state.particles.length - 1; i >= 0; i--) {
|
||||
const p = state.particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= 0.03;
|
||||
if (p.life <= 0) {
|
||||
state.particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawParticles(ctx) {
|
||||
state.particles.forEach(p => {
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.globalAlpha = p.life;
|
||||
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Spawn obstacles and gifts
|
||||
function spawnObstacle(canvasSize) {
|
||||
const { width, height } = canvasSize;
|
||||
const maxY = height - 40;
|
||||
const minY = 40;
|
||||
|
||||
state.obstacles.push({
|
||||
x: width + 50,
|
||||
y: Math.random() * (maxY - minY) + minY,
|
||||
size: Math.random() * 20 + 30
|
||||
});
|
||||
}
|
||||
|
||||
function spawnGift(canvasSize) {
|
||||
const { width, height } = canvasSize;
|
||||
const maxY = height - 30;
|
||||
const minY = 30;
|
||||
|
||||
state.giftItems.push({
|
||||
x: width + 30,
|
||||
y: Math.random() * (maxY - minY) + minY,
|
||||
type: Math.random() < 0.3 ? 'boost' : 'gift'
|
||||
});
|
||||
}
|
||||
|
||||
// Collision detection
|
||||
function checkCollision(ax, ay, aw, ah, bx, by, bw, bh) {
|
||||
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
||||
}
|
||||
|
||||
// Update game
|
||||
let lastSpawnTime = 0;
|
||||
let lastGiftTime = 0;
|
||||
|
||||
function update(timestamp) {
|
||||
if (!state.isPlaying || state.isPaused) return;
|
||||
|
||||
const canvasSize = getCanvasSize();
|
||||
const { height } = canvasSize;
|
||||
|
||||
// Increase difficulty over time
|
||||
state.speed = state.baseSpeed + state.distance / 1200;
|
||||
const currentSpeed = state.isBoostActive ? state.boostSpeed : state.speed;
|
||||
|
||||
// Update distance
|
||||
state.distance += currentSpeed * 0.5;
|
||||
|
||||
// Player physics
|
||||
state.playerY += state.playerVelocity;
|
||||
state.playerVelocity *= 0.9;
|
||||
state.playerY = Math.max(24, Math.min(height - 24, state.playerY));
|
||||
|
||||
// Spawn obstacles
|
||||
const spawnInterval = Math.max(800, 1500 - state.distance * 0.5);
|
||||
if (timestamp - lastSpawnTime > spawnInterval) {
|
||||
spawnObstacle(canvasSize);
|
||||
lastSpawnTime = timestamp;
|
||||
}
|
||||
|
||||
// Spawn gifts
|
||||
if (timestamp - lastGiftTime > 2200) {
|
||||
spawnGift(canvasSize);
|
||||
lastGiftTime = timestamp;
|
||||
}
|
||||
|
||||
// Update obstacles
|
||||
for (let i = state.obstacles.length - 1; i >= 0; i--) {
|
||||
state.obstacles[i].x -= currentSpeed;
|
||||
|
||||
// Collision with player
|
||||
if (checkCollision(
|
||||
60, state.playerY - 16, 36, 28,
|
||||
state.obstacles[i].x - state.obstacles[i].size/2,
|
||||
state.obstacles[i].y - state.obstacles[i].size/2,
|
||||
state.obstacles[i].size,
|
||||
state.obstacles[i].size
|
||||
)) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.obstacles[i].x < -50) {
|
||||
state.obstacles.splice(i, 1);
|
||||
state.score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Update gifts
|
||||
for (let i = state.giftItems.length - 1; i >= 0; i--) {
|
||||
state.giftItems[i].x -= currentSpeed;
|
||||
|
||||
// Collision with player
|
||||
if (checkCollision(
|
||||
60, state.playerY - 16, 36, 28,
|
||||
state.giftItems[i].x - 12,
|
||||
state.giftItems[i].y - 12,
|
||||
24, 24
|
||||
)) {
|
||||
// Collect gift
|
||||
const gift = state.giftItems[i];
|
||||
if (gift.type === 'boost') {
|
||||
state.boost = Math.min(state.boost + 35, state.maxBoost);
|
||||
state.score += 25;
|
||||
} else {
|
||||
state.gifts++;
|
||||
state.boost = Math.min(state.boost + 15, state.maxBoost);
|
||||
state.score += 50;
|
||||
}
|
||||
|
||||
// Particles
|
||||
for (let j = 0; j < 8; j++) {
|
||||
createParticle(gift.x, gift.y, gift.type === 'boost' ? '#ffd60a' : '#f72585');
|
||||
}
|
||||
|
||||
state.giftItems.splice(i, 1);
|
||||
updateHUD();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.giftItems[i].x < -30) {
|
||||
state.giftItems.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Boost drain
|
||||
if (state.isBoostActive) {
|
||||
state.boost -= 0.6;
|
||||
if (state.boost <= 0) {
|
||||
state.boost = 0;
|
||||
state.isBoostActive = false;
|
||||
}
|
||||
// Boost particles
|
||||
createParticle(60, state.playerY, 'rgba(0, 245, 212, 0.8)');
|
||||
}
|
||||
|
||||
// Announce boost ready (only once)
|
||||
const boostFull = state.boost >= state.maxBoost;
|
||||
if (boostFull && !state.lastBoostAnnounce) {
|
||||
gameStatus.textContent = 'בוסט מוכן! לחצו רווח להפעלה';
|
||||
state.lastBoostAnnounce = true;
|
||||
} else if (!boostFull && state.lastBoostAnnounce) {
|
||||
state.lastBoostAnnounce = false;
|
||||
gameStatus.textContent = '';
|
||||
}
|
||||
|
||||
updateParticles();
|
||||
updateHUD();
|
||||
}
|
||||
|
||||
function updateHUD() {
|
||||
scoreDisplay.textContent = Math.floor(state.score);
|
||||
distanceDisplay.textContent = Math.floor(state.distance) + ' מ׳';
|
||||
giftsDisplay.textContent = state.gifts;
|
||||
|
||||
const boostPercent = Math.floor(state.boost / state.maxBoost * 100);
|
||||
boostBar.style.width = boostPercent + '%';
|
||||
boostValue.textContent = boostPercent + '%';
|
||||
boostBarContainer.setAttribute('aria-valuenow', boostPercent);
|
||||
}
|
||||
|
||||
// Render game
|
||||
function render() {
|
||||
const canvasSize = getCanvasSize();
|
||||
const { width, height } = canvasSize;
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
drawWater(ctx, canvasSize);
|
||||
|
||||
// Draw obstacles
|
||||
state.obstacles.forEach(obs => {
|
||||
drawObstacle(ctx, obs.x, obs.y, obs.size);
|
||||
});
|
||||
|
||||
// Draw gifts
|
||||
state.giftItems.forEach(gift => {
|
||||
drawGift(ctx, gift.x, gift.y, gift.type);
|
||||
});
|
||||
|
||||
// Draw player
|
||||
boatSprite.draw(ctx, 60, state.playerY, state.isBoostActive);
|
||||
|
||||
// Draw particles
|
||||
drawParticles(ctx);
|
||||
|
||||
// Boost ready indicator
|
||||
if (state.boost >= state.maxBoost && state.isPlaying) {
|
||||
ctx.font = 'bold 14px Heebo, sans-serif';
|
||||
ctx.fillStyle = '#ffd60a';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText('⚡ בוסט מוכן! לחצו רווח ⚡', width / 2, 12);
|
||||
}
|
||||
}
|
||||
|
||||
// Game loop
|
||||
let animationId;
|
||||
function gameLoop(timestamp) {
|
||||
update(timestamp);
|
||||
render();
|
||||
if (state.isPlaying) {
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
// Start game
|
||||
function startGame() {
|
||||
const canvasSize = getCanvasSize();
|
||||
|
||||
state.isPlaying = true;
|
||||
state.isPaused = false;
|
||||
state.score = 0;
|
||||
state.distance = 0;
|
||||
state.gifts = 0;
|
||||
state.boost = 0;
|
||||
state.isBoostActive = false;
|
||||
state.speed = state.baseSpeed;
|
||||
state.playerY = canvasSize.height / 2;
|
||||
state.playerVelocity = 0;
|
||||
state.obstacles = [];
|
||||
state.giftItems = [];
|
||||
state.particles = [];
|
||||
state.lastBoostAnnounce = false;
|
||||
|
||||
gameOverlay.classList.add('hidden');
|
||||
gameStatus.textContent = 'המשחק התחיל!';
|
||||
updateHUD();
|
||||
|
||||
lastSpawnTime = performance.now();
|
||||
lastGiftTime = performance.now();
|
||||
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Game over
|
||||
function gameOver() {
|
||||
state.isPlaying = false;
|
||||
cancelAnimationFrame(animationId);
|
||||
|
||||
// Save highscore
|
||||
const entry = {
|
||||
score: Math.floor(state.score),
|
||||
gifts: state.gifts,
|
||||
distance: Math.floor(state.distance)
|
||||
};
|
||||
|
||||
state.highscores.push(entry);
|
||||
state.highscores.sort((a, b) => b.score - a.score);
|
||||
state.highscores = state.highscores.slice(0, 5);
|
||||
|
||||
try {
|
||||
localStorage.setItem('boatRaceHighscores', JSON.stringify(state.highscores));
|
||||
} catch (e) {
|
||||
// Storage might be unavailable
|
||||
}
|
||||
|
||||
// Show overlay
|
||||
overlayTitle.textContent = 'המשחק נגמר!';
|
||||
overlayMessage.textContent = 'כל הכבוד! הגעתם למרחק ' + Math.floor(state.distance) + ' מטר';
|
||||
finalStats.hidden = false;
|
||||
document.getElementById('finalScore').textContent = Math.floor(state.score);
|
||||
document.getElementById('finalGifts').textContent = state.gifts;
|
||||
document.getElementById('finalDistance').textContent = Math.floor(state.distance) + ' מ׳';
|
||||
overlayBtn.textContent = 'שחק שוב';
|
||||
gameOverlay.classList.remove('hidden');
|
||||
overlayBtn.focus();
|
||||
|
||||
gameStatus.textContent = 'המשחק נגמר. ניקוד סופי: ' + Math.floor(state.score);
|
||||
|
||||
renderHighscores();
|
||||
}
|
||||
|
||||
// Render highscores
|
||||
function renderHighscores() {
|
||||
if (state.highscores.length === 0) {
|
||||
highscoresBody.innerHTML = '<div class="table-row table-row--empty" role="row"><span role="cell">עדיין אין שיאים — היו הראשונים!</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
highscoresBody.innerHTML = state.highscores.map((entry, i) => `
|
||||
<div class="table-row" role="row">
|
||||
<span role="cell">#${i + 1}</span>
|
||||
<span role="cell">${entry.score}</span>
|
||||
<span role="cell">${entry.gifts}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Input handling
|
||||
function handleKeyDown(e) {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
if (!state.isPlaying) {
|
||||
startGame();
|
||||
} else if (state.boost >= state.maxBoost && !state.isBoostActive) {
|
||||
state.isBoostActive = true;
|
||||
gameStatus.textContent = 'בוסט פעיל!';
|
||||
}
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowUp' || e.code === 'KeyW') {
|
||||
e.preventDefault();
|
||||
state.playerVelocity = -7;
|
||||
}
|
||||
|
||||
if (e.code === 'ArrowDown' || e.code === 'KeyS') {
|
||||
e.preventDefault();
|
||||
state.playerVelocity = 7;
|
||||
}
|
||||
|
||||
if (e.code === 'KeyP' && state.isPlaying) {
|
||||
state.isPaused = !state.isPaused;
|
||||
gameStatus.textContent = state.isPaused ? 'המשחק מושהה' : 'המשחק ממשיך';
|
||||
if (!state.isPaused) {
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
if (e.code === 'Escape') {
|
||||
if (!instructionsModal.hidden) {
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Touch handling
|
||||
let touchStartY = 0;
|
||||
let lastTap = 0;
|
||||
|
||||
function handleTouchStart(e) {
|
||||
if (!state.isPlaying) {
|
||||
startGame();
|
||||
return;
|
||||
}
|
||||
|
||||
touchStartY = e.touches[0].clientY;
|
||||
|
||||
// Double tap for boost
|
||||
const now = Date.now();
|
||||
if (now - lastTap < 300 && state.boost >= state.maxBoost && !state.isBoostActive) {
|
||||
state.isBoostActive = true;
|
||||
gameStatus.textContent = 'בוסט פעיל!';
|
||||
}
|
||||
lastTap = now;
|
||||
}
|
||||
|
||||
function handleTouchMove(e) {
|
||||
if (!state.isPlaying) return;
|
||||
e.preventDefault();
|
||||
|
||||
const touchY = e.touches[0].clientY;
|
||||
const diff = touchStartY - touchY;
|
||||
state.playerVelocity = -diff * 0.12;
|
||||
touchStartY = touchY;
|
||||
}
|
||||
|
||||
// Modal handling
|
||||
let previousActiveElement = null;
|
||||
|
||||
function openModal() {
|
||||
previousActiveElement = document.activeElement;
|
||||
instructionsModal.hidden = false;
|
||||
setupFocusTrap(instructionsModal);
|
||||
closeInstructions.focus();
|
||||
document.body.style.overflow = 'hidden';
|
||||
instructionsModal.addEventListener('keydown', handleFocusTrap);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
instructionsModal.hidden = true;
|
||||
document.body.style.overflow = '';
|
||||
instructionsModal.removeEventListener('keydown', handleFocusTrap);
|
||||
if (previousActiveElement) {
|
||||
previousActiveElement.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
resizeCanvas();
|
||||
|
||||
// Load highscores
|
||||
try {
|
||||
const saved = localStorage.getItem('boatRaceHighscores');
|
||||
if (saved) {
|
||||
state.highscores = JSON.parse(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
// Storage might be unavailable
|
||||
}
|
||||
|
||||
renderHighscores();
|
||||
|
||||
// Event listeners
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
canvas.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
canvas.addEventListener('touchmove', handleTouchMove, { passive: false });
|
||||
|
||||
startBtn.addEventListener('click', startGame);
|
||||
overlayBtn.addEventListener('click', startGame);
|
||||
|
||||
// Navigation
|
||||
document.querySelectorAll('.nav-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'instructions') {
|
||||
openModal();
|
||||
} else if (action === 'highscores') {
|
||||
document.getElementById('highscoresSection').scrollIntoView({ behavior: prefersReducedMotion ? 'auto' : 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
closeInstructions.addEventListener('click', closeModal);
|
||||
|
||||
instructionsModal.addEventListener('click', (e) => {
|
||||
if (e.target === instructionsModal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Scroll reveal animations (skip if reduced motion)
|
||||
if (!prefersReducedMotion) {
|
||||
const observerOptions = {
|
||||
threshold: 0.1,
|
||||
rootMargin: '0px 0px -40px 0px'
|
||||
};
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('revealed');
|
||||
observer.unobserve(entry.target);
|
||||
}
|
||||
});
|
||||
}, observerOptions);
|
||||
|
||||
document.querySelectorAll('.feature-card, .section-title').forEach(el => {
|
||||
el.classList.add('reveal-on-scroll');
|
||||
observer.observe(el);
|
||||
});
|
||||
|
||||
// Add reveal styles dynamically
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.reveal-on-scroll {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
.reveal-on-scroll.revealed {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Initial render
|
||||
render();
|
||||
}
|
||||
|
||||
// Start
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,800 @@
|
||||
:root {
|
||||
--color-bg: #0a0a12;
|
||||
--color-water: #0d1b2a;
|
||||
--color-water-light: #1b263b;
|
||||
--color-primary: #00f5d4;
|
||||
--color-secondary: #7b2cbf;
|
||||
--color-accent: #f72585;
|
||||
--color-gold: #ffd60a;
|
||||
--color-text: #e8e8e8;
|
||||
--color-text-muted: #b8b8b8;
|
||||
--color-card-bg: #14142a;
|
||||
--font-display: 'Rubik Mono One', 'Heebo', sans-serif;
|
||||
--font-body: 'Heebo', sans-serif;
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 1rem;
|
||||
--space-md: 1.5rem;
|
||||
--space-lg: 3rem;
|
||||
--space-xl: 5rem;
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.7;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Skip link */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
z-index: 1000;
|
||||
transform: translateY(-100%);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Screen reader only */
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Scanlines - subtle CRT effect */
|
||||
.scanlines {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent 2px,
|
||||
rgba(0, 0, 0, 0.06) 2px,
|
||||
rgba(0, 0, 0, 0.06) 4px
|
||||
);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.site-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline: 0;
|
||||
z-index: 100;
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: linear-gradient(to bottom, var(--color-bg) 60%, transparent);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
color: var(--color-primary);
|
||||
text-shadow: 0 0 20px rgba(0, 245, 212, 0.5);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.main-nav {
|
||||
display: flex;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: transparent;
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
.nav-btn:focus-visible {
|
||||
outline: 3px solid var(--color-gold);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Hero */
|
||||
.hero {
|
||||
min-height: 100vh;
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
padding-block-start: calc(var(--space-xl) + 60px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.75rem, 6vw, 3rem);
|
||||
color: var(--color-primary);
|
||||
text-shadow:
|
||||
0 0 30px rgba(0, 245, 212, 0.6),
|
||||
0 0 60px rgba(0, 245, 212, 0.3);
|
||||
margin-block-end: var(--space-sm);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: clamp(1rem, 2.5vw, 1.25rem);
|
||||
color: var(--color-text-muted);
|
||||
margin-block-end: var(--space-lg);
|
||||
max-width: 400px;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.pixel-btn {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(0.875rem, 2vw, 1rem);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: linear-gradient(135deg, var(--color-secondary), var(--color-accent));
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
box-shadow:
|
||||
0 4px 0 #4a1a6b,
|
||||
0 8px 24px rgba(123, 44, 191, 0.4);
|
||||
}
|
||||
|
||||
.pixel-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 6px 0 #4a1a6b,
|
||||
0 12px 32px rgba(123, 44, 191, 0.5);
|
||||
}
|
||||
|
||||
.pixel-btn:focus-visible {
|
||||
outline: 3px solid var(--color-gold);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.pixel-btn:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow:
|
||||
0 2px 0 #4a1a6b,
|
||||
0 4px 12px rgba(123, 44, 191, 0.4);
|
||||
}
|
||||
|
||||
/* Hero decoration - harbor theme */
|
||||
.hero-decoration {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.harbor-elements {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
inset-inline: 0;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.lighthouse {
|
||||
position: absolute;
|
||||
inset-inline-end: 10%;
|
||||
bottom: 0;
|
||||
width: 24px;
|
||||
height: 80px;
|
||||
background: linear-gradient(to bottom, #fff 0%, #fff 10%, #e74c3c 10%, #e74c3c 30%, #fff 30%, #fff 50%, #e74c3c 50%, #e74c3c 70%, #fff 70%);
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.lighthouse::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -16px;
|
||||
inset-inline-start: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--color-gold);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 20px var(--color-gold), 0 0 40px var(--color-gold);
|
||||
animation: lighthouse-glow 2s ease-in-out infinite;
|
||||
/* RTL-safe centering */
|
||||
margin-inline-start: -8px;
|
||||
}
|
||||
|
||||
.buoy {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 12px;
|
||||
height: 24px;
|
||||
background: linear-gradient(to bottom, #e74c3c 50%, #fff 50%);
|
||||
border-radius: 6px 6px 2px 2px;
|
||||
}
|
||||
|
||||
.buoy-1 {
|
||||
inset-inline-start: 15%;
|
||||
animation: buoy-bob 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.buoy-2 {
|
||||
inset-inline-start: 35%;
|
||||
animation: buoy-bob 3s ease-in-out infinite 1s;
|
||||
}
|
||||
|
||||
@keyframes lighthouse-glow {
|
||||
0%, 100% { opacity: 0.6; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes buoy-bob {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-6px); }
|
||||
}
|
||||
|
||||
.wave {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
inset-inline-start: 0;
|
||||
width: 200%;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.wave-1 {
|
||||
background:
|
||||
radial-gradient(ellipse 60px 20px at 30px 100%, var(--color-water) 50%, transparent 50%),
|
||||
radial-gradient(ellipse 60px 20px at 90px 100%, var(--color-water) 50%, transparent 50%);
|
||||
background-size: 120px 60px;
|
||||
animation: wave 6s linear infinite;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.wave-2 {
|
||||
background:
|
||||
radial-gradient(ellipse 40px 15px at 20px 100%, var(--color-water-light) 50%, transparent 50%),
|
||||
radial-gradient(ellipse 40px 15px at 60px 100%, var(--color-water-light) 50%, transparent 50%);
|
||||
background-size: 80px 40px;
|
||||
animation: wave 4s linear infinite reverse;
|
||||
opacity: 0.5;
|
||||
bottom: 10px;
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(-50%); }
|
||||
}
|
||||
|
||||
/* Game Container */
|
||||
.game-container {
|
||||
max-width: 960px;
|
||||
margin: 0 auto var(--space-xl);
|
||||
padding: var(--space-md);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-hud {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--space-md);
|
||||
margin-block-end: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
background: var(--color-card-bg);
|
||||
border: 2px solid var(--color-water-light);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.hud-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hud-label {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text);
|
||||
display: block;
|
||||
margin-block-end: var(--space-xs);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.hud-value {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1rem, 3vw, 1.25rem);
|
||||
color: var(--color-gold);
|
||||
text-shadow: 0 0 10px rgba(255, 214, 10, 0.5);
|
||||
}
|
||||
|
||||
.boost-bar {
|
||||
height: 28px;
|
||||
background: var(--color-water);
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.boost-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--color-primary), var(--color-accent));
|
||||
transition: width 0.2s ease;
|
||||
position: relative;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.boost-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
transparent 6px,
|
||||
rgba(255, 255, 255, 0.15) 6px,
|
||||
rgba(255, 255, 255, 0.15) 12px
|
||||
);
|
||||
}
|
||||
|
||||
.boost-value {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: var(--color-water);
|
||||
border: 3px solid var(--color-water-light);
|
||||
border-radius: var(--radius-md);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.game-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(10, 10, 18, 0.95);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.game-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overlay-content {
|
||||
text-align: center;
|
||||
padding: var(--space-lg);
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.overlay-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.25rem, 4vw, 1.75rem);
|
||||
color: var(--color-primary);
|
||||
margin-block-end: var(--space-sm);
|
||||
}
|
||||
|
||||
.overlay-content p {
|
||||
color: var(--color-text-muted);
|
||||
margin-block-end: var(--space-md);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.final-stats {
|
||||
background: var(--color-card-bg);
|
||||
padding: var(--space-md);
|
||||
margin-block-end: var(--space-md);
|
||||
border: 2px solid var(--color-gold);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.final-stats[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-xs) 0;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stat-row span:last-child {
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
/* Features */
|
||||
.features {
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: clamp(1.125rem, 3vw, 1.5rem);
|
||||
text-align: center;
|
||||
color: var(--color-primary);
|
||||
margin-block-end: var(--space-lg);
|
||||
text-shadow: 0 0 30px rgba(0, 245, 212, 0.4);
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
@media (min-width: 700px) {
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: var(--color-card-bg);
|
||||
padding: var(--space-md);
|
||||
border: 2px solid var(--color-water-light);
|
||||
border-radius: var(--radius-md);
|
||||
text-align: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 32px rgba(0, 245, 212, 0.15);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.feature-card:focus-within {
|
||||
outline: 3px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.25rem;
|
||||
margin-block-end: var(--space-sm);
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-gold);
|
||||
margin-block-end: var(--space-xs);
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Highscores */
|
||||
.highscores-section {
|
||||
padding: var(--space-xl) var(--space-md);
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.highscores-table {
|
||||
background: var(--color-card-bg);
|
||||
border: 3px solid var(--color-water-light);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
padding: var(--space-sm);
|
||||
background: var(--color-water);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.table-body {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr 1fr;
|
||||
padding: var(--space-sm);
|
||||
border-block-end: 1px solid var(--color-water-light);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-block-end: none;
|
||||
}
|
||||
|
||||
.table-row:hover {
|
||||
background: var(--color-water);
|
||||
}
|
||||
|
||||
.table-row--empty {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 400;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.table-row:first-child:not(.table-row--empty) span:first-child {
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
.table-row:nth-child(2) span:first-child {
|
||||
color: #c0c0c0;
|
||||
}
|
||||
|
||||
.table-row:nth-child(3) span:first-child {
|
||||
color: #cd7f32;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.site-footer {
|
||||
background: var(--color-water);
|
||||
padding: var(--space-md) var(--space-md) var(--space-lg);
|
||||
text-align: center;
|
||||
border-block-start: 3px solid var(--color-water-light);
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
color: var(--color-primary);
|
||||
margin-block-end: var(--space-xs);
|
||||
}
|
||||
|
||||
.footer-credits {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.instructions-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(10, 10, 18, 0.95);
|
||||
z-index: 200;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: var(--space-md);
|
||||
}
|
||||
|
||||
.instructions-modal[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--color-card-bg);
|
||||
padding: var(--space-lg);
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
border: 3px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
inset-block-start: var(--space-sm);
|
||||
inset-inline-end: var(--space-sm);
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-muted);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
padding: var(--space-xs);
|
||||
line-height: 1;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.modal-close:hover,
|
||||
.modal-close:focus {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.modal-close:focus-visible {
|
||||
outline: 2px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.125rem;
|
||||
color: var(--color-primary);
|
||||
margin-block-end: var(--space-md);
|
||||
}
|
||||
|
||||
.modal-content h3 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1rem;
|
||||
color: var(--color-gold);
|
||||
margin-block-start: var(--space-md);
|
||||
margin-block-end: var(--space-sm);
|
||||
}
|
||||
|
||||
.instructions-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.instruction-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.instruction-item .key {
|
||||
font-family: var(--font-body);
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
background: var(--color-water);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--color-primary);
|
||||
min-width: 56px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tips-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.tips-list li {
|
||||
padding: var(--space-xs) 0;
|
||||
padding-inline-start: var(--space-sm);
|
||||
border-inline-start: 3px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 600px) {
|
||||
.game-hud {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero {
|
||||
padding-block-start: 100px;
|
||||
}
|
||||
|
||||
.lighthouse {
|
||||
inset-inline-end: 5%;
|
||||
height: 60px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.buoy {
|
||||
width: 10px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Focus visible for all interactive elements */
|
||||
:focus-visible {
|
||||
outline: 3px solid var(--color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
Reference in New Issue
Block a user