mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-16 10:59:39 +02:00
Rename games/mana-games/ to games/arcade/, update all package names (@mana-games/* → @arcade/*), appIds, display names, docker-compose service, root scripts, and documentation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
710 lines
No EOL
21 KiB
HTML
710 lines
No EOL
21 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Card Stack Rush</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
|
min-height: 100vh;
|
|
padding: 0;
|
|
color: #333;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.top-bar {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 10px 20px;
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 15px;
|
|
}
|
|
|
|
h1 {
|
|
font-size: 1.5rem;
|
|
color: #e74c3c;
|
|
margin: 0;
|
|
}
|
|
|
|
.game-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.stat-group {
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: center;
|
|
}
|
|
|
|
.stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.stat-label {
|
|
color: #666;
|
|
}
|
|
|
|
.stat-value {
|
|
font-weight: bold;
|
|
color: #e74c3c;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.timer-bar {
|
|
height: 8px;
|
|
background: #f0f0f0;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
width: 200px;
|
|
}
|
|
|
|
.timer-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #27ae60, #f39c12, #e74c3c);
|
|
transition: width 0.1s linear;
|
|
width: 100%;
|
|
}
|
|
|
|
.btn-new {
|
|
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 20px;
|
|
font-size: 0.9rem;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.btn-new:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.game-area {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 10px;
|
|
height: calc(100vh - 60px);
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.incoming-card-area {
|
|
margin-bottom: 20px;
|
|
height: 180px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.card {
|
|
width: 120px;
|
|
height: 180px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 3rem;
|
|
font-weight: bold;
|
|
cursor: grab;
|
|
transition: all 0.2s ease;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
position: relative;
|
|
user-select: none;
|
|
}
|
|
|
|
.card:active {
|
|
cursor: grabbing;
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.card.dragging {
|
|
opacity: 0.8;
|
|
transform: scale(1.1);
|
|
z-index: 1000;
|
|
}
|
|
|
|
.card-suit {
|
|
font-size: 2.5rem;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.card.red {
|
|
background: white;
|
|
color: #e74c3c;
|
|
border: 2px solid #e74c3c;
|
|
}
|
|
|
|
.card.black {
|
|
background: white;
|
|
color: #2c3e50;
|
|
border: 2px solid #2c3e50;
|
|
}
|
|
|
|
.stacks-container {
|
|
display: flex;
|
|
gap: 40px;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.stack {
|
|
width: 140px;
|
|
height: 200px;
|
|
border: 3px dashed rgba(255, 255, 255, 0.5);
|
|
border-radius: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
transition: all 0.3s ease;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
.stack-label {
|
|
position: absolute;
|
|
top: -25px;
|
|
color: white;
|
|
font-weight: bold;
|
|
font-size: 0.9rem;
|
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
.stack.valid-drop {
|
|
border-color: #27ae60;
|
|
background: rgba(39, 174, 96, 0.2);
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.stack.invalid-drop {
|
|
border-color: #e74c3c;
|
|
background: rgba(231, 76, 60, 0.2);
|
|
animation: shake 0.3s ease;
|
|
}
|
|
|
|
@keyframes shake {
|
|
0%, 100% { transform: translateX(0); }
|
|
25% { transform: translateX(-5px); }
|
|
75% { transform: translateX(5px); }
|
|
}
|
|
|
|
.stack-cards {
|
|
position: relative;
|
|
width: 120px;
|
|
height: 180px;
|
|
}
|
|
|
|
.stack-card {
|
|
position: absolute;
|
|
width: 120px;
|
|
height: 180px;
|
|
border-radius: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 2.5rem;
|
|
font-weight: bold;
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.score-popup {
|
|
position: absolute;
|
|
font-weight: bold;
|
|
font-size: 1.2rem;
|
|
animation: scoreFloat 1s ease-out forwards;
|
|
pointer-events: none;
|
|
z-index: 100;
|
|
}
|
|
|
|
@keyframes scoreFloat {
|
|
0% {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
100% {
|
|
opacity: 0;
|
|
transform: translateY(-50px);
|
|
}
|
|
}
|
|
|
|
.combo-indicator {
|
|
position: fixed;
|
|
top: 80px;
|
|
right: 20px;
|
|
background: rgba(255, 255, 255, 0.95);
|
|
padding: 10px 20px;
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
display: none;
|
|
animation: comboAnimation 0.3s ease;
|
|
}
|
|
|
|
@keyframes comboAnimation {
|
|
0% { transform: scale(0.8); opacity: 0; }
|
|
50% { transform: scale(1.1); }
|
|
100% { transform: scale(1); opacity: 1; }
|
|
}
|
|
|
|
.combo-text {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: #e74c3c;
|
|
}
|
|
|
|
.game-over {
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 20px;
|
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
|
|
text-align: center;
|
|
display: none;
|
|
z-index: 1000;
|
|
}
|
|
|
|
.game-over h2 {
|
|
color: #e74c3c;
|
|
font-size: 2.5rem;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.final-stats {
|
|
margin: 20px 0;
|
|
font-size: 1.2rem;
|
|
}
|
|
|
|
.overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: none;
|
|
z-index: 999;
|
|
}
|
|
|
|
.rule-text {
|
|
color: white;
|
|
text-align: center;
|
|
font-size: 1.2rem;
|
|
margin: 10px 0;
|
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.stacks-container {
|
|
gap: 20px;
|
|
}
|
|
|
|
.stack {
|
|
width: 100px;
|
|
height: 150px;
|
|
}
|
|
|
|
.card {
|
|
width: 90px;
|
|
height: 135px;
|
|
font-size: 2.2rem;
|
|
}
|
|
|
|
.card-suit {
|
|
font-size: 1.8rem;
|
|
}
|
|
|
|
.stack-card {
|
|
width: 90px;
|
|
height: 135px;
|
|
font-size: 1.8rem;
|
|
}
|
|
|
|
.timer-bar {
|
|
width: 150px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="top-bar">
|
|
<h1>Card Stack Rush</h1>
|
|
|
|
<div class="game-controls">
|
|
<div class="stat-group">
|
|
<div class="stat">
|
|
<span class="stat-label">Punkte:</span>
|
|
<span class="stat-value" id="score">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Karten:</span>
|
|
<span class="stat-value" id="cardsPlaced">0</span>
|
|
</div>
|
|
<div class="stat">
|
|
<span class="stat-label">Zeit:</span>
|
|
<div class="timer-bar">
|
|
<div class="timer-fill" id="timerFill"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<button class="btn-new" onclick="newGame()">Neues Spiel</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="game-area">
|
|
<div class="rule-text" id="ruleText">Sortiere nach Farbe!</div>
|
|
|
|
<div class="incoming-card-area" id="incomingArea">
|
|
</div>
|
|
|
|
<div class="stacks-container" id="stacksContainer">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="combo-indicator" id="comboIndicator">
|
|
<div class="combo-text" id="comboText">Combo x2!</div>
|
|
</div>
|
|
|
|
<div class="overlay" id="overlay"></div>
|
|
<div class="game-over" id="gameOver">
|
|
<h2>Zeit abgelaufen!</h2>
|
|
<div class="final-stats">
|
|
<p>Endpunktzahl: <strong id="finalScore">0</strong></p>
|
|
<p>Platzierte Karten: <strong id="finalCards">0</strong></p>
|
|
<p>Höchste Combo: <strong id="finalCombo">0</strong></p>
|
|
</div>
|
|
<button class="btn-new" onclick="newGame()">Neues Spiel</button>
|
|
</div>
|
|
|
|
<script>
|
|
const suits = ['♠', '♣', '♥', '♦'];
|
|
const values = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'];
|
|
const rules = {
|
|
color: {
|
|
name: 'Sortiere nach Farbe!',
|
|
stacks: [
|
|
{ label: 'Rot', accepts: ['♥', '♦'] },
|
|
{ label: 'Schwarz', accepts: ['♠', '♣'] }
|
|
]
|
|
},
|
|
suit: {
|
|
name: 'Sortiere nach Symbol!',
|
|
stacks: [
|
|
{ label: '♠', accepts: ['♠'] },
|
|
{ label: '♣', accepts: ['♣'] },
|
|
{ label: '♥', accepts: ['♥'] },
|
|
{ label: '♦', accepts: ['♦'] }
|
|
]
|
|
},
|
|
value: {
|
|
name: 'Sortiere nach Wert!',
|
|
stacks: [
|
|
{ label: 'Niedrig (A-5)', accepts: ['A', '2', '3', '4', '5'] },
|
|
{ label: 'Mittel (6-10)', accepts: ['6', '7', '8', '9', '10'] },
|
|
{ label: 'Hoch (J-K)', accepts: ['J', 'Q', 'K'] }
|
|
]
|
|
}
|
|
};
|
|
|
|
let score = 0;
|
|
let cardsPlaced = 0;
|
|
let combo = 0;
|
|
let maxCombo = 0;
|
|
let currentRule = 'color';
|
|
let gameActive = false;
|
|
let timeLeft = 45;
|
|
let timerInterval;
|
|
let currentCard = null;
|
|
let isDragging = false;
|
|
|
|
function createCard(value, suit) {
|
|
const card = document.createElement('div');
|
|
const isRed = suit === '♥' || suit === '♦';
|
|
card.className = `card ${isRed ? 'red' : 'black'}`;
|
|
card.draggable = true;
|
|
card.dataset.value = value;
|
|
card.dataset.suit = suit;
|
|
|
|
card.innerHTML = `
|
|
<div>${value}</div>
|
|
<div class="card-suit">${suit}</div>
|
|
`;
|
|
|
|
card.addEventListener('dragstart', handleDragStart);
|
|
card.addEventListener('dragend', handleDragEnd);
|
|
card.addEventListener('click', handleCardClick);
|
|
|
|
return card;
|
|
}
|
|
|
|
function generateRandomCard() {
|
|
const value = values[Math.floor(Math.random() * values.length)];
|
|
const suit = suits[Math.floor(Math.random() * suits.length)];
|
|
return createCard(value, suit);
|
|
}
|
|
|
|
function createStacks() {
|
|
const container = document.getElementById('stacksContainer');
|
|
container.innerHTML = '';
|
|
|
|
const rule = rules[currentRule];
|
|
rule.stacks.forEach((stack, index) => {
|
|
const stackDiv = document.createElement('div');
|
|
stackDiv.className = 'stack';
|
|
stackDiv.dataset.stackIndex = index;
|
|
|
|
const label = document.createElement('div');
|
|
label.className = 'stack-label';
|
|
label.textContent = stack.label;
|
|
|
|
const cardsDiv = document.createElement('div');
|
|
cardsDiv.className = 'stack-cards';
|
|
|
|
stackDiv.appendChild(label);
|
|
stackDiv.appendChild(cardsDiv);
|
|
|
|
stackDiv.addEventListener('dragover', handleDragOver);
|
|
stackDiv.addEventListener('drop', handleDrop);
|
|
stackDiv.addEventListener('dragleave', handleDragLeave);
|
|
stackDiv.addEventListener('click', handleStackClick);
|
|
|
|
container.appendChild(stackDiv);
|
|
});
|
|
}
|
|
|
|
function handleDragStart(e) {
|
|
isDragging = true;
|
|
currentCard = e.target;
|
|
e.target.classList.add('dragging');
|
|
}
|
|
|
|
function handleDragEnd(e) {
|
|
isDragging = false;
|
|
e.target.classList.remove('dragging');
|
|
}
|
|
|
|
function handleDragOver(e) {
|
|
e.preventDefault();
|
|
const stack = e.currentTarget;
|
|
if (isValidDrop(currentCard, stack)) {
|
|
stack.classList.add('valid-drop');
|
|
}
|
|
}
|
|
|
|
function handleDragLeave(e) {
|
|
e.currentTarget.classList.remove('valid-drop');
|
|
}
|
|
|
|
function handleDrop(e) {
|
|
e.preventDefault();
|
|
const stack = e.currentTarget;
|
|
stack.classList.remove('valid-drop');
|
|
|
|
if (currentCard && isValidDrop(currentCard, stack)) {
|
|
placeCard(currentCard, stack);
|
|
}
|
|
}
|
|
|
|
function handleCardClick(e) {
|
|
if (!isDragging && !currentCard) {
|
|
currentCard = e.target.closest('.card');
|
|
currentCard.style.transform = 'scale(1.1)';
|
|
currentCard.style.boxShadow = '0 8px 20px rgba(0, 0, 0, 0.3)';
|
|
}
|
|
}
|
|
|
|
function handleStackClick(e) {
|
|
if (currentCard && !isDragging) {
|
|
const stack = e.currentTarget;
|
|
if (isValidDrop(currentCard, stack)) {
|
|
placeCard(currentCard, stack);
|
|
} else {
|
|
stack.classList.add('invalid-drop');
|
|
setTimeout(() => stack.classList.remove('invalid-drop'), 300);
|
|
}
|
|
currentCard.style.transform = '';
|
|
currentCard.style.boxShadow = '';
|
|
currentCard = null;
|
|
}
|
|
}
|
|
|
|
function isValidDrop(card, stack) {
|
|
const stackIndex = parseInt(stack.dataset.stackIndex);
|
|
const rule = rules[currentRule];
|
|
const stackRule = rule.stacks[stackIndex];
|
|
|
|
if (currentRule === 'color') {
|
|
const isRed = card.dataset.suit === '♥' || card.dataset.suit === '♦';
|
|
return (stackRule.label === 'Rot' && isRed) ||
|
|
(stackRule.label === 'Schwarz' && !isRed);
|
|
} else if (currentRule === 'suit') {
|
|
return stackRule.accepts.includes(card.dataset.suit);
|
|
} else if (currentRule === 'value') {
|
|
return stackRule.accepts.includes(card.dataset.value);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
function placeCard(card, stack) {
|
|
const stackCards = stack.querySelector('.stack-cards');
|
|
const stackCard = card.cloneNode(true);
|
|
stackCard.className = 'stack-card ' + (card.classList.contains('red') ? 'red' : 'black');
|
|
stackCard.style.transform = `translateY(${stackCards.children.length * -2}px)`;
|
|
stackCards.appendChild(stackCard);
|
|
|
|
card.remove();
|
|
|
|
cardsPlaced++;
|
|
combo++;
|
|
score += 10 * Math.max(1, Math.floor(combo / 3));
|
|
|
|
if (combo > maxCombo) maxCombo = combo;
|
|
|
|
updateStats();
|
|
showScorePopup(stack, 10 * Math.max(1, Math.floor(combo / 3)));
|
|
|
|
if (combo >= 3 && combo % 3 === 0) {
|
|
showCombo();
|
|
}
|
|
|
|
nextCard();
|
|
}
|
|
|
|
function showScorePopup(element, points) {
|
|
const popup = document.createElement('div');
|
|
popup.className = 'score-popup';
|
|
popup.textContent = `+${points}`;
|
|
popup.style.color = points > 10 ? '#27ae60' : '#e74c3c';
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
popup.style.left = rect.left + rect.width / 2 + 'px';
|
|
popup.style.top = rect.top + 'px';
|
|
|
|
document.body.appendChild(popup);
|
|
setTimeout(() => popup.remove(), 1000);
|
|
}
|
|
|
|
function showCombo() {
|
|
const indicator = document.getElementById('comboIndicator');
|
|
const text = document.getElementById('comboText');
|
|
text.textContent = `${combo}x Combo!`;
|
|
indicator.style.display = 'block';
|
|
|
|
setTimeout(() => {
|
|
indicator.style.display = 'none';
|
|
}, 1500);
|
|
}
|
|
|
|
function nextCard() {
|
|
const incomingArea = document.getElementById('incomingArea');
|
|
incomingArea.innerHTML = '';
|
|
|
|
if (Math.random() < 0.25) {
|
|
changeRule();
|
|
}
|
|
|
|
const newCard = generateRandomCard();
|
|
incomingArea.appendChild(newCard);
|
|
}
|
|
|
|
function changeRule() {
|
|
const ruleKeys = Object.keys(rules);
|
|
let newRule;
|
|
do {
|
|
newRule = ruleKeys[Math.floor(Math.random() * ruleKeys.length)];
|
|
} while (newRule === currentRule);
|
|
|
|
currentRule = newRule;
|
|
document.getElementById('ruleText').textContent = rules[currentRule].name;
|
|
createStacks();
|
|
combo = 0;
|
|
}
|
|
|
|
function updateStats() {
|
|
document.getElementById('score').textContent = score;
|
|
document.getElementById('cardsPlaced').textContent = cardsPlaced;
|
|
}
|
|
|
|
function updateTimer() {
|
|
timeLeft--;
|
|
const percentage = (timeLeft / 45) * 100;
|
|
document.getElementById('timerFill').style.width = percentage + '%';
|
|
|
|
if (timeLeft <= 0) {
|
|
endGame();
|
|
}
|
|
}
|
|
|
|
function startGame() {
|
|
gameActive = true;
|
|
score = 0;
|
|
cardsPlaced = 0;
|
|
combo = 0;
|
|
maxCombo = 0;
|
|
timeLeft = 45;
|
|
currentRule = 'color';
|
|
|
|
document.getElementById('ruleText').textContent = rules[currentRule].name;
|
|
updateStats();
|
|
createStacks();
|
|
nextCard();
|
|
|
|
timerInterval = setInterval(updateTimer, 1000);
|
|
}
|
|
|
|
function endGame() {
|
|
gameActive = false;
|
|
clearInterval(timerInterval);
|
|
|
|
document.getElementById('finalScore').textContent = score;
|
|
document.getElementById('finalCards').textContent = cardsPlaced;
|
|
document.getElementById('finalCombo').textContent = maxCombo;
|
|
|
|
document.getElementById('overlay').style.display = 'block';
|
|
document.getElementById('gameOver').style.display = 'block';
|
|
}
|
|
|
|
function newGame() {
|
|
document.getElementById('overlay').style.display = 'none';
|
|
document.getElementById('gameOver').style.display = 'none';
|
|
startGame();
|
|
}
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (currentCard && !e.target.closest('.card') && !e.target.closest('.stack')) {
|
|
currentCard.style.transform = '';
|
|
currentCard.style.boxShadow = '';
|
|
currentCard = null;
|
|
}
|
|
});
|
|
|
|
startGame();
|
|
</script>
|
|
</body>
|
|
</html> |