Update 2025-04-17_20:33:11
This commit is contained in:
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
# Bytecode-Dateien
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# Virtuelle Umgebungen
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Betriebssystem-Dateien
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logfiles und Dumps
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
*.tmp
|
||||
|
||||
# IDEs und Editoren
|
||||
.vscode/
|
||||
.idea/
|
24
main.py
Normal file
24
main.py
Normal file
@ -0,0 +1,24 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pathlib import Path
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Mount static files directory
|
||||
app.mount("/static", StaticFiles(directory="static"), name="static")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return FileResponse("static/index.html")
|
||||
|
||||
@app.post("/api/start")
|
||||
async def start_game(request: Request):
|
||||
return JSONResponse({"status": "started"})
|
||||
|
||||
@app.post("/api/score")
|
||||
async def submit_score(request: Request):
|
||||
data = await request.json()
|
||||
score = data.get("score", 0)
|
||||
# TODO: Implement score saving logic
|
||||
return JSONResponse({"status": "saved", "score": score})
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
uvicorn
|
||||
|
363
static/app.js
Normal file
363
static/app.js
Normal file
@ -0,0 +1,363 @@
|
||||
import { DEBUG, GAME_CONFIG } from './config.js';
|
||||
|
||||
class Game {
|
||||
constructor() {
|
||||
this.score = 0;
|
||||
this.isRunning = false;
|
||||
this.obstacles = [];
|
||||
this.gameStartTime = 0;
|
||||
this.gameTimer = null;
|
||||
this.GAME_DURATION = 120000; // 2 minutes in milliseconds
|
||||
this.lockedRoads = new Set(); // Track which roads are currently in use
|
||||
this.collectedItems = new Set(); // Track unique collected items
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.startButton = document.getElementById('start-game');
|
||||
this.scoreElement = document.getElementById('score');
|
||||
this.timeElement = document.getElementById('time');
|
||||
this.collectionElement = document.getElementById('collection');
|
||||
this.pipes = document.querySelectorAll('.pipe');
|
||||
this.roads = document.querySelectorAll('.road');
|
||||
|
||||
// Initialize UI with emojis
|
||||
this.startButton.textContent = '🎮 Start Game';
|
||||
this.updateScore();
|
||||
this.timeElement.textContent = '⏱️ 2:00';
|
||||
this.updateCollection();
|
||||
|
||||
this.startButton.addEventListener('click', () => this.startGame());
|
||||
this.pipes.forEach(pipe => {
|
||||
pipe.addEventListener('click', (e) => this.handlePipeClick(e));
|
||||
});
|
||||
|
||||
// Start collision detection loop
|
||||
this.collisionDetectionLoop();
|
||||
}
|
||||
|
||||
updateTimer() {
|
||||
const timeLeft = Math.max(0, this.GAME_DURATION - (Date.now() - this.gameStartTime));
|
||||
const minutes = Math.floor(timeLeft / 60000);
|
||||
const seconds = Math.floor((timeLeft % 60000) / 1000);
|
||||
this.timeElement.textContent = `⏱️ ${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
|
||||
if (this.isRunning) {
|
||||
requestAnimationFrame(() => this.updateTimer());
|
||||
}
|
||||
}
|
||||
|
||||
updateCollection() {
|
||||
if (!this.collectionElement) return;
|
||||
|
||||
const adventureNames = {
|
||||
// Places & Landmarks
|
||||
'🗺️': 'Treasure Map', '🧭': 'Compass', '⛺': 'Camp Site', '🏰': 'Castle',
|
||||
'🗿': 'Ancient Monument', '🎪': 'Circus', '🎡': 'Fair', '🎢': 'Adventure Park',
|
||||
'🏛️': 'Ancient Ruins', '⛩️': 'Sacred Shrine', '🕌': 'Grand Mosque', '⛪': 'Old Church',
|
||||
'🏯': 'Palace', '🏭': 'Steam Factory',
|
||||
|
||||
// Nature & Weather
|
||||
'🌋': 'Volcano', '🏔️': 'Mountain Peak', '🏝️': 'Desert Island', '🌅': 'Sunset Beach',
|
||||
'🌠': 'Stargazing', '🌄': 'Mountain Sunrise', '🏞️': 'National Park', '🌈': 'Rainbow Valley',
|
||||
'🌊': 'Ocean Waves', '❄️': 'Snow Peak', '🌺': 'Flower Garden', '🌴': 'Palm Beach',
|
||||
'🍄': 'Mystic Forest', '🌵': 'Desert Trail',
|
||||
|
||||
// Transport & Movement
|
||||
'🚂': 'Steam Train', '🚤': 'Speed Boat', '✈️': 'Sky Journey', '🎈': 'Balloon Ride',
|
||||
'🛸': 'Space Travel', '🚁': 'Helicopter Tour', '⛵': 'Sailing Trip',
|
||||
|
||||
// Wildlife
|
||||
'🦁': 'Lion Safari', '🐘': 'Elephant Trek', '🦒': 'Giraffe Watch', '🦜': 'Exotic Birds',
|
||||
'🐠': 'Ocean Diving', '🦋': 'Butterfly Garden', '🐪': 'Desert Caravan'
|
||||
};
|
||||
|
||||
const totalAdventures = Object.keys(adventureNames).length;
|
||||
const collectedCount = this.collectedItems.size;
|
||||
|
||||
if (collectedCount === totalAdventures) {
|
||||
// All adventures collected!
|
||||
this.collectionElement.textContent = '🎉 Congratulations! You\'ve collected all adventures! You\'re a true explorer! 🌟';
|
||||
this.collectionElement.style.fontSize = '20px';
|
||||
this.collectionElement.style.color = '#F39C12'; // Using the highlight-orange color
|
||||
} else if (collectedCount === 0) {
|
||||
this.collectionElement.textContent = 'No adventures yet! 🎒';
|
||||
this.collectionElement.style.fontSize = '16px';
|
||||
this.collectionElement.style.color = ''; // Reset color
|
||||
} else {
|
||||
// Show progress in two lines
|
||||
this.collectionElement.innerHTML =
|
||||
`🎒 ${collectedCount} different adventures collected! 🗺️<br>` +
|
||||
`🔍 ${totalAdventures - collectedCount} more to discover! ✨`;
|
||||
this.collectionElement.style.fontSize = '16px';
|
||||
this.collectionElement.style.color = ''; // Reset color
|
||||
}
|
||||
}
|
||||
|
||||
startGame() {
|
||||
if (this.isRunning) {
|
||||
this.endGame();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
this.score = 0;
|
||||
this.collectedItems.clear(); // Reset collection
|
||||
this.gameStartTime = Date.now();
|
||||
this.updateScore();
|
||||
this.updateCollection();
|
||||
this.startButton.textContent = '🔄 Reset Game';
|
||||
this.timeElement.textContent = '⏱️ 2:00';
|
||||
|
||||
// Start spawning obstacles
|
||||
this.spawnObstacles();
|
||||
|
||||
// Start game timer
|
||||
this.gameTimer = setTimeout(() => {
|
||||
this.endGame(true);
|
||||
}, this.GAME_DURATION);
|
||||
|
||||
// Start timer updates
|
||||
this.updateTimer();
|
||||
}
|
||||
|
||||
endGame(timeUp = false) {
|
||||
this.isRunning = false;
|
||||
// Don't clear collection on game end to show final collection
|
||||
this.score = 0;
|
||||
this.updateScore();
|
||||
this.lockedRoads.clear(); // Clear all road locks
|
||||
|
||||
if (timeUp) {
|
||||
this.startButton.textContent = '🏁 Game Over! Play Again?';
|
||||
} else {
|
||||
this.startButton.textContent = '🎮 Start Game';
|
||||
}
|
||||
|
||||
// Clear timer if it exists
|
||||
if (this.gameTimer) {
|
||||
clearTimeout(this.gameTimer);
|
||||
this.gameTimer = null;
|
||||
}
|
||||
|
||||
// Reset squares to top position
|
||||
document.querySelectorAll('.square').forEach(square => {
|
||||
square.className = 'square top';
|
||||
square.dataset.position = 'top';
|
||||
});
|
||||
|
||||
// Clear obstacles
|
||||
document.querySelectorAll('.obstacle').forEach(o => o.remove());
|
||||
}
|
||||
|
||||
handlePipeClick(event) {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const pipe = event.currentTarget;
|
||||
const square = pipe.querySelector('.square');
|
||||
|
||||
if (!square) return;
|
||||
|
||||
// Toggle square position
|
||||
const isTop = square.dataset.position === 'top';
|
||||
square.className = `square ${isTop ? 'bottom' : 'top'}`;
|
||||
square.dataset.position = isTop ? 'bottom' : 'top';
|
||||
|
||||
if (DEBUG) console.log(`Pipe ${pipe.dataset.pipe} square moved to ${square.dataset.position}`);
|
||||
}
|
||||
|
||||
checkCollision(square, obstacle) {
|
||||
const squareRect = square.getBoundingClientRect();
|
||||
const obstacleRect = obstacle.getBoundingClientRect();
|
||||
const roadRect = obstacle.parentElement.getBoundingClientRect();
|
||||
|
||||
// Only check collisions near the ends
|
||||
const isNearEnds =
|
||||
obstacleRect.right <= roadRect.left + 70 || // Left end
|
||||
obstacleRect.left >= roadRect.right - 70; // Right end
|
||||
|
||||
if (!isNearEnds) return false;
|
||||
|
||||
const hasCollision = !(squareRect.right < obstacleRect.left ||
|
||||
squareRect.left > obstacleRect.right ||
|
||||
squareRect.bottom < obstacleRect.top ||
|
||||
squareRect.top > obstacleRect.bottom);
|
||||
|
||||
if (hasCollision) {
|
||||
// Add to collection when collected
|
||||
this.collectedItems.add(obstacle.textContent);
|
||||
this.updateCollection();
|
||||
}
|
||||
|
||||
return hasCollision;
|
||||
}
|
||||
|
||||
collisionDetectionLoop() {
|
||||
if (this.isRunning) {
|
||||
const squares = document.querySelectorAll('.square');
|
||||
const obstacles = document.querySelectorAll('.obstacle');
|
||||
|
||||
squares.forEach(square => {
|
||||
obstacles.forEach(obstacle => {
|
||||
if (this.checkCollision(square, obstacle)) {
|
||||
// Handle collision
|
||||
square.classList.add('collision');
|
||||
obstacle.classList.add('collision');
|
||||
this.updateScore(1);
|
||||
|
||||
// Remove collision class after animation
|
||||
setTimeout(() => {
|
||||
square.classList.remove('collision');
|
||||
obstacle.classList.remove('collision');
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Continue the loop
|
||||
requestAnimationFrame(() => this.collisionDetectionLoop());
|
||||
}
|
||||
|
||||
getDifficultySettings() {
|
||||
const elapsedTime = Date.now() - this.gameStartTime;
|
||||
const progress = Math.min(1, elapsedTime / GAME_CONFIG.difficultyRampUpTime);
|
||||
|
||||
// Interpolate between initial and final values
|
||||
return {
|
||||
minSpeed: this.lerp(GAME_CONFIG.initial.minSpeed, GAME_CONFIG.final.minSpeed, progress),
|
||||
maxSpeed: this.lerp(GAME_CONFIG.initial.maxSpeed, GAME_CONFIG.final.maxSpeed, progress),
|
||||
minSpawnRate: this.lerp(GAME_CONFIG.initial.minSpawnRate, GAME_CONFIG.final.minSpawnRate, progress),
|
||||
maxSpawnRate: this.lerp(GAME_CONFIG.initial.maxSpawnRate, GAME_CONFIG.final.maxSpawnRate, progress),
|
||||
maxObstacles: Math.round(this.lerp(GAME_CONFIG.initial.maxObstacles, GAME_CONFIG.final.maxObstacles, progress)),
|
||||
multiSpawnChance: this.lerp(GAME_CONFIG.initial.multiSpawnChance, GAME_CONFIG.final.multiSpawnChance, progress)
|
||||
};
|
||||
}
|
||||
|
||||
lerp(start, end, progress) {
|
||||
return start + (end - start) * progress;
|
||||
}
|
||||
|
||||
spawnObstacles() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
const settings = this.getDifficultySettings();
|
||||
|
||||
// Count current obstacles
|
||||
const currentObstacles = document.querySelectorAll('.obstacle').length;
|
||||
if (currentObstacles >= settings.maxObstacles) {
|
||||
// Schedule next check if at max
|
||||
setTimeout(() => this.spawnObstacles(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get available (unlocked) roads
|
||||
const availableRoads = Array.from(this.roads).filter(road => !this.lockedRoads.has(road));
|
||||
|
||||
if (availableRoads.length === 0) {
|
||||
// No available roads, try again soon
|
||||
setTimeout(() => this.spawnObstacles(), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pick a random available road
|
||||
const roadIndex = Math.floor(Math.random() * availableRoads.length);
|
||||
const road = availableRoads[roadIndex];
|
||||
|
||||
// Lock the road
|
||||
this.lockedRoads.add(road);
|
||||
|
||||
// Create obstacle (now collectible)
|
||||
const obstacle = document.createElement('div');
|
||||
obstacle.className = 'obstacle';
|
||||
|
||||
// Add random adventure emoji as collectible
|
||||
const adventureEmojis = [
|
||||
// Places & Landmarks (14)
|
||||
'🗺️', '🧭', '⛺', '🏰', '🗿', '🎪', '🎡', '🎢', '🏛️', '⛩️', '🕌', '⛪', '🏯', '🏭',
|
||||
|
||||
// Nature & Weather (14)
|
||||
'🌋', '🏔️', '🏝️', '🌅', '🌠', '🌄', '🏞️', '🌈', '🌊', '❄️', '🌺', '🌴', '🍄', '🌵',
|
||||
|
||||
// Transport & Movement (7)
|
||||
'🚂', '🚤', '✈️', '🎈', '🛸', '🚁', '⛵',
|
||||
|
||||
// Wildlife (7)
|
||||
'🦁', '🐘', '🦒', '🦜', '🐠', '🦋', '🐪'
|
||||
];
|
||||
const randomEmoji = adventureEmojis[Math.floor(Math.random() * adventureEmojis.length)];
|
||||
obstacle.textContent = randomEmoji;
|
||||
|
||||
// Set initial position (left or right side)
|
||||
const goingLeft = Math.random() > 0.5;
|
||||
|
||||
// Base distance is 330px
|
||||
// If spawning on left and moving right:
|
||||
// Start at 324px (6px less on left), end at 336px (6px more on right)
|
||||
// If spawning on right and moving left:
|
||||
// Start at -324px (6px less on right), end at -336px (6px more on left)
|
||||
const startPos = goingLeft ? 324 : -324;
|
||||
const endPos = goingLeft ? -336 : 336;
|
||||
|
||||
obstacle.style.transform = `translateX(${startPos}px)`;
|
||||
|
||||
road.appendChild(obstacle);
|
||||
|
||||
// Random speed between current min and max speeds
|
||||
const randomSpeed = settings.minSpeed +
|
||||
Math.random() * (settings.maxSpeed - settings.minSpeed);
|
||||
|
||||
// Animate obstacle
|
||||
const animation = obstacle.animate([
|
||||
{ transform: `translateX(${startPos}px)` },
|
||||
{ transform: `translateX(${endPos}px)` }
|
||||
], {
|
||||
duration: randomSpeed * 1000,
|
||||
easing: 'linear'
|
||||
});
|
||||
|
||||
animation.onfinish = () => {
|
||||
if (this.isRunning) {
|
||||
// Unlock the road when obstacle is removed
|
||||
this.lockedRoads.delete(road);
|
||||
obstacle.remove();
|
||||
}
|
||||
};
|
||||
|
||||
// Random spawn rate based on current difficulty
|
||||
const nextSpawnRate = settings.minSpawnRate +
|
||||
Math.random() * (settings.maxSpawnRate - settings.minSpawnRate);
|
||||
|
||||
// Schedule next spawn
|
||||
setTimeout(() => {
|
||||
if (this.isRunning) {
|
||||
this.spawnObstacles();
|
||||
}
|
||||
}, nextSpawnRate);
|
||||
}
|
||||
|
||||
updateScore(increment = 0) {
|
||||
this.score += increment;
|
||||
this.scoreElement.textContent = `🧭 Adventures: ${this.score}`;
|
||||
|
||||
// Add sparkle effect on score increase
|
||||
if (increment > 0) {
|
||||
const sparkle = document.createElement('span');
|
||||
sparkle.textContent = ' ✨';
|
||||
sparkle.style.opacity = '1';
|
||||
this.scoreElement.appendChild(sparkle);
|
||||
|
||||
// Fade out and remove sparkle
|
||||
setTimeout(() => {
|
||||
sparkle.style.transition = 'opacity 0.5s';
|
||||
sparkle.style.opacity = '0';
|
||||
setTimeout(() => sparkle.remove(), 500);
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start the game when the page loads
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
new Game();
|
||||
});
|
23
static/config.js
Normal file
23
static/config.js
Normal file
@ -0,0 +1,23 @@
|
||||
export const DEBUG = true;
|
||||
export const GAME_CONFIG = {
|
||||
// Initial (easier) settings
|
||||
initial: {
|
||||
minSpeed: 2, // Slower at start
|
||||
maxSpeed: 2.5, // More consistent speed
|
||||
minSpawnRate: 1000, // Slower spawns
|
||||
maxSpawnRate: 1500, // More time between spawns
|
||||
maxObstacles: 6, // Fewer obstacles
|
||||
multiSpawnChance: 0.1 // Rare multi-spawns
|
||||
},
|
||||
// Final (challenging) settings after 60 seconds
|
||||
final: {
|
||||
minSpeed: 1.2, // Faster minimum speed
|
||||
maxSpeed: 3.5, // More speed variation
|
||||
minSpawnRate: 300, // Faster spawns
|
||||
maxSpawnRate: 900, // Quicker timing
|
||||
maxObstacles: 12, // More simultaneous obstacles
|
||||
multiSpawnChance: 0.4 // More frequent multi-spawns
|
||||
},
|
||||
difficultyRampUpTime: 60000 // Time in ms to reach final difficulty (60 seconds)
|
||||
};
|
||||
|
66
static/index.html
Normal file
66
static/index.html
Normal file
@ -0,0 +1,66 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>2 Cars Game</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="game-area">
|
||||
<!-- Left buttons -->
|
||||
<div class="button-column">
|
||||
<div class="pipe" data-pipe="1">
|
||||
<div class="square top" data-position="top"></div>
|
||||
</div>
|
||||
<div class="pipe" data-pipe="2">
|
||||
<div class="square top" data-position="top"></div>
|
||||
</div>
|
||||
<div class="pipe" data-pipe="3">
|
||||
<div class="square top" data-position="top"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Roads -->
|
||||
<div class="road-container">
|
||||
<div class="road-pair">
|
||||
<div class="road" data-road="1"></div>
|
||||
<div class="road" data-road="2"></div>
|
||||
</div>
|
||||
<div class="road-pair">
|
||||
<div class="road" data-road="3"></div>
|
||||
<div class="road" data-road="4"></div>
|
||||
</div>
|
||||
<div class="road-pair">
|
||||
<div class="road" data-road="5"></div>
|
||||
<div class="road" data-road="6"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right buttons -->
|
||||
<div class="button-column">
|
||||
<div class="pipe" data-pipe="4">
|
||||
<div class="square top" data-position="top"></div>
|
||||
</div>
|
||||
<div class="pipe" data-pipe="5">
|
||||
<div class="square top" data-position="top"></div>
|
||||
</div>
|
||||
<div class="pipe" data-pipe="6">
|
||||
<div class="square top" data-position="top"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="score-container">
|
||||
<div class="score" id="score">🧭 Adventures: 0</div>
|
||||
<div class="timer"><span id="time">⏱️ 2:00</span></div>
|
||||
</div>
|
||||
<button id="start-game">Start Game</button>
|
||||
<div class="collection" id="collection">No adventures yet! 🎒</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/static/config.js"></script>
|
||||
<script type="module" src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
182
static/style.css
Normal file
182
static/style.css
Normal file
@ -0,0 +1,182 @@
|
||||
:root {
|
||||
--primary-blue: #4A90E2;
|
||||
--highlight-orange: #F39C12;
|
||||
--soft-cream: #FFF9E5;
|
||||
--dark-gray: #34495E;
|
||||
--success-green: #2ECC71;
|
||||
--error-red: #E74C3C;
|
||||
--light-gray: #ECF0F1;
|
||||
--button-height: 50px;
|
||||
--road-height: 30px;
|
||||
--gap: 10px;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
background-color: var(--soft-cream);
|
||||
color: var(--dark-gray);
|
||||
}
|
||||
|
||||
.game-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.game-area {
|
||||
display: grid;
|
||||
grid-template-columns: 50px 1fr 50px;
|
||||
gap: var(--gap);
|
||||
min-width: 600px;
|
||||
border: 2px solid var(--dark-gray);
|
||||
border-radius: 8px;
|
||||
padding: var(--gap);
|
||||
background-color: var(--light-gray);
|
||||
}
|
||||
|
||||
.button-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 50px;
|
||||
height: 170px;
|
||||
}
|
||||
|
||||
.pipe {
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
background-color: var(--primary-blue);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.square {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
background-color: var(--highlight-orange);
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
transition: top 0.3s ease-out;
|
||||
}
|
||||
|
||||
.square.top {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
.square.bottom {
|
||||
top: 25px;
|
||||
}
|
||||
|
||||
.road-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap);
|
||||
}
|
||||
|
||||
.road-pair {
|
||||
height: var(--button-height);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.road {
|
||||
height: 15px;
|
||||
background-color: var(--primary-blue);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.obstacle {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
top: -2.5px;
|
||||
left: calc(50% - 10px);
|
||||
will-change: transform;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.collision {
|
||||
animation: flash 0.3s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
@keyframes flash {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1.2);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: var(--gap);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.score-container {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
font-size: 1.2em;
|
||||
background-color: var(--primary-blue);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--highlight-orange);
|
||||
}
|
||||
|
||||
.score, .timer {
|
||||
font-size: 1.5em;
|
||||
margin-top: var(--gap);
|
||||
}
|
||||
|
||||
.timer {
|
||||
color: var(--dark-gray);
|
||||
}
|
||||
|
||||
.collection {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.collection:empty {
|
||||
display: none;
|
||||
|
Reference in New Issue
Block a user