feat(games): add mana-games - AI-powered browser games platform
New project with 22+ browser games and AI game generation capabilities: - Astro PWA web app with game catalog and player - NestJS backend with AI game generator (Gemini, Claude, GPT-4o) - Community game submission via GitHub API - postMessage integration for score tracking - PWA support with offline capabilities 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
178
games/mana-games/CLAUDE.md
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
# Mana Games - CLAUDE.md
|
||||
|
||||
AI-powered browser games platform mit 22+ Spielen und KI-Spielgenerierung.
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
games/mana-games/
|
||||
├── apps/
|
||||
│ ├── web/ # Astro PWA (@mana-games/web)
|
||||
│ │ ├── src/
|
||||
│ │ │ ├── pages/ # Astro-Seiten
|
||||
│ │ │ ├── layouts/ # Layout-Komponenten
|
||||
│ │ │ ├── components/
|
||||
│ │ │ ├── data/ # Spielekatalog (games.ts)
|
||||
│ │ │ └── services/ # Stats, etc.
|
||||
│ │ └── public/
|
||||
│ │ ├── games/ # 22 HTML-Spiele
|
||||
│ │ ├── screenshots/
|
||||
│ │ └── icons/ # PWA Icons
|
||||
│ └── backend/ # NestJS API (@mana-games/backend)
|
||||
│ └── src/
|
||||
│ ├── game-generator/ # AI-Spielgenerierung (OpenRouter)
|
||||
│ ├── game-submission/ # Community-Einreichungen (GitHub API)
|
||||
│ └── health/
|
||||
└── package.json # Root (mana-games)
|
||||
```
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
# Alles starten (Web + Backend)
|
||||
pnpm mana-games:dev
|
||||
|
||||
# Nur Web (Astro)
|
||||
pnpm dev:mana-games:web
|
||||
|
||||
# Nur Backend (NestJS)
|
||||
pnpm dev:mana-games:backend
|
||||
|
||||
# Web + Backend zusammen
|
||||
pnpm dev:mana-games:app
|
||||
```
|
||||
|
||||
**Ports:**
|
||||
- Web: http://localhost:4321
|
||||
- Backend: http://localhost:3011
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Method | Beschreibung |
|
||||
|----------|--------|--------------|
|
||||
| `/api/health` | GET | Health Check |
|
||||
| `/api/games/generate` | POST | AI-Spielgenerierung |
|
||||
| `/api/games/submit` | POST | Community-Einreichung |
|
||||
|
||||
### POST /api/games/generate
|
||||
|
||||
```json
|
||||
{
|
||||
"description": "Ein Snake-Spiel im Neon-Stil",
|
||||
"mode": "create", // oder "iterate"
|
||||
"model": "gemini-2.0-flash",
|
||||
"originalPrompt": "...", // nur bei iterate
|
||||
"currentCode": "..." // nur bei iterate
|
||||
}
|
||||
```
|
||||
|
||||
**Unterstützte Modelle:**
|
||||
|
||||
| Modell | Provider | Beschreibung |
|
||||
|--------|----------|--------------|
|
||||
| `gemini-2.0-flash` | Google | Schnell & günstig (Standard) |
|
||||
| `gemini-2.5-flash` | Google | Schnell & gut |
|
||||
| `gemini-2.5-pro` | Google | Höchste Qualität |
|
||||
| `claude-3.5-haiku` | Anthropic | Schnell & präzise |
|
||||
| `claude-3.5-sonnet` | Anthropic | Beste Code-Qualität |
|
||||
| `gpt-4o-mini` | Azure OpenAI | Ausgewogen |
|
||||
| `gpt-4o` | Azure OpenAI | Sehr gut |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Die Variablen werden zentral in `.env.development` verwaltet:
|
||||
|
||||
```bash
|
||||
MANA_GAMES_BACKEND_PORT=3011
|
||||
|
||||
# Google Gemini API
|
||||
MANA_GAMES_GOOGLE_GENAI_API_KEY=your_key
|
||||
|
||||
# Anthropic Claude API
|
||||
MANA_GAMES_ANTHROPIC_API_KEY=your_key
|
||||
|
||||
# Azure OpenAI API
|
||||
MANA_GAMES_AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com
|
||||
MANA_GAMES_AZURE_OPENAI_API_KEY=your_key
|
||||
MANA_GAMES_AZURE_OPENAI_DEPLOYMENT=gpt-4o
|
||||
|
||||
# GitHub (für Community-Einreichungen)
|
||||
MANA_GAMES_GITHUB_TOKEN=your_token
|
||||
MANA_GAMES_GITHUB_OWNER=tillschneider
|
||||
MANA_GAMES_GITHUB_REPO=mana-games
|
||||
```
|
||||
|
||||
Nach Änderungen: `pnpm setup:env`
|
||||
|
||||
## Spiel hinzufügen
|
||||
|
||||
1. HTML-Datei erstellen in `apps/web/public/games/spiel_name.html`
|
||||
2. Screenshot in `apps/web/public/screenshots/spiel-name.jpg`
|
||||
3. Registrieren in `apps/web/src/data/games.ts`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
id: '23',
|
||||
title: 'Spiel Titel',
|
||||
description: 'Beschreibung',
|
||||
slug: 'spiel-name',
|
||||
htmlFile: '/games/spiel_name.html',
|
||||
thumbnail: '/screenshots/spiel-name.jpg',
|
||||
tags: ['Arcade', 'Action'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Einfach',
|
||||
controls: 'Pfeiltasten zum Steuern'
|
||||
}
|
||||
```
|
||||
|
||||
## Spiel-postMessage Integration
|
||||
|
||||
```javascript
|
||||
// Beim Laden
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: 'spiel-slug'
|
||||
}, '*');
|
||||
|
||||
// Bei Score-Update
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'spiel-slug',
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: 123 }
|
||||
}, '*');
|
||||
|
||||
// Bei Game Over
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'spiel-slug',
|
||||
event: 'GAME_OVER',
|
||||
data: { score: 123 }
|
||||
}, '*');
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
**Farbschema:**
|
||||
- Primary Background: `#0a0a0a`
|
||||
- Secondary Background: `#1a1a1a`
|
||||
- Accent: `#00ff88`
|
||||
- Text: `#ffffff`
|
||||
- Border: `#2a2a2a`
|
||||
|
||||
## PWA
|
||||
|
||||
- Manifest: `apps/web/public/manifest.json`
|
||||
- Service Worker: `apps/web/public/sw.js`
|
||||
- Icons in `apps/web/public/icons/` (72x72 bis 512x512)
|
||||
|
||||
## Spielekatalog
|
||||
|
||||
**22 Spiele** in folgenden Genres:
|
||||
- Arcade
|
||||
- Puzzle
|
||||
- Tower Defense
|
||||
- Idle/Incremental
|
||||
- Jump 'n' Run
|
||||
- Action
|
||||
- Strategie
|
||||
8
games/mana-games/apps/backend/nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
33
games/mana-games/apps/backend/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "@mana-games/backend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"start": "nest start",
|
||||
"start:prod": "node dist/main"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@azure/openai": "^2.0.0",
|
||||
"@google/genai": "^1.14.0",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/platform-express": "^10.4.15",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.4.9",
|
||||
"@nestjs/schematics": "^10.2.3",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.10.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
17
games/mana-games/apps/backend/src/app.module.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { GameGeneratorModule } from './game-generator/game-generator.module';
|
||||
import { GameSubmissionModule } from './game-submission/game-submission.module';
|
||||
import { HealthModule } from './health/health.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
HealthModule,
|
||||
GameGeneratorModule,
|
||||
GameSubmissionModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { IsString, IsOptional, IsIn, MinLength, IsNumber } from 'class-validator';
|
||||
|
||||
export class GenerateGameDto {
|
||||
@IsString()
|
||||
@MinLength(10, { message: 'Bitte gib eine Spielbeschreibung mit mindestens 10 Zeichen ein' })
|
||||
description: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['create', 'iterate'])
|
||||
mode?: 'create' | 'iterate' = 'create';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
originalPrompt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
currentCode?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
iterationCount?: number = 0;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
model?: string = 'gemini-2.0-flash';
|
||||
}
|
||||
|
||||
export class GenerateGameResponseDto {
|
||||
success: boolean;
|
||||
html: string;
|
||||
metadata: {
|
||||
description: string;
|
||||
generatedAt: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { GameGeneratorService } from './game-generator.service';
|
||||
import { GenerateGameDto, GenerateGameResponseDto } from './dto/generate-game.dto';
|
||||
|
||||
@Controller('games')
|
||||
export class GameGeneratorController {
|
||||
constructor(private readonly gameGeneratorService: GameGeneratorService) {}
|
||||
|
||||
@Post('generate')
|
||||
async generateGame(@Body() dto: GenerateGameDto): Promise<GenerateGameResponseDto> {
|
||||
return this.gameGeneratorService.generateGame(dto);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { GameGeneratorController } from './game-generator.controller';
|
||||
import { GameGeneratorService } from './game-generator.service';
|
||||
|
||||
@Module({
|
||||
controllers: [GameGeneratorController],
|
||||
providers: [GameGeneratorService],
|
||||
})
|
||||
export class GameGeneratorModule {}
|
||||
|
|
@ -0,0 +1,331 @@
|
|||
import { Injectable, BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { GenerateGameDto, GenerateGameResponseDto } from './dto/generate-game.dto';
|
||||
import { GoogleGenAI } from '@google/genai';
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { AzureOpenAI } from '@azure/openai';
|
||||
|
||||
type AIProvider = 'google' | 'anthropic' | 'azure';
|
||||
|
||||
interface ModelConfig {
|
||||
provider: AIProvider;
|
||||
modelId: string;
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GameGeneratorService {
|
||||
private readonly logger = new Logger(GameGeneratorService.name);
|
||||
|
||||
// Model configurations
|
||||
private readonly modelConfigs: Record<string, ModelConfig> = {
|
||||
// Google Gemini Models
|
||||
'gemini-2.0-flash': { provider: 'google', modelId: 'gemini-2.0-flash', displayName: 'Gemini 2.0 Flash' },
|
||||
'gemini-2.5-flash': { provider: 'google', modelId: 'gemini-2.5-flash-preview-05-20', displayName: 'Gemini 2.5 Flash' },
|
||||
'gemini-2.5-pro': { provider: 'google', modelId: 'gemini-2.5-pro-preview-05-06', displayName: 'Gemini 2.5 Pro' },
|
||||
// Anthropic Claude Models
|
||||
'claude-3.5-haiku': { provider: 'anthropic', modelId: 'claude-3-5-haiku-20241022', displayName: 'Claude 3.5 Haiku' },
|
||||
'claude-3.5-sonnet': { provider: 'anthropic', modelId: 'claude-sonnet-4-20250514', displayName: 'Claude Sonnet 4' },
|
||||
// Azure OpenAI Models
|
||||
'gpt-4o': { provider: 'azure', modelId: 'gpt-4o', displayName: 'GPT-4o' },
|
||||
'gpt-4o-mini': { provider: 'azure', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini' },
|
||||
};
|
||||
|
||||
// AI Clients
|
||||
private googleClient: GoogleGenAI | null = null;
|
||||
private anthropicClient: Anthropic | null = null;
|
||||
private azureClient: AzureOpenAI | null = null;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
this.initializeClients();
|
||||
}
|
||||
|
||||
private initializeClients(): void {
|
||||
// Initialize Google Gemini
|
||||
const googleApiKey = this.configService.get<string>('GOOGLE_GENAI_API_KEY');
|
||||
if (googleApiKey && googleApiKey !== 'your_google_genai_key_here') {
|
||||
this.googleClient = new GoogleGenAI({ apiKey: googleApiKey });
|
||||
this.logger.log('Google Gemini client initialized');
|
||||
}
|
||||
|
||||
// Initialize Anthropic Claude
|
||||
const anthropicApiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
|
||||
if (anthropicApiKey && anthropicApiKey !== 'your_anthropic_key_here') {
|
||||
this.anthropicClient = new Anthropic({ apiKey: anthropicApiKey });
|
||||
this.logger.log('Anthropic Claude client initialized');
|
||||
}
|
||||
|
||||
// Initialize Azure OpenAI
|
||||
const azureEndpoint = this.configService.get<string>('AZURE_OPENAI_ENDPOINT');
|
||||
const azureApiKey = this.configService.get<string>('AZURE_OPENAI_API_KEY');
|
||||
if (azureEndpoint && azureApiKey && azureApiKey !== 'your_azure_openai_key_here') {
|
||||
this.azureClient = new AzureOpenAI({
|
||||
endpoint: azureEndpoint,
|
||||
apiKey: azureApiKey,
|
||||
apiVersion: '2024-08-01-preview',
|
||||
});
|
||||
this.logger.log('Azure OpenAI client initialized');
|
||||
}
|
||||
}
|
||||
|
||||
async generateGame(dto: GenerateGameDto): Promise<GenerateGameResponseDto> {
|
||||
const model = dto.model || 'gemini-2.0-flash';
|
||||
const config = this.modelConfigs[model];
|
||||
|
||||
if (!config) {
|
||||
this.logger.warn(`Unknown model: ${model}, falling back to gemini-2.0-flash`);
|
||||
return this.generateGame({ ...dto, model: 'gemini-2.0-flash' });
|
||||
}
|
||||
|
||||
// Check if the provider is available
|
||||
const providerAvailable = this.isProviderAvailable(config.provider);
|
||||
if (!providerAvailable) {
|
||||
this.logger.error(`Provider ${config.provider} is not configured`);
|
||||
throw new InternalServerErrorException(`AI provider ${config.provider} is not configured. Please add the API key.`);
|
||||
}
|
||||
|
||||
// Build prompt
|
||||
const prompt = this.createGamePrompt(
|
||||
dto.description.trim(),
|
||||
dto.mode || 'create',
|
||||
dto.originalPrompt,
|
||||
dto.currentCode,
|
||||
);
|
||||
|
||||
this.logger.log(`${dto.mode === 'iterate' ? 'Iterating' : 'Generating'} game with model: ${config.displayName} (${config.provider})`);
|
||||
|
||||
try {
|
||||
let generatedContent: string;
|
||||
|
||||
switch (config.provider) {
|
||||
case 'google':
|
||||
generatedContent = await this.generateWithGoogle(config.modelId, prompt);
|
||||
break;
|
||||
case 'anthropic':
|
||||
generatedContent = await this.generateWithAnthropic(config.modelId, prompt);
|
||||
break;
|
||||
case 'azure':
|
||||
generatedContent = await this.generateWithAzure(config.modelId, prompt);
|
||||
break;
|
||||
default:
|
||||
throw new InternalServerErrorException(`Unknown provider: ${config.provider}`);
|
||||
}
|
||||
|
||||
// Extract HTML from response
|
||||
let html = generatedContent;
|
||||
const htmlMatch = generatedContent.match(/```html\n([\s\S]*?)\n```/);
|
||||
if (htmlMatch) {
|
||||
html = htmlMatch[1];
|
||||
}
|
||||
|
||||
// Validate and sanitize
|
||||
const safeHtml = this.validateAndSanitizeGame(html);
|
||||
|
||||
this.logger.log(`Game generated successfully with ${config.displayName}`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
html: safeHtml,
|
||||
metadata: {
|
||||
description: dto.description.trim(),
|
||||
generatedAt: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
if (error instanceof BadRequestException || error instanceof InternalServerErrorException) {
|
||||
throw error;
|
||||
}
|
||||
this.logger.error(`Generation error with ${config.displayName}:`, error);
|
||||
throw new InternalServerErrorException(`Failed to generate game: ${error.message || 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
private isProviderAvailable(provider: AIProvider): boolean {
|
||||
switch (provider) {
|
||||
case 'google':
|
||||
return this.googleClient !== null;
|
||||
case 'anthropic':
|
||||
return this.anthropicClient !== null;
|
||||
case 'azure':
|
||||
return this.azureClient !== null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async generateWithGoogle(modelId: string, prompt: string): Promise<string> {
|
||||
if (!this.googleClient) {
|
||||
throw new InternalServerErrorException('Google Gemini client not initialized');
|
||||
}
|
||||
|
||||
const response = await this.googleClient.models.generateContent({
|
||||
model: modelId,
|
||||
contents: prompt,
|
||||
config: {
|
||||
temperature: 0.7,
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
});
|
||||
|
||||
const content = response.text;
|
||||
if (!content) {
|
||||
throw new InternalServerErrorException('No content generated by Google Gemini');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private async generateWithAnthropic(modelId: string, prompt: string): Promise<string> {
|
||||
if (!this.anthropicClient) {
|
||||
throw new InternalServerErrorException('Anthropic Claude client not initialized');
|
||||
}
|
||||
|
||||
const response = await this.anthropicClient.messages.create({
|
||||
model: modelId,
|
||||
max_tokens: 8192,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
|
||||
const content = response.content[0];
|
||||
if (!content || content.type !== 'text') {
|
||||
throw new InternalServerErrorException('No content generated by Anthropic Claude');
|
||||
}
|
||||
|
||||
return content.text;
|
||||
}
|
||||
|
||||
private async generateWithAzure(modelId: string, prompt: string): Promise<string> {
|
||||
if (!this.azureClient) {
|
||||
throw new InternalServerErrorException('Azure OpenAI client not initialized');
|
||||
}
|
||||
|
||||
const deployment = this.configService.get<string>('AZURE_OPENAI_DEPLOYMENT') || modelId;
|
||||
|
||||
const response = await this.azureClient.chat.completions.create({
|
||||
model: deployment,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
temperature: 0.7,
|
||||
max_tokens: 8192,
|
||||
});
|
||||
|
||||
const content = response.choices?.[0]?.message?.content;
|
||||
if (!content) {
|
||||
throw new InternalServerErrorException('No content generated by Azure OpenAI');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
getAvailableModels(): { id: string; name: string; provider: string; available: boolean }[] {
|
||||
return Object.entries(this.modelConfigs).map(([id, config]) => ({
|
||||
id,
|
||||
name: config.displayName,
|
||||
provider: config.provider,
|
||||
available: this.isProviderAvailable(config.provider),
|
||||
}));
|
||||
}
|
||||
|
||||
private createGamePrompt(
|
||||
description: string,
|
||||
mode: 'create' | 'iterate',
|
||||
originalPrompt?: string,
|
||||
currentCode?: string,
|
||||
): string {
|
||||
if (mode === 'iterate' && originalPrompt && currentCode) {
|
||||
return `Du bist ein begabter Coder und Gamedesigner.
|
||||
|
||||
Der Nutzer hat ursprünglich folgendes Spiel gewünscht: "${originalPrompt}"
|
||||
|
||||
Jetzt möchte der Nutzer folgende Änderung: "${description}"
|
||||
|
||||
ERSTELLE DAS SPIEL KOMPLETT NEU mit den gewünschten Änderungen. Orientiere dich am ursprünglichen Konzept, aber implementiere die Änderungen vollständig.
|
||||
|
||||
WICHTIGE REGELN:
|
||||
- Erstelle ein VOLLSTÄNDIGES neues HTML-Dokument
|
||||
- Maximal 400 Zeilen Code insgesamt
|
||||
- Nutze Canvas für die Grafik
|
||||
- Das Spiel muss sofort spielbar sein
|
||||
- Implementiere die gewünschten Änderungen vollständig
|
||||
- PostMessage Integration: window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
|
||||
|
||||
STRUKTUR:
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Spielname</title>
|
||||
<style>
|
||||
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
canvas { border: 1px solid #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game" width="800" height="600"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Spielcode hier mit den gewünschten Änderungen
|
||||
window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Schreibe nur den Code, keine weiteren Kommentare. Nutze keine externen Bibliotheken, Bilder oder Sounds.`;
|
||||
}
|
||||
|
||||
return `Du bist ein begabter Coder und Gamedesigner. Erstelle ein HTML5-Spiel basierend auf dieser Beschreibung: ${description}
|
||||
|
||||
WICHTIGE REGELN:
|
||||
- Maximal 400 Zeilen Code insgesamt
|
||||
- Nutze Canvas für die Grafik
|
||||
- Verwende einfache Formen (Rechtecke, Kreise, etc.)
|
||||
- Das Spiel muss sofort spielbar sein
|
||||
- Füge Steuerungshinweise im Spiel ein
|
||||
- PostMessage Integration: window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
|
||||
|
||||
STRUKTUR:
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Spielname</title>
|
||||
<style>
|
||||
body { margin: 0; background: #000; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
canvas { border: 1px solid #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="game" width="800" height="600"></canvas>
|
||||
<script>
|
||||
const canvas = document.getElementById('game');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Spielcode hier
|
||||
// PostMessage beim Start senden:
|
||||
window.parent.postMessage({type: 'GAME_LOADED', gameId: 'generated'}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Schreibe nur den Code, keine weiteren Kommentare. Nutze keine externen Bibliotheken, Bilder oder Sounds.`;
|
||||
}
|
||||
|
||||
private validateAndSanitizeGame(html: string): string {
|
||||
if (!html || typeof html !== 'string') {
|
||||
throw new BadRequestException('Invalid HTML content');
|
||||
}
|
||||
|
||||
if (!html.includes('<!DOCTYPE html>')) {
|
||||
throw new BadRequestException('Invalid game HTML structure');
|
||||
}
|
||||
|
||||
// Security sanitization
|
||||
const sanitized = html
|
||||
.replace(/<script[^>]*src=[^>]*>/gi, '')
|
||||
.replace(/<link[^>]*href=[^>]*>/gi, '')
|
||||
.replace(/fetch\s*\(/gi, '// fetch disabled: fetch(')
|
||||
.replace(/XMLHttpRequest/gi, '// XMLHttpRequest disabled')
|
||||
.replace(/eval\s*\(/gi, '// eval disabled: eval(');
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import { IsString, IsArray, IsOptional, IsObject, ValidateNested, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
class AuthorDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
email?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
github?: string;
|
||||
}
|
||||
|
||||
class FileDto {
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
content: string;
|
||||
}
|
||||
|
||||
class FilesDto {
|
||||
@ValidateNested()
|
||||
@Type(() => FileDto)
|
||||
html: FileDto;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => FileDto)
|
||||
screenshot: FileDto;
|
||||
}
|
||||
|
||||
export class SubmitGameDto {
|
||||
@IsString()
|
||||
title: string;
|
||||
|
||||
@IsString()
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
controls: string;
|
||||
|
||||
@IsIn(['Einfach', 'Mittel', 'Schwer'])
|
||||
difficulty: string;
|
||||
|
||||
@IsIn(['Minimal', 'Einfach', 'Mittel', 'Komplex'])
|
||||
complexity: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
tags: string[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => AuthorDto)
|
||||
author: AuthorDto;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => FilesDto)
|
||||
files: FilesDto;
|
||||
|
||||
@IsString()
|
||||
submittedAt: string;
|
||||
}
|
||||
|
||||
export class SubmitGameResponseDto {
|
||||
success: boolean;
|
||||
message: string;
|
||||
prUrl: string;
|
||||
prNumber: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { GameSubmissionService } from './game-submission.service';
|
||||
import { SubmitGameDto, SubmitGameResponseDto } from './dto/submit-game.dto';
|
||||
|
||||
@Controller('games')
|
||||
export class GameSubmissionController {
|
||||
constructor(private readonly gameSubmissionService: GameSubmissionService) {}
|
||||
|
||||
@Post('submit')
|
||||
async submitGame(@Body() dto: SubmitGameDto): Promise<SubmitGameResponseDto> {
|
||||
return this.gameSubmissionService.submitGame(dto);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { GameSubmissionController } from './game-submission.controller';
|
||||
import { GameSubmissionService } from './game-submission.service';
|
||||
|
||||
@Module({
|
||||
controllers: [GameSubmissionController],
|
||||
providers: [GameSubmissionService],
|
||||
})
|
||||
export class GameSubmissionModule {}
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
import { Injectable, BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { SubmitGameDto, SubmitGameResponseDto } from './dto/submit-game.dto';
|
||||
|
||||
@Injectable()
|
||||
export class GameSubmissionService {
|
||||
private readonly logger = new Logger(GameSubmissionService.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
async submitGame(dto: SubmitGameDto): Promise<SubmitGameResponseDto> {
|
||||
const githubToken = this.configService.get<string>('GITHUB_TOKEN');
|
||||
const githubOwner = this.configService.get<string>('GITHUB_OWNER') || 'tillschneider';
|
||||
const githubRepo = this.configService.get<string>('GITHUB_REPO') || 'mana-games';
|
||||
|
||||
if (!githubToken) {
|
||||
this.logger.error('GitHub token not configured');
|
||||
throw new InternalServerErrorException('Server configuration error - GitHub token missing');
|
||||
}
|
||||
|
||||
// Generate safe file names
|
||||
const gameSlug = dto.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
const timestamp = Date.now();
|
||||
const branchName = `community-game-${gameSlug}-${timestamp}`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
try {
|
||||
// 1. Get the default branch
|
||||
this.logger.log(`Fetching repo: ${githubOwner}/${githubRepo}`);
|
||||
const repoResponse = await fetch(`https://api.github.com/repos/${githubOwner}/${githubRepo}`, { headers });
|
||||
|
||||
if (!repoResponse.ok) {
|
||||
const errorBody = await repoResponse.text();
|
||||
this.logger.error('GitHub API Error:', { status: repoResponse.status, body: errorBody });
|
||||
throw new InternalServerErrorException(`Failed to fetch repository info: ${repoResponse.status}`);
|
||||
}
|
||||
|
||||
const repoData = await repoResponse.json();
|
||||
const defaultBranch = repoData.default_branch;
|
||||
|
||||
// 2. Get the latest commit SHA from the default branch
|
||||
const refResponse = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs/heads/${defaultBranch}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (!refResponse.ok) {
|
||||
throw new InternalServerErrorException('Failed to fetch branch info');
|
||||
}
|
||||
|
||||
const refData = await refResponse.json();
|
||||
const baseSha = refData.object.sha;
|
||||
|
||||
// 3. Create a new branch
|
||||
const createBranchResponse = await fetch(`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
ref: `refs/heads/${branchName}`,
|
||||
sha: baseSha,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createBranchResponse.ok) {
|
||||
throw new InternalServerErrorException('Failed to create branch');
|
||||
}
|
||||
|
||||
// 4. Prepare game data
|
||||
const nextId = String(Date.now());
|
||||
const gameData = {
|
||||
id: nextId,
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
slug: gameSlug,
|
||||
htmlFile: `/games/${gameSlug}.html`,
|
||||
thumbnail: `/screenshots/${gameSlug}.jpg`,
|
||||
tags: dto.tags,
|
||||
difficulty: dto.difficulty,
|
||||
complexity: dto.complexity,
|
||||
controls: dto.controls,
|
||||
community: true,
|
||||
author: dto.author.name,
|
||||
submittedAt: dto.submittedAt,
|
||||
};
|
||||
|
||||
// 5. Create files
|
||||
const filesToCreate = [
|
||||
{
|
||||
path: `public/games/${gameSlug}.html`,
|
||||
content: dto.files.html.content,
|
||||
encoding: 'utf-8' as const,
|
||||
},
|
||||
{
|
||||
path: `public/screenshots/${gameSlug}.jpg`,
|
||||
content: dto.files.screenshot.content.split(',')[1], // Remove data:image/jpeg;base64,
|
||||
encoding: 'base64' as const,
|
||||
},
|
||||
];
|
||||
|
||||
// Fetch existing community games
|
||||
const communityGamesPath = 'src/data/community-games.json';
|
||||
let communityGames: any[] = [];
|
||||
|
||||
try {
|
||||
const existingFileResponse = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${communityGamesPath}?ref=${defaultBranch}`,
|
||||
{ headers },
|
||||
);
|
||||
|
||||
if (existingFileResponse.ok) {
|
||||
const existingFile = await existingFileResponse.json();
|
||||
const content = Buffer.from(existingFile.content, 'base64').toString('utf-8');
|
||||
communityGames = JSON.parse(content);
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist yet
|
||||
}
|
||||
|
||||
communityGames.push(gameData);
|
||||
|
||||
filesToCreate.push({
|
||||
path: communityGamesPath,
|
||||
content: JSON.stringify(communityGames, null, 2),
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
|
||||
// Create all files
|
||||
for (const file of filesToCreate) {
|
||||
const fileContent =
|
||||
file.encoding === 'base64' ? file.content : Buffer.from(file.content).toString('base64');
|
||||
|
||||
const createFileResponse = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${file.path}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
message: `Add community game: ${dto.title}`,
|
||||
content: fileContent,
|
||||
branch: branchName,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!createFileResponse.ok) {
|
||||
const error = await createFileResponse.text();
|
||||
this.logger.error(`Failed to create file ${file.path}:`, error);
|
||||
throw new InternalServerErrorException(`Failed to create file ${file.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Create pull request
|
||||
const prBody = `## Neues Community-Spiel: ${dto.title}
|
||||
|
||||
### Spiel-Details
|
||||
- **Autor:** ${dto.author.name}${dto.author.github ? ` (@${dto.author.github})` : ''}
|
||||
- **Beschreibung:** ${dto.description}
|
||||
- **Schwierigkeit:** ${dto.difficulty}
|
||||
- **Komplexität:** ${dto.complexity}
|
||||
- **Steuerung:** ${dto.controls}
|
||||
- **Tags:** ${dto.tags.join(', ')}
|
||||
|
||||
### Dateien
|
||||
- HTML: \`public/games/${gameSlug}.html\`
|
||||
- Screenshot: \`public/screenshots/${gameSlug}.jpg\`
|
||||
|
||||
### Checkliste für Review
|
||||
- [ ] Spiel funktioniert einwandfrei
|
||||
- [ ] Keine externen Abhängigkeiten oder Sicherheitsprobleme
|
||||
- [ ] Familienfreundlicher Inhalt
|
||||
- [ ] Screenshot zeigt das Spiel korrekt
|
||||
- [ ] postMessage Integration vorhanden (optional)
|
||||
|
||||
---
|
||||
*Eingereicht am: ${new Date(dto.submittedAt).toLocaleString('de-DE')}*
|
||||
${dto.author.email ? `*Kontakt: ${dto.author.email}*` : ''}`;
|
||||
|
||||
const prResponse = await fetch(`https://api.github.com/repos/${githubOwner}/${githubRepo}/pulls`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
title: `Community: ${dto.title}`,
|
||||
body: prBody,
|
||||
head: branchName,
|
||||
base: defaultBranch,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!prResponse.ok) {
|
||||
const error = await prResponse.text();
|
||||
this.logger.error('Failed to create PR:', error);
|
||||
throw new InternalServerErrorException('Failed to create pull request');
|
||||
}
|
||||
|
||||
const prData = await prResponse.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Game submitted successfully',
|
||||
prUrl: prData.html_url,
|
||||
prNumber: prData.number,
|
||||
};
|
||||
} catch (error: any) {
|
||||
this.logger.error('Submission error:', error);
|
||||
if (error instanceof BadRequestException || error instanceof InternalServerErrorException) {
|
||||
throw error;
|
||||
}
|
||||
throw new InternalServerErrorException('Failed to submit game: ' + (error.message || 'Unknown error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
@Get()
|
||||
check() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
service: 'mana-games-backend',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
37
games/mana-games/apps/backend/src/main.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// CORS configuration
|
||||
app.enableCors({
|
||||
origin: [
|
||||
'http://localhost:4321', // Astro dev
|
||||
'http://localhost:3000', // Alternative dev
|
||||
/\.netlify\.app$/, // Legacy Netlify
|
||||
],
|
||||
methods: ['GET', 'POST', 'OPTIONS'],
|
||||
credentials: false,
|
||||
});
|
||||
|
||||
app.setGlobalPrefix('api');
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const port = process.env.PORT || 3010;
|
||||
|
||||
// Increase timeout for long-running AI requests (2 minutes)
|
||||
const server = await app.listen(port);
|
||||
server.setTimeout(120000);
|
||||
|
||||
console.log(`Mana Games backend running on http://localhost:${port}`);
|
||||
}
|
||||
bootstrap();
|
||||
22
games/mana-games/apps/backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
11
games/mana-games/apps/web/.env.example
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# OpenRouter API Key
|
||||
# Get your API key from https://openrouter.ai/keys
|
||||
OPENROUTER_API_KEY=your_api_key_here
|
||||
|
||||
# GitHub API Token (for community submissions)
|
||||
# Create a personal access token with 'repo' scope at https://github.com/settings/tokens
|
||||
GITHUB_TOKEN=your_github_token_here
|
||||
|
||||
# GitHub Repository Settings (optional - defaults to current repo)
|
||||
GITHUB_OWNER=your_github_username
|
||||
GITHUB_REPO=mana-games
|
||||
5
games/mana-games/apps/web/astro.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({});
|
||||
18
games/mana-games/apps/web/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "@mana-games/web",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.10.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sharp": "^0.34.2"
|
||||
}
|
||||
}
|
||||
5
games/mana-games/apps/web/public/favicon.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect x="0" y="0" width="32" height="32" rx="6" fill="#0a0a0a"/>
|
||||
<path d="M8 12 L8 20 L10 20 L10 16 L12 20 L14 20 L16 16 L16 20 L18 20 L18 12 L15 12 L13 16 L11 12 Z" fill="#ffffff"/>
|
||||
<path d="M20 16 Q20 12 24 12 L24 14 Q22 14 22 16 Q22 18 24 18 Q24 16 26 16 L26 18 Q26 20 22 20 Q20 20 20 16 Z" fill="#00ff88"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 385 B |
666
games/mana-games/apps/web/public/games/asteroid_dash.html
Normal file
|
|
@ -0,0 +1,666 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Asteroid Dash</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #000814;
|
||||
color: #fff;
|
||||
font-family: 'Courier New', monospace;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 2px solid #001d3d;
|
||||
background: radial-gradient(circle at 30% 20%, #001d3d 0%, #000814 70%);
|
||||
box-shadow: 0 0 30px rgba(0, 123, 255, 0.3);
|
||||
}
|
||||
|
||||
.ui {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 18px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #00f5ff;
|
||||
text-shadow: 0 0 10px rgba(0, 245, 255, 0.5);
|
||||
}
|
||||
|
||||
.lives {
|
||||
color: #ff0080;
|
||||
margin-top: 10px;
|
||||
text-shadow: 0 0 10px rgba(255, 0, 128, 0.5);
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 30px;
|
||||
border: 2px solid #00f5ff;
|
||||
border-radius: 10px;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #001d3d;
|
||||
color: #00f5ff;
|
||||
border: 2px solid #00f5ff;
|
||||
padding: 10px 20px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
border-radius: 5px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #00f5ff;
|
||||
color: #000814;
|
||||
box-shadow: 0 0 15px rgba(0, 245, 255, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
|
||||
<div class="ui">
|
||||
<div class="score">Score: <span id="score">0</span></div>
|
||||
<div class="lives">Lives: <span id="lives">3</span></div>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
WASD / Pfeiltasten: Bewegung | Leertaste: Boost
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>Game Over!</h2>
|
||||
<p>Endpunktzahl: <span id="finalScore">0</span></p>
|
||||
<button onclick="restartGame()">Nochmal spielen</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'asteroid-dash';
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Spielvariablen
|
||||
let gameRunning = true;
|
||||
let score = 0;
|
||||
let lives = 3;
|
||||
let stars = [];
|
||||
|
||||
// Eingabe-System
|
||||
const keys = {};
|
||||
|
||||
// Spieler-Objekt
|
||||
const player = {
|
||||
x: canvas.width / 2,
|
||||
y: canvas.height / 2,
|
||||
width: 20,
|
||||
height: 15,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
speed: 0.3,
|
||||
maxSpeed: 8,
|
||||
friction: 0.95,
|
||||
boost: false,
|
||||
boostCooldown: 0,
|
||||
invulnerable: 0,
|
||||
trail: []
|
||||
};
|
||||
|
||||
// Asteroid-Array
|
||||
const asteroids = [];
|
||||
|
||||
// Kristall-Array
|
||||
const crystals = [];
|
||||
|
||||
// Power-up Array
|
||||
const powerups = [];
|
||||
|
||||
// Partikel-Array
|
||||
const particles = [];
|
||||
|
||||
// Sterne für Hintergrund erzeugen
|
||||
function createStars() {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
stars.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
size: Math.random() * 2,
|
||||
brightness: Math.random()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Asteroid erstellen
|
||||
function createAsteroid() {
|
||||
const side = Math.floor(Math.random() * 4);
|
||||
let x, y;
|
||||
|
||||
switch(side) {
|
||||
case 0: x = -30; y = Math.random() * canvas.height; break;
|
||||
case 1: x = canvas.width + 30; y = Math.random() * canvas.height; break;
|
||||
case 2: x = Math.random() * canvas.width; y = -30; break;
|
||||
case 3: x = Math.random() * canvas.width; y = canvas.height + 30; break;
|
||||
}
|
||||
|
||||
const asteroid = {
|
||||
x: x,
|
||||
y: y,
|
||||
size: 15 + Math.random() * 25,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: (Math.random() - 0.5) * 4,
|
||||
rotation: 0,
|
||||
rotationSpeed: (Math.random() - 0.5) * 0.1,
|
||||
color: `hsl(${20 + Math.random() * 40}, 70%, ${40 + Math.random() * 20}%)`
|
||||
};
|
||||
|
||||
asteroids.push(asteroid);
|
||||
}
|
||||
|
||||
// Kristall erstellen
|
||||
function createCrystal() {
|
||||
crystals.push({
|
||||
x: Math.random() * (canvas.width - 40) + 20,
|
||||
y: Math.random() * (canvas.height - 40) + 20,
|
||||
size: 8,
|
||||
rotation: 0,
|
||||
rotationSpeed: 0.05,
|
||||
pulse: 0,
|
||||
collected: false
|
||||
});
|
||||
}
|
||||
|
||||
// Power-up erstellen
|
||||
function createPowerup() {
|
||||
const types = ['shield', 'boost', 'magnet'];
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
|
||||
powerups.push({
|
||||
x: Math.random() * (canvas.width - 40) + 20,
|
||||
y: Math.random() * (canvas.height - 40) + 20,
|
||||
type: type,
|
||||
size: 12,
|
||||
rotation: 0,
|
||||
life: 300
|
||||
});
|
||||
}
|
||||
|
||||
// Partikel erstellen
|
||||
function createParticles(x, y, count, color) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: (Math.random() - 0.5) * 10,
|
||||
vy: (Math.random() - 0.5) * 10,
|
||||
size: Math.random() * 3 + 1,
|
||||
life: 30,
|
||||
maxLife: 30,
|
||||
color: color || '#00f5ff'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Kollisionserkennung
|
||||
function checkCollision(rect1, rect2) {
|
||||
return rect1.x < rect2.x + rect2.size &&
|
||||
rect1.x + rect1.width > rect2.x &&
|
||||
rect1.y < rect2.y + rect2.size &&
|
||||
rect1.y + rect1.height > rect2.y;
|
||||
}
|
||||
|
||||
// Distanz zwischen zwei Punkten
|
||||
function distance(x1, y1, x2, y2) {
|
||||
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
}
|
||||
|
||||
// Eingabe-Event-Listener
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
if (e.key === ' ') {
|
||||
e.preventDefault();
|
||||
if (player.boostCooldown <= 0) {
|
||||
player.boost = true;
|
||||
player.boostCooldown = 60;
|
||||
createParticles(player.x + player.width/2, player.y + player.height, 5, '#ff6b00');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
if (e.key === ' ') {
|
||||
player.boost = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Spieler updaten
|
||||
function updatePlayer() {
|
||||
// Bewegung
|
||||
if (keys['w'] || keys['arrowup']) player.vy -= player.speed;
|
||||
if (keys['s'] || keys['arrowdown']) player.vy += player.speed;
|
||||
if (keys['a'] || keys['arrowleft']) player.vx -= player.speed;
|
||||
if (keys['d'] || keys['arrowright']) player.vx += player.speed;
|
||||
|
||||
// Boost
|
||||
let currentMaxSpeed = player.maxSpeed;
|
||||
if (player.boost) {
|
||||
currentMaxSpeed *= 2;
|
||||
}
|
||||
|
||||
// Geschwindigkeit begrenzen
|
||||
const speed = Math.sqrt(player.vx ** 2 + player.vy ** 2);
|
||||
if (speed > currentMaxSpeed) {
|
||||
player.vx = (player.vx / speed) * currentMaxSpeed;
|
||||
player.vy = (player.vy / speed) * currentMaxSpeed;
|
||||
}
|
||||
|
||||
// Reibung
|
||||
player.vx *= player.friction;
|
||||
player.vy *= player.friction;
|
||||
|
||||
// Position updaten
|
||||
player.x += player.vx;
|
||||
player.y += player.vy;
|
||||
|
||||
// Bildschirmgrenzen
|
||||
if (player.x < 0) player.x = 0;
|
||||
if (player.x + player.width > canvas.width) player.x = canvas.width - player.width;
|
||||
if (player.y < 0) player.y = 0;
|
||||
if (player.y + player.height > canvas.height) player.y = canvas.height - player.height;
|
||||
|
||||
// Cooldowns
|
||||
if (player.boostCooldown > 0) player.boostCooldown--;
|
||||
if (player.invulnerable > 0) player.invulnerable--;
|
||||
|
||||
// Trail für Boost-Effekt
|
||||
if (player.boost) {
|
||||
player.trail.push({
|
||||
x: player.x + player.width/2,
|
||||
y: player.y + player.height/2,
|
||||
life: 10
|
||||
});
|
||||
}
|
||||
|
||||
// Trail updaten
|
||||
player.trail = player.trail.filter(t => {
|
||||
t.life--;
|
||||
return t.life > 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Asteroiden updaten
|
||||
function updateAsteroids() {
|
||||
for (let i = asteroids.length - 1; i >= 0; i--) {
|
||||
const asteroid = asteroids[i];
|
||||
|
||||
asteroid.x += asteroid.vx;
|
||||
asteroid.y += asteroid.vy;
|
||||
asteroid.rotation += asteroid.rotationSpeed;
|
||||
|
||||
// Asteroiden entfernen die zu weit weg sind
|
||||
if (asteroid.x < -100 || asteroid.x > canvas.width + 100 ||
|
||||
asteroid.y < -100 || asteroid.y > canvas.height + 100) {
|
||||
asteroids.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kollision mit Spieler
|
||||
if (player.invulnerable <= 0 &&
|
||||
distance(player.x + player.width/2, player.y + player.height/2,
|
||||
asteroid.x, asteroid.y) < asteroid.size + 10) {
|
||||
player.invulnerable = 120;
|
||||
lives--;
|
||||
createParticles(player.x + player.width/2, player.y + player.height/2, 10, '#ff0080');
|
||||
|
||||
if (lives <= 0) {
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kristalle updaten
|
||||
function updateCrystals() {
|
||||
for (let i = crystals.length - 1; i >= 0; i--) {
|
||||
const crystal = crystals[i];
|
||||
|
||||
crystal.rotation += crystal.rotationSpeed;
|
||||
crystal.pulse += 0.1;
|
||||
|
||||
// Kollision mit Spieler
|
||||
if (distance(player.x + player.width/2, player.y + player.height/2,
|
||||
crystal.x, crystal.y) < crystal.size + 10) {
|
||||
score += 100;
|
||||
createParticles(crystal.x, crystal.y, 8, '#00ff80');
|
||||
crystals.splice(i, 1);
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power-ups updaten
|
||||
function updatePowerups() {
|
||||
for (let i = powerups.length - 1; i >= 0; i--) {
|
||||
const powerup = powerups[i];
|
||||
|
||||
powerup.rotation += 0.03;
|
||||
powerup.life--;
|
||||
|
||||
if (powerup.life <= 0) {
|
||||
powerups.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kollision mit Spieler
|
||||
if (distance(player.x + player.width/2, player.y + player.height/2,
|
||||
powerup.x, powerup.y) < powerup.size + 10) {
|
||||
|
||||
if (powerup.type === 'shield') {
|
||||
player.invulnerable = 180;
|
||||
} else if (powerup.type === 'boost') {
|
||||
player.boostCooldown = 0;
|
||||
}
|
||||
|
||||
createParticles(powerup.x, powerup.y, 6, '#ffff00');
|
||||
powerups.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Partikel updaten
|
||||
function updateParticles() {
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const particle = particles[i];
|
||||
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.vx *= 0.98;
|
||||
particle.vy *= 0.98;
|
||||
particle.life--;
|
||||
|
||||
if (particle.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeichnen
|
||||
function draw() {
|
||||
// Hintergrund löschen
|
||||
ctx.fillStyle = '#000814';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Sterne zeichnen
|
||||
ctx.fillStyle = '#ffffff';
|
||||
for (const star of stars) {
|
||||
ctx.globalAlpha = star.brightness;
|
||||
ctx.fillRect(star.x, star.y, star.size, star.size);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Spieler-Trail zeichnen
|
||||
for (const trail of player.trail) {
|
||||
ctx.globalAlpha = trail.life / 10;
|
||||
ctx.fillStyle = '#ff6b00';
|
||||
ctx.fillRect(trail.x - 2, trail.y - 2, 4, 4);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Spieler zeichnen
|
||||
ctx.save();
|
||||
ctx.translate(player.x + player.width/2, player.y + player.height/2);
|
||||
|
||||
if (player.invulnerable > 0 && Math.floor(player.invulnerable / 5) % 2) {
|
||||
ctx.globalAlpha = 0.5;
|
||||
}
|
||||
|
||||
ctx.fillStyle = player.boost ? '#ff6b00' : '#00f5ff';
|
||||
ctx.fillRect(-player.width/2, -player.height/2, player.width, player.height);
|
||||
|
||||
// Spieler-Spitze
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(-2, -player.height/2 - 3, 4, 3);
|
||||
|
||||
ctx.restore();
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Asteroiden zeichnen
|
||||
for (const asteroid of asteroids) {
|
||||
ctx.save();
|
||||
ctx.translate(asteroid.x, asteroid.y);
|
||||
ctx.rotate(asteroid.rotation);
|
||||
|
||||
ctx.fillStyle = asteroid.color;
|
||||
ctx.fillRect(-asteroid.size/2, -asteroid.size/2, asteroid.size, asteroid.size);
|
||||
|
||||
// Dunkle Kanten für 3D-Effekt
|
||||
ctx.fillStyle = '#2d1810';
|
||||
ctx.fillRect(-asteroid.size/2, -asteroid.size/2, asteroid.size, 3);
|
||||
ctx.fillRect(-asteroid.size/2, -asteroid.size/2, 3, asteroid.size);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Kristalle zeichnen
|
||||
for (const crystal of crystals) {
|
||||
ctx.save();
|
||||
ctx.translate(crystal.x, crystal.y);
|
||||
ctx.rotate(crystal.rotation);
|
||||
|
||||
const pulseSize = crystal.size + Math.sin(crystal.pulse) * 2;
|
||||
|
||||
ctx.fillStyle = '#00ff80';
|
||||
ctx.fillRect(-pulseSize/2, -pulseSize/2, pulseSize, pulseSize);
|
||||
|
||||
// Glitzer-Effekt
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(-2, -2, 4, 4);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Power-ups zeichnen
|
||||
for (const powerup of powerups) {
|
||||
ctx.save();
|
||||
ctx.translate(powerup.x, powerup.y);
|
||||
ctx.rotate(powerup.rotation);
|
||||
|
||||
let color = '#ffff00';
|
||||
if (powerup.type === 'shield') color = '#ff00ff';
|
||||
if (powerup.type === 'boost') color = '#ff6b00';
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(-powerup.size/2, -powerup.size/2, powerup.size, powerup.size);
|
||||
|
||||
// Symbol
|
||||
ctx.fillStyle = '#000000';
|
||||
if (powerup.type === 'shield') {
|
||||
ctx.fillRect(-4, -6, 8, 12);
|
||||
} else if (powerup.type === 'boost') {
|
||||
ctx.fillRect(-2, -6, 4, 12);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Partikel zeichnen
|
||||
for (const particle of particles) {
|
||||
ctx.globalAlpha = particle.life / particle.maxLife;
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.fillRect(particle.x - particle.size/2, particle.y - particle.size/2,
|
||||
particle.size, particle.size);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Spawn-System
|
||||
let asteroidSpawnTimer = 0;
|
||||
let crystalSpawnTimer = 0;
|
||||
let powerupSpawnTimer = 0;
|
||||
|
||||
function handleSpawning() {
|
||||
asteroidSpawnTimer++;
|
||||
crystalSpawnTimer++;
|
||||
powerupSpawnTimer++;
|
||||
|
||||
// Asteroiden spawnen (Schwierigkeit steigt)
|
||||
const asteroidDelay = Math.max(30, 120 - Math.floor(score / 500));
|
||||
if (asteroidSpawnTimer >= asteroidDelay) {
|
||||
createAsteroid();
|
||||
asteroidSpawnTimer = 0;
|
||||
}
|
||||
|
||||
// Kristalle spawnen
|
||||
if (crystalSpawnTimer >= 180 && crystals.length < 3) {
|
||||
createCrystal();
|
||||
crystalSpawnTimer = 0;
|
||||
}
|
||||
|
||||
// Power-ups spawnen
|
||||
if (powerupSpawnTimer >= 600 && powerups.length < 1) {
|
||||
createPowerup();
|
||||
powerupSpawnTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Spiel-Loop
|
||||
function gameLoop() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
updatePlayer();
|
||||
updateAsteroids();
|
||||
updateCrystals();
|
||||
updatePowerups();
|
||||
updateParticles();
|
||||
handleSpawning();
|
||||
|
||||
draw();
|
||||
|
||||
// UI updaten
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('lives').textContent = lives;
|
||||
|
||||
score++;
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Game Over
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
document.getElementById('gameOver').style.display = 'block';
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 5000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'asteroid_survivor',
|
||||
name: 'Asteroid Survivor',
|
||||
description: 'Score 5000 points in Asteroid Dash',
|
||||
icon: '🚀'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (score >= 10000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'space_ace',
|
||||
name: 'Space Ace',
|
||||
description: 'Score 10000 points in Asteroid Dash',
|
||||
icon: '⭐'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Spiel neustarten
|
||||
function restartGame() {
|
||||
gameRunning = true;
|
||||
score = 0;
|
||||
lives = 3;
|
||||
|
||||
// Arrays leeren
|
||||
asteroids.length = 0;
|
||||
crystals.length = 0;
|
||||
powerups.length = 0;
|
||||
particles.length = 0;
|
||||
|
||||
// Spieler zurücksetzen
|
||||
player.x = canvas.width / 2;
|
||||
player.y = canvas.height / 2;
|
||||
player.vx = 0;
|
||||
player.vy = 0;
|
||||
player.invulnerable = 60;
|
||||
player.boostCooldown = 0;
|
||||
player.trail.length = 0;
|
||||
|
||||
// Timer zurücksetzen
|
||||
asteroidSpawnTimer = 0;
|
||||
crystalSpawnTimer = 0;
|
||||
powerupSpawnTimer = 0;
|
||||
|
||||
document.getElementById('gameOver').style.display = 'none';
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Spiel initialisieren
|
||||
createStars();
|
||||
gameLoop();
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
677
games/mana-games/apps/web/public/games/balloon_pop.html
Normal file
|
|
@ -0,0 +1,677 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Balloon Pop</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: linear-gradient(180deg, #87CEEB 0%, #98FB98 50%, #90EE90 100%);
|
||||
color: #fff;
|
||||
font-family: 'Comic Sans MS', cursive;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #fff;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 25px rgba(0, 0, 0, 0.3);
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.ui {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 22px;
|
||||
z-index: 10;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #ff6b35;
|
||||
font-weight: bold;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.level {
|
||||
color: #4CAF50;
|
||||
margin-top: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.combo {
|
||||
color: #ff1744;
|
||||
margin-top: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
background: rgba(50, 150, 250, 0.95);
|
||||
padding: 30px;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 25px;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(145deg, #ff6b35, #ff8c42);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.powerup-ui {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-size: 16px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.powerup-active {
|
||||
color: #ffff00;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
|
||||
<div class="ui">
|
||||
<div class="score">🎈 Punkte: <span id="score">0</span></div>
|
||||
<div class="level">Level: <span id="level">1</span></div>
|
||||
<div class="combo">Combo: <span id="combo">0</span>x</div>
|
||||
</div>
|
||||
|
||||
<div class="powerup-ui" id="powerupStatus">
|
||||
<!-- Power-up Status wird hier angezeigt -->
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
🖱️ Klicke auf die Ballons zum Platzen lassen!
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>🎉 Spiel beendet!</h2>
|
||||
<p>Endpunktzahl: <span id="finalScore">0</span></p>
|
||||
<p>Erreichte Level: <span id="finalLevel">1</span></p>
|
||||
<p id="achievement"></p>
|
||||
<button onclick="restartGame()">🔄 Nochmal spielen</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'balloon-pop';
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Spiel-Zustand
|
||||
let gameRunning = true;
|
||||
let score = 0;
|
||||
let level = 1;
|
||||
let combo = 0;
|
||||
let comboTimer = 0;
|
||||
let balloonsMissed = 0;
|
||||
let maxMissed = 10;
|
||||
|
||||
// Power-ups
|
||||
let multiShotActive = false;
|
||||
let multiShotTimer = 0;
|
||||
let slowTimeActive = false;
|
||||
let slowTimeTimer = 0;
|
||||
|
||||
// Arrays für Spielobjekte
|
||||
const balloons = [];
|
||||
const particles = [];
|
||||
const clouds = [];
|
||||
const powerups = [];
|
||||
|
||||
// Ballon-Typen
|
||||
const balloonTypes = {
|
||||
normal: { color: '#ff6b35', points: 10, speed: 1 },
|
||||
fast: { color: '#ff1744', points: 20, speed: 2 },
|
||||
big: { color: '#9c27b0', points: 30, speed: 0.7, size: 1.5 },
|
||||
bonus: { color: '#ffff00', points: 50, speed: 1.2 },
|
||||
bomb: { color: '#424242', points: -20, speed: 1.5 }
|
||||
};
|
||||
|
||||
// Wolken für Hintergrund erstellen
|
||||
function createClouds() {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
clouds.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * 200,
|
||||
size: 40 + Math.random() * 60,
|
||||
speed: 0.2 + Math.random() * 0.3,
|
||||
opacity: 0.6 + Math.random() * 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Ballon erstellen
|
||||
function createBalloon() {
|
||||
const typeNames = Object.keys(balloonTypes);
|
||||
let typeName;
|
||||
|
||||
// Wahrscheinlichkeiten basierend auf Level
|
||||
const rand = Math.random();
|
||||
if (rand < 0.5) typeName = 'normal';
|
||||
else if (rand < 0.7) typeName = 'fast';
|
||||
else if (rand < 0.85) typeName = 'big';
|
||||
else if (rand < 0.95) typeName = 'bonus';
|
||||
else typeName = 'bomb';
|
||||
|
||||
const type = balloonTypes[typeName];
|
||||
const baseSize = 25;
|
||||
const size = baseSize * (type.size || 1);
|
||||
|
||||
balloons.push({
|
||||
x: Math.random() * (canvas.width - size * 2) + size,
|
||||
y: canvas.height + size,
|
||||
size: size,
|
||||
type: typeName,
|
||||
color: type.color,
|
||||
points: type.points,
|
||||
speed: type.speed * (slowTimeActive ? 0.5 : 1),
|
||||
wiggle: Math.random() * Math.PI * 2,
|
||||
wiggleSpeed: 0.02 + Math.random() * 0.02,
|
||||
string: true
|
||||
});
|
||||
}
|
||||
|
||||
// Power-up erstellen
|
||||
function createPowerup() {
|
||||
const types = ['multishot', 'slowtime'];
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
|
||||
powerups.push({
|
||||
x: Math.random() * (canvas.width - 30) + 15,
|
||||
y: canvas.height + 15,
|
||||
size: 20,
|
||||
type: type,
|
||||
speed: 0.8,
|
||||
rotation: 0,
|
||||
pulse: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Partikel-Effekt erstellen
|
||||
function createParticles(x, y, color, count = 8) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: (Math.random() - 0.5) * 12,
|
||||
vy: (Math.random() - 0.5) * 12 - 5,
|
||||
size: Math.random() * 4 + 2,
|
||||
life: 30,
|
||||
maxLife: 30,
|
||||
color: color,
|
||||
gravity: 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Maus-Klick Event
|
||||
canvas.addEventListener('click', (e) => {
|
||||
if (!gameRunning) return;
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mouseX = e.clientX - rect.left;
|
||||
const mouseY = e.clientY - rect.top;
|
||||
|
||||
let hit = false;
|
||||
|
||||
// Prüfe Ballons
|
||||
for (let i = balloons.length - 1; i >= 0; i--) {
|
||||
const balloon = balloons[i];
|
||||
const distance = Math.sqrt(
|
||||
(mouseX - balloon.x) ** 2 + (mouseY - balloon.y) ** 2
|
||||
);
|
||||
|
||||
if (distance < balloon.size) {
|
||||
// Ballon getroffen
|
||||
hit = true;
|
||||
|
||||
if (balloon.type === 'bomb') {
|
||||
// Bombe - negative Punkte
|
||||
score += balloon.points;
|
||||
combo = 0;
|
||||
createParticles(balloon.x, balloon.y, '#ff0000', 12);
|
||||
} else {
|
||||
// Normaler Ballon
|
||||
combo++;
|
||||
comboTimer = 180; // 3 Sekunden
|
||||
const comboBonus = Math.floor(combo / 5);
|
||||
score += balloon.points + (comboBonus * 5);
|
||||
createParticles(balloon.x, balloon.y, balloon.color, 10);
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
}
|
||||
|
||||
balloons.splice(i, 1);
|
||||
|
||||
// Multi-Shot: nur ein Ballon pro Klick
|
||||
if (!multiShotActive) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe Power-ups
|
||||
for (let i = powerups.length - 1; i >= 0; i--) {
|
||||
const powerup = powerups[i];
|
||||
const distance = Math.sqrt(
|
||||
(mouseX - powerup.x) ** 2 + (mouseY - powerup.y) ** 2
|
||||
);
|
||||
|
||||
if (distance < powerup.size) {
|
||||
if (powerup.type === 'multishot') {
|
||||
multiShotActive = true;
|
||||
multiShotTimer = 300; // 5 Sekunden
|
||||
} else if (powerup.type === 'slowtime') {
|
||||
slowTimeActive = true;
|
||||
slowTimeTimer = 600; // 10 Sekunden
|
||||
}
|
||||
|
||||
createParticles(powerup.x, powerup.y, '#ffff00', 6);
|
||||
powerups.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Combo-Timer zurücksetzen wenn kein Treffer
|
||||
if (!hit) {
|
||||
combo = Math.max(0, combo - 1);
|
||||
}
|
||||
});
|
||||
|
||||
// Ballons updaten
|
||||
function updateBalloons() {
|
||||
for (let i = balloons.length - 1; i >= 0; i--) {
|
||||
const balloon = balloons[i];
|
||||
|
||||
// Bewegung
|
||||
const currentSpeed = balloon.speed * (slowTimeActive ? 0.5 : 1);
|
||||
balloon.y -= currentSpeed;
|
||||
balloon.wiggle += balloon.wiggleSpeed;
|
||||
balloon.x += Math.sin(balloon.wiggle) * 0.8;
|
||||
|
||||
// Ballon entkommen
|
||||
if (balloon.y + balloon.size < 0) {
|
||||
balloons.splice(i, 1);
|
||||
balloonsMissed++;
|
||||
combo = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power-ups updaten
|
||||
function updatePowerups() {
|
||||
for (let i = powerups.length - 1; i >= 0; i--) {
|
||||
const powerup = powerups[i];
|
||||
|
||||
powerup.y -= powerup.speed;
|
||||
powerup.rotation += 0.1;
|
||||
powerup.pulse += 0.15;
|
||||
|
||||
if (powerup.y + powerup.size < 0) {
|
||||
powerups.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Power-up Timer
|
||||
if (multiShotTimer > 0) {
|
||||
multiShotTimer--;
|
||||
if (multiShotTimer <= 0) {
|
||||
multiShotActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (slowTimeTimer > 0) {
|
||||
slowTimeTimer--;
|
||||
if (slowTimeTimer <= 0) {
|
||||
slowTimeActive = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wolken updaten
|
||||
function updateClouds() {
|
||||
for (const cloud of clouds) {
|
||||
cloud.x += cloud.speed;
|
||||
if (cloud.x > canvas.width + cloud.size) {
|
||||
cloud.x = -cloud.size;
|
||||
cloud.y = Math.random() * 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Partikel updaten
|
||||
function updateParticles() {
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const particle = particles[i];
|
||||
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.vy += particle.gravity;
|
||||
particle.life--;
|
||||
|
||||
if (particle.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeichnen
|
||||
function draw() {
|
||||
// Himmel-Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
gradient.addColorStop(0, '#87CEEB');
|
||||
gradient.addColorStop(0.5, '#98FB98');
|
||||
gradient.addColorStop(1, '#90EE90');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Wolken zeichnen
|
||||
for (const cloud of clouds) {
|
||||
ctx.globalAlpha = cloud.opacity;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
|
||||
// Einfache Wolken-Form
|
||||
ctx.beginPath();
|
||||
ctx.arc(cloud.x, cloud.y, cloud.size * 0.5, 0, Math.PI * 2);
|
||||
ctx.arc(cloud.x + cloud.size * 0.3, cloud.y, cloud.size * 0.4, 0, Math.PI * 2);
|
||||
ctx.arc(cloud.x - cloud.size * 0.3, cloud.y, cloud.size * 0.4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Ballons zeichnen
|
||||
for (const balloon of balloons) {
|
||||
// Ballon-String
|
||||
if (balloon.string) {
|
||||
ctx.strokeStyle = '#8B4513';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(balloon.x, balloon.y + balloon.size);
|
||||
ctx.lineTo(balloon.x, balloon.y + balloon.size + 30);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Ballon-Körper
|
||||
ctx.fillStyle = balloon.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(balloon.x, balloon.y, balloon.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Ballon-Highlight
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.beginPath();
|
||||
ctx.arc(balloon.x - balloon.size * 0.3, balloon.y - balloon.size * 0.3,
|
||||
balloon.size * 0.2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Spezielle Markierungen
|
||||
if (balloon.type === 'bomb') {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '16px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('💣', balloon.x, balloon.y + 5);
|
||||
} else if (balloon.type === 'bonus') {
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText('★', balloon.x, balloon.y + 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Power-ups zeichnen
|
||||
for (const powerup of powerups) {
|
||||
ctx.save();
|
||||
ctx.translate(powerup.x, powerup.y);
|
||||
ctx.rotate(powerup.rotation);
|
||||
|
||||
const pulseSize = powerup.size + Math.sin(powerup.pulse) * 3;
|
||||
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
ctx.fillRect(-pulseSize/2, -pulseSize/2, pulseSize, pulseSize);
|
||||
|
||||
// Symbol
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
if (powerup.type === 'multishot') {
|
||||
ctx.fillText('⚡', 0, 4);
|
||||
} else {
|
||||
ctx.fillText('⏰', 0, 4);
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Partikel zeichnen
|
||||
for (const particle of particles) {
|
||||
ctx.globalAlpha = particle.life / particle.maxLife;
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Slow-Time Effekt
|
||||
if (slowTimeActive) {
|
||||
ctx.fillStyle = 'rgba(100, 200, 255, 0.1)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn-System
|
||||
let balloonSpawnTimer = 0;
|
||||
let powerupSpawnTimer = 0;
|
||||
|
||||
function handleSpawning() {
|
||||
balloonSpawnTimer++;
|
||||
powerupSpawnTimer++;
|
||||
|
||||
// Ballons spawnen (Häufigkeit steigt mit Level)
|
||||
const spawnRate = Math.max(30, 80 - level * 3);
|
||||
if (balloonSpawnTimer >= spawnRate) {
|
||||
createBalloon();
|
||||
balloonSpawnTimer = 0;
|
||||
}
|
||||
|
||||
// Power-ups spawnen
|
||||
if (powerupSpawnTimer >= 900 && powerups.length < 1) {
|
||||
createPowerup();
|
||||
powerupSpawnTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Spiel-Loop
|
||||
function gameLoop() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
updateBalloons();
|
||||
updatePowerups();
|
||||
updateClouds();
|
||||
updateParticles();
|
||||
handleSpawning();
|
||||
|
||||
// Combo-Timer
|
||||
if (comboTimer > 0) {
|
||||
comboTimer--;
|
||||
if (comboTimer <= 0) {
|
||||
combo = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Level-System
|
||||
const newLevel = Math.floor(score / 500) + 1;
|
||||
if (newLevel > level) {
|
||||
level = newLevel;
|
||||
// Bonus für Level-Up
|
||||
score += level * 50;
|
||||
createParticles(canvas.width/2, canvas.height/2, '#ffff00', 15);
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
// UI updaten
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('level').textContent = level;
|
||||
document.getElementById('combo').textContent = combo;
|
||||
|
||||
// Power-up Status
|
||||
const powerupStatus = document.getElementById('powerupStatus');
|
||||
let statusText = '';
|
||||
if (multiShotActive) {
|
||||
statusText += `⚡ Multi-Shot: ${Math.ceil(multiShotTimer / 60)}s<br>`;
|
||||
}
|
||||
if (slowTimeActive) {
|
||||
statusText += `⏰ Slow-Time: ${Math.ceil(slowTimeTimer / 60)}s`;
|
||||
}
|
||||
powerupStatus.innerHTML = statusText;
|
||||
|
||||
// Spiel beenden
|
||||
if (balloonsMissed >= maxMissed) {
|
||||
gameOver();
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Game Over
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
document.getElementById('finalLevel').textContent = level;
|
||||
|
||||
let achievement = '';
|
||||
if (score >= 2000) achievement = '🏆 Ballon-Meister!';
|
||||
else if (score >= 1000) achievement = '🥈 Profi-Platzer!';
|
||||
else if (score >= 500) achievement = '🥉 Guter Start!';
|
||||
else achievement = '🎈 Weiter üben!';
|
||||
|
||||
document.getElementById('achievement').textContent = achievement;
|
||||
document.getElementById('gameOver').style.display = 'block';
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 2000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'balloon_master',
|
||||
name: 'Balloon Master',
|
||||
description: 'Score 2000 points in Balloon Pop',
|
||||
icon: '🏆'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (combo >= 20) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'combo_popper',
|
||||
name: 'Combo Popper',
|
||||
description: 'Achieve a 20x combo in Balloon Pop',
|
||||
icon: '💥'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Neustart
|
||||
function restartGame() {
|
||||
gameRunning = true;
|
||||
score = 0;
|
||||
level = 1;
|
||||
combo = 0;
|
||||
comboTimer = 0;
|
||||
balloonsMissed = 0;
|
||||
|
||||
// Power-ups zurücksetzen
|
||||
multiShotActive = false;
|
||||
multiShotTimer = 0;
|
||||
slowTimeActive = false;
|
||||
slowTimeTimer = 0;
|
||||
|
||||
// Arrays leeren
|
||||
balloons.length = 0;
|
||||
particles.length = 0;
|
||||
powerups.length = 0;
|
||||
|
||||
// Timer zurücksetzen
|
||||
balloonSpawnTimer = 0;
|
||||
powerupSpawnTimer = 0;
|
||||
|
||||
document.getElementById('gameOver').style.display = 'none';
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Spiel starten
|
||||
createClouds();
|
||||
gameLoop();
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,491 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Bounce & Catch - Tutorial Game</title>
|
||||
<style>
|
||||
/* ============================================
|
||||
GRUNDLEGENDE STYLES
|
||||
Definiert das Aussehen der Seite
|
||||
============================================ */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #1a1a1a;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: 'Arial', sans-serif;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Canvas ist die Zeichenfläche für unser Spiel */
|
||||
canvas {
|
||||
border: 2px solid #4CAF50;
|
||||
background: #0a0a0a;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* UI-Elemente für Spielinformationen */
|
||||
.game-info {
|
||||
margin: 10px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.lives {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
/* Start-Bildschirm */
|
||||
.start-screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.start-screen h1 {
|
||||
color: #4CAF50;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.start-screen p {
|
||||
margin: 10px 0;
|
||||
max-width: 400px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.start-button:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
/* Game Over Bildschirm */
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.final-score {
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.restart-button {
|
||||
background: #ff6b6b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.restart-button:hover {
|
||||
background: #ff5252;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<!-- Spielinformationen -->
|
||||
<div class="game-info">
|
||||
<span class="score">Punkte: <span id="score">0</span></span> |
|
||||
<span class="lives">Leben: <span id="lives">3</span></span>
|
||||
</div>
|
||||
|
||||
<!-- Das Haupt-Canvas für das Spiel -->
|
||||
<canvas id="gameCanvas" width="600" height="400"></canvas>
|
||||
|
||||
<!-- Start-Bildschirm -->
|
||||
<div class="start-screen" id="startScreen">
|
||||
<h1>Bounce & Catch Tutorial</h1>
|
||||
<p>Ein einfaches Lernspiel, das die Grundlagen der Spieleentwicklung zeigt!</p>
|
||||
<p><strong>Steuerung:</strong> Bewege die Maus, um das Paddle zu steuern</p>
|
||||
<p><strong>Ziel:</strong> Fange den Ball mit dem Paddle auf, bevor er unten aus dem Bild fällt</p>
|
||||
<p>Je länger du spielst, desto schneller wird der Ball!</p>
|
||||
<button class="start-button" onclick="startGame()">Spiel starten</button>
|
||||
</div>
|
||||
|
||||
<!-- Game Over Bildschirm -->
|
||||
<div class="game-over" id="gameOverScreen">
|
||||
<h2>Game Over!</h2>
|
||||
<div class="final-score">Endpunktzahl: <span id="finalScore">0</span></div>
|
||||
<button class="restart-button" onclick="restartGame()">Nochmal spielen</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ============================================
|
||||
SPIEL-INITIALISIERUNG
|
||||
Hier werden alle wichtigen Variablen und
|
||||
Objekte für das Spiel definiert
|
||||
============================================ */
|
||||
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'bounce-catch-tutorial';
|
||||
|
||||
// Canvas und 2D-Kontext holen
|
||||
// Der Canvas ist unsere Zeichenfläche, ctx ist der "Pinsel"
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// UI-Elemente für die Anzeige
|
||||
const scoreElement = document.getElementById('score');
|
||||
const livesElement = document.getElementById('lives');
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const gameOverScreen = document.getElementById('gameOverScreen');
|
||||
const finalScoreElement = document.getElementById('finalScore');
|
||||
|
||||
/* ============================================
|
||||
SPIELOBJEKTE
|
||||
Definiert die Eigenschaften von Ball und Paddle
|
||||
============================================ */
|
||||
|
||||
// Ball-Objekt mit Position, Geschwindigkeit und Größe
|
||||
const ball = {
|
||||
x: canvas.width / 2, // Horizontale Position (Mitte)
|
||||
y: 50, // Vertikale Position (oben)
|
||||
radius: 10, // Radius des Balls
|
||||
dx: 3, // Horizontale Geschwindigkeit
|
||||
dy: 3, // Vertikale Geschwindigkeit
|
||||
color: '#4CAF50', // Farbe des Balls
|
||||
speedIncrease: 0.1 // Geschwindigkeitszunahme pro Treffer
|
||||
};
|
||||
|
||||
// Paddle-Objekt (das Brett zum Fangen)
|
||||
const paddle = {
|
||||
width: 100, // Breite des Paddles
|
||||
height: 15, // Höhe des Paddles
|
||||
x: canvas.width / 2 - 50, // Startposition (zentriert)
|
||||
y: canvas.height - 30, // Position am unteren Rand
|
||||
color: '#2196F3', // Farbe des Paddles
|
||||
speed: 8 // Bewegungsgeschwindigkeit
|
||||
};
|
||||
|
||||
/* ============================================
|
||||
SPIELZUSTAND
|
||||
Variablen die den aktuellen Zustand speichern
|
||||
============================================ */
|
||||
let score = 0; // Aktuelle Punktzahl
|
||||
let lives = 3; // Anzahl der Leben
|
||||
let gameRunning = false; // Ist das Spiel aktiv?
|
||||
let mouseX = canvas.width / 2; // Mausposition
|
||||
|
||||
/* ============================================
|
||||
EINGABE-VERWALTUNG
|
||||
Reagiert auf Mausbewegungen des Spielers
|
||||
============================================ */
|
||||
|
||||
// Event-Listener für Mausbewegung
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
// Berechne die Mausposition relativ zum Canvas
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouseX = e.clientX - rect.left;
|
||||
|
||||
// Aktualisiere Paddle-Position (zentriert auf Maus)
|
||||
if (gameRunning) {
|
||||
paddle.x = mouseX - paddle.width / 2;
|
||||
|
||||
// Verhindere, dass das Paddle aus dem Bildschirm geht
|
||||
if (paddle.x < 0) paddle.x = 0;
|
||||
if (paddle.x + paddle.width > canvas.width) {
|
||||
paddle.x = canvas.width - paddle.width;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* ============================================
|
||||
SPIEL-FUNKTIONEN
|
||||
Hauptfunktionen für Spiellogik und Rendering
|
||||
============================================ */
|
||||
|
||||
// Funktion zum Starten des Spiels
|
||||
function startGame() {
|
||||
gameRunning = true;
|
||||
startScreen.style.display = 'none';
|
||||
gameLoop(); // Starte die Spielschleife
|
||||
}
|
||||
|
||||
// Funktion zum Neustarten des Spiels
|
||||
function restartGame() {
|
||||
// Setze alle Werte zurück
|
||||
score = 0;
|
||||
lives = 3;
|
||||
ball.x = canvas.width / 2;
|
||||
ball.y = 50;
|
||||
ball.dx = 3;
|
||||
ball.dy = 3;
|
||||
|
||||
// Aktualisiere UI
|
||||
scoreElement.textContent = score;
|
||||
livesElement.textContent = lives;
|
||||
gameOverScreen.style.display = 'none';
|
||||
|
||||
gameRunning = true;
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Ball-Bewegung und Physik
|
||||
function updateBall() {
|
||||
// Bewege den Ball
|
||||
ball.x += ball.dx;
|
||||
ball.y += ball.dy;
|
||||
|
||||
// Kollision mit linker oder rechter Wand
|
||||
if (ball.x - ball.radius < 0 || ball.x + ball.radius > canvas.width) {
|
||||
ball.dx = -ball.dx; // Kehre horizontale Richtung um
|
||||
}
|
||||
|
||||
// Kollision mit oberer Wand
|
||||
if (ball.y - ball.radius < 0) {
|
||||
ball.dy = -ball.dy; // Kehre vertikale Richtung um
|
||||
}
|
||||
|
||||
// Ball fällt unten aus dem Bildschirm
|
||||
if (ball.y - ball.radius > canvas.height) {
|
||||
loseLife();
|
||||
}
|
||||
}
|
||||
|
||||
// Kollisionserkennung zwischen Ball und Paddle
|
||||
function checkPaddleCollision() {
|
||||
// Prüfe ob Ball im Bereich des Paddles ist
|
||||
if (ball.y + ball.radius > paddle.y &&
|
||||
ball.y - ball.radius < paddle.y + paddle.height &&
|
||||
ball.x > paddle.x &&
|
||||
ball.x < paddle.x + paddle.width) {
|
||||
|
||||
// Ball prallt ab
|
||||
ball.dy = -Math.abs(ball.dy); // Immer nach oben
|
||||
|
||||
// Erhöhe Punkte
|
||||
score += 10;
|
||||
scoreElement.textContent = score;
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Erhöhe Geschwindigkeit (macht das Spiel schwieriger)
|
||||
ball.dx *= (1 + ball.speedIncrease);
|
||||
ball.dy *= (1 + ball.speedIncrease);
|
||||
|
||||
// Spiele einen Ton (optional - hier nur visuelles Feedback)
|
||||
paddle.color = '#00FF00'; // Kurz grün
|
||||
setTimeout(() => {
|
||||
paddle.color = '#2196F3'; // Zurück zu blau
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Funktion wenn ein Leben verloren wird
|
||||
function loseLife() {
|
||||
lives--;
|
||||
livesElement.textContent = lives;
|
||||
|
||||
if (lives <= 0) {
|
||||
gameOver();
|
||||
} else {
|
||||
// Setze Ball zurück
|
||||
ball.x = canvas.width / 2;
|
||||
ball.y = 50;
|
||||
ball.dx = Math.abs(ball.dx) * 0.8; // Reduziere Geschwindigkeit etwas
|
||||
ball.dy = Math.abs(ball.dy) * 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
// Game Over Funktion
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
finalScoreElement.textContent = score;
|
||||
gameOverScreen.style.display = 'flex';
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 100) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'bounce_beginner',
|
||||
name: 'Bounce Beginner',
|
||||
description: 'Score 100 points in Bounce & Catch',
|
||||
icon: '🏓'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (score >= 500) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'paddle_pro',
|
||||
name: 'Paddle Pro',
|
||||
description: 'Score 500 points in Bounce & Catch',
|
||||
icon: '🎯'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RENDERING (ZEICHNEN)
|
||||
Funktionen zum Zeichnen der Spielobjekte
|
||||
============================================ */
|
||||
|
||||
// Zeichne den Ball
|
||||
function drawBall() {
|
||||
ctx.beginPath();
|
||||
ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = ball.color;
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
|
||||
// Zeichne einen Glanzeffekt
|
||||
ctx.beginPath();
|
||||
ctx.arc(ball.x - 3, ball.y - 3, ball.radius / 3, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.fill();
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
// Zeichne das Paddle
|
||||
function drawPaddle() {
|
||||
// Hauptkörper des Paddles
|
||||
ctx.fillStyle = paddle.color;
|
||||
ctx.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);
|
||||
|
||||
// Zeichne einen Rand
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(paddle.x, paddle.y, paddle.width, paddle.height);
|
||||
}
|
||||
|
||||
// Lösche den Canvas und zeichne Hintergrund
|
||||
function clearCanvas() {
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Zeichne ein Gitter für visuellen Effekt
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < canvas.width; i += 50) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i, 0);
|
||||
ctx.lineTo(i, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i < canvas.height; i += 50) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, i);
|
||||
ctx.lineTo(canvas.width, i);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
HAUPTSPIELSCHLEIFE
|
||||
Wird 60x pro Sekunde aufgerufen
|
||||
============================================ */
|
||||
function gameLoop() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
// 1. Lösche alten Frame
|
||||
clearCanvas();
|
||||
|
||||
// 2. Aktualisiere Spiellogik
|
||||
updateBall();
|
||||
checkPaddleCollision();
|
||||
|
||||
// 3. Zeichne alle Objekte
|
||||
drawBall();
|
||||
drawPaddle();
|
||||
|
||||
// 4. Nächster Frame
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ZUSÄTZLICHE EFFEKTE
|
||||
Kleine Details die das Spiel besser machen
|
||||
============================================ */
|
||||
|
||||
// Partikeleffekt beim Treffen (optional)
|
||||
function createParticles(x, y) {
|
||||
// Hier könnte man Partikeleffekte hinzufügen
|
||||
// Für dieses Tutorial halten wir es simpel
|
||||
}
|
||||
|
||||
// Debug-Informationen (für Entwickler)
|
||||
console.log('Bounce & Catch Tutorial geladen!');
|
||||
console.log('Canvas-Größe:', canvas.width, 'x', canvas.height);
|
||||
console.log('Dies ist ein Lernspiel um Spieleentwicklung zu verstehen.');
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
710
games/mana-games/apps/web/public/games/card_stack_rush.html
Normal file
|
|
@ -0,0 +1,710 @@
|
|||
<!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>
|
||||
180
games/mana-games/apps/web/public/games/click_race.html
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Click Race</title>
|
||||
<style>
|
||||
/* Grundlegende Styles für die Seite */
|
||||
body {
|
||||
margin: 0;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
/* Container für das Spiel */
|
||||
#game {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Das klickbare Quadrat */
|
||||
#target {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: #f00;
|
||||
margin: 20px auto;
|
||||
cursor: pointer;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
|
||||
/* Animation beim Klicken */
|
||||
#target:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* Restart-Button Style */
|
||||
#restart {
|
||||
display: none;
|
||||
padding: 10px 20px;
|
||||
background: #00ff00;
|
||||
color: #000;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#restart:hover {
|
||||
background: #00cc00;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="game">
|
||||
<h1>CLICK RACE</h1>
|
||||
<p>30 Klicks so schnell wie möglich!</p>
|
||||
<div id="target"></div>
|
||||
<h2 id="info">Klicke zum Starten!</h2>
|
||||
<button id="restart" onclick="restart()">NOCHMAL!</button>
|
||||
</div>
|
||||
<script>
|
||||
// Spielvariablen
|
||||
let clicks = 0; // Anzahl der Klicks
|
||||
let startTime = 0; // Startzeit für die Zeitmessung
|
||||
let gameStarted = false; // Track ob das Spiel läuft
|
||||
|
||||
// DOM-Elemente holen
|
||||
const target = document.getElementById('target');
|
||||
const info = document.getElementById('info');
|
||||
const restartBtn = document.getElementById('restart');
|
||||
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'click-race';
|
||||
|
||||
// Sende Game Loaded Event beim Laden
|
||||
window.addEventListener('load', () => {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
});
|
||||
|
||||
// Klick-Handler für das rote Quadrat
|
||||
target.onclick = () => {
|
||||
// Beim ersten Klick Timer starten
|
||||
if (clicks === 0) {
|
||||
startTime = Date.now();
|
||||
gameStarted = true;
|
||||
}
|
||||
|
||||
// Klicks zählen
|
||||
clicks++;
|
||||
|
||||
// Prüfen ob Spiel noch läuft
|
||||
if (clicks < 30) {
|
||||
// Farbe ändern basierend auf Fortschritt
|
||||
target.style.background = `hsl(${clicks * 12}, 100%, 50%)`;
|
||||
// Anzeige aktualisieren
|
||||
info.textContent = `${30 - clicks} übrig`;
|
||||
} else {
|
||||
// Spiel beendet - Zeit berechnen
|
||||
const time = ((Date.now() - startTime) / 1000).toFixed(2);
|
||||
const timeInMs = Math.round(time * 1000);
|
||||
info.textContent = `FERTIG! Zeit: ${time}s`;
|
||||
|
||||
// Quadrat verstecken und Restart-Button zeigen
|
||||
target.style.display = 'none';
|
||||
restartBtn.style.display = 'inline-block';
|
||||
|
||||
// Sende Score (Zeit in Millisekunden - niedriger ist besser)
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: timeInMs }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (timeInMs < 5000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'speed-demon',
|
||||
name: 'Speed Demon',
|
||||
description: '30 Klicks in unter 5 Sekunden!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (timeInMs < 3000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'lightning-fast',
|
||||
name: 'Blitzschnell',
|
||||
description: '30 Klicks in unter 3 Sekunden!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
gameStarted = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Restart-Funktion
|
||||
function restart() {
|
||||
// Alle Werte zurücksetzen
|
||||
clicks = 0;
|
||||
startTime = 0;
|
||||
gameStarted = false;
|
||||
// UI zurücksetzen
|
||||
target.style.display = 'block';
|
||||
target.style.background = '#f00';
|
||||
restartBtn.style.display = 'none';
|
||||
info.textContent = 'Klicke zum Starten!';
|
||||
}
|
||||
|
||||
// Sende Game Ended Event beim Verlassen
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (gameStarted) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_ENDED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
144
games/mana-games/apps/web/public/games/color_memory.html
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Color Memory</title>
|
||||
<style>
|
||||
body {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-family: Arial;
|
||||
padding: 50px;
|
||||
}
|
||||
.box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
display: inline-block;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
background: #333;
|
||||
transition: 0.3s;
|
||||
}
|
||||
.box:hover {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>COLOR MEMORY</h1>
|
||||
<p id="info">Merke dir die Reihenfolge!</p>
|
||||
<div>
|
||||
<div class="box" onclick="check(0)"></div>
|
||||
<div class="box" onclick="check(1)"></div>
|
||||
<div class="box" onclick="check(2)"></div>
|
||||
<div class="box" onclick="check(3)"></div>
|
||||
</div>
|
||||
<h2 id="score">Level: 1</h2>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'color-memory';
|
||||
|
||||
let sequence = [];
|
||||
let playerSeq = [];
|
||||
let level = 1;
|
||||
let playing = false;
|
||||
const colors = ['#f00', '#0f0', '#00f', '#ff0'];
|
||||
const boxes = document.querySelectorAll('.box');
|
||||
|
||||
function nextLevel() {
|
||||
playerSeq = [];
|
||||
sequence.push(Math.floor(Math.random() * 4));
|
||||
playing = false;
|
||||
document.getElementById('info').textContent = 'Schau zu!';
|
||||
|
||||
sequence.forEach((num, i) => {
|
||||
setTimeout(() => {
|
||||
boxes[num].style.background = colors[num];
|
||||
setTimeout(() => boxes[num].style.background = '#333', 400);
|
||||
}, i * 600 + 600);
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
playing = true;
|
||||
document.getElementById('info').textContent = 'Dein Zug!';
|
||||
}, sequence.length * 600 + 600);
|
||||
}
|
||||
|
||||
function check(num) {
|
||||
if (!playing) return;
|
||||
|
||||
boxes[num].style.background = colors[num];
|
||||
setTimeout(() => boxes[num].style.background = '#333', 200);
|
||||
|
||||
playerSeq.push(num);
|
||||
|
||||
if (playerSeq[playerSeq.length - 1] !== sequence[playerSeq.length - 1]) {
|
||||
document.getElementById('info').textContent = `Game Over! Level ${level} erreicht`;
|
||||
playing = false;
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: level * 100 }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (level >= 10) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'memory_master',
|
||||
name: 'Memory Master',
|
||||
description: 'Reach level 10 in Color Memory',
|
||||
icon: '🧠'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (level >= 15) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'photographic_memory',
|
||||
name: 'Photographic Memory',
|
||||
description: 'Reach level 15 in Color Memory',
|
||||
icon: '📸'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
setTimeout(() => location.reload(), 2000);
|
||||
} else if (playerSeq.length === sequence.length) {
|
||||
level++;
|
||||
document.getElementById('score').textContent = `Level: ${level}`;
|
||||
document.getElementById('info').textContent = 'Richtig!';
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: level * 100 }
|
||||
}, '*');
|
||||
|
||||
setTimeout(nextLevel, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
nextLevel();
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
697
games/mana-games/apps/web/public/games/fish_catcher.html
Normal file
|
|
@ -0,0 +1,697 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Fish Catcher</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: linear-gradient(180deg, #87CEEB 0%, #1e90ff 30%, #0066cc 60%, #003d82 100%);
|
||||
color: #fff;
|
||||
font-family: 'Comic Sans MS', cursive;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #fff;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.ui {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 20px;
|
||||
z-index: 10;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #ffff00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.lives {
|
||||
color: #ff6b6b;
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
background: rgba(0, 50, 100, 0.9);
|
||||
padding: 30px;
|
||||
border: 3px solid #fff;
|
||||
border-radius: 20px;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
font-size: 14px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(145deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 16px;
|
||||
border-radius: 25px;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.timer {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-size: 18px;
|
||||
color: #ffff00;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
|
||||
<div class="ui">
|
||||
<div class="score">🐟 Fische: <span id="score">0</span></div>
|
||||
<div class="lives">❤️ Leben: <span id="lives">3</span></div>
|
||||
</div>
|
||||
|
||||
<div class="timer">
|
||||
⏰ Zeit: <span id="timeLeft">60</span>s
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
A/D oder ← → : Boot bewegen | Maus: Alternative Steuerung
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>🎣 Angeltag beendet!</h2>
|
||||
<p>Gefangene Fische: <span id="finalScore">0</span></p>
|
||||
<p id="rating"></p>
|
||||
<button onclick="restartGame()">🔄 Nochmal angeln</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'fish-catcher';
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Spiel-Zustand
|
||||
let gameRunning = true;
|
||||
let score = 0;
|
||||
let lives = 3;
|
||||
let timeLeft = 60;
|
||||
let gameTimer;
|
||||
|
||||
// Eingabe
|
||||
const keys = {};
|
||||
let mouseX = canvas.width / 2;
|
||||
|
||||
// Boot (Spieler)
|
||||
const boat = {
|
||||
x: canvas.width / 2 - 50,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 40,
|
||||
speed: 6,
|
||||
netWidth: 80,
|
||||
netActive: false,
|
||||
netAnimation: 0
|
||||
};
|
||||
|
||||
// Arrays für Spielobjekte
|
||||
const fish = [];
|
||||
const bubbles = [];
|
||||
const powerups = [];
|
||||
const splashes = [];
|
||||
|
||||
// Wellen für Hintergrund
|
||||
const waves = [];
|
||||
|
||||
// Wellen erstellen
|
||||
function createWaves() {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
waves.push({
|
||||
x: i * 120,
|
||||
y: canvas.height - 60 + Math.sin(i) * 10,
|
||||
amplitude: 8 + Math.random() * 5,
|
||||
frequency: 0.02 + Math.random() * 0.01,
|
||||
offset: Math.random() * Math.PI * 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fisch erstellen
|
||||
function createFish() {
|
||||
const fishTypes = [
|
||||
{ color: '#ff6b35', points: 10, speed: 1, size: 20 }, // Orange Fisch
|
||||
{ color: '#f7931e', points: 15, speed: 1.5, size: 16 }, // Gelber Fisch
|
||||
{ color: '#ff1744', points: 25, speed: 2, size: 12 }, // Roter Fisch (schnell)
|
||||
{ color: '#9c27b0', points: 50, speed: 0.8, size: 25 } // Lila Fisch (groß, langsam)
|
||||
];
|
||||
|
||||
const type = fishTypes[Math.floor(Math.random() * fishTypes.length)];
|
||||
|
||||
fish.push({
|
||||
x: Math.random() * (canvas.width - 40) + 20,
|
||||
y: canvas.height,
|
||||
width: type.size,
|
||||
height: type.size * 0.6,
|
||||
speed: type.speed,
|
||||
color: type.color,
|
||||
points: type.points,
|
||||
wiggle: Math.random() * Math.PI * 2,
|
||||
wiggleSpeed: 0.05 + Math.random() * 0.05
|
||||
});
|
||||
}
|
||||
|
||||
// Power-up erstellen
|
||||
function createPowerup() {
|
||||
const types = ['bignet', 'multiplier', 'timeadd'];
|
||||
const type = types[Math.floor(Math.random() * types.length)];
|
||||
|
||||
powerups.push({
|
||||
x: Math.random() * (canvas.width - 30) + 15,
|
||||
y: canvas.height,
|
||||
width: 25,
|
||||
height: 25,
|
||||
speed: 0.8,
|
||||
type: type,
|
||||
rotation: 0,
|
||||
pulse: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Luftblasen erstellen
|
||||
function createBubbles() {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
bubbles.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: canvas.height,
|
||||
size: Math.random() * 8 + 3,
|
||||
speed: 0.5 + Math.random() * 1,
|
||||
opacity: 0.3 + Math.random() * 0.4
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Splash-Effekt erstellen
|
||||
function createSplash(x, y, color = '#87CEEB') {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
splashes.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: (Math.random() - 0.5) * 8,
|
||||
vy: (Math.random() - 0.5) * 8,
|
||||
size: Math.random() * 4 + 2,
|
||||
life: 20,
|
||||
maxLife: 20,
|
||||
color: color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Kollisionserkennung
|
||||
function checkCollision(rect1, rect2) {
|
||||
return rect1.x < rect2.x + rect2.width &&
|
||||
rect1.x + rect1.width > rect2.x &&
|
||||
rect1.y < rect2.y + rect2.height &&
|
||||
rect1.y + rect1.height > rect2.y;
|
||||
}
|
||||
|
||||
// Event Listener
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
mouseX = e.clientX - rect.left;
|
||||
});
|
||||
|
||||
// Boot updaten
|
||||
function updateBoat() {
|
||||
// Tastatur-Steuerung
|
||||
if (keys['a'] || keys['arrowleft']) {
|
||||
boat.x -= boat.speed;
|
||||
}
|
||||
if (keys['d'] || keys['arrowright']) {
|
||||
boat.x += boat.speed;
|
||||
}
|
||||
|
||||
// Maus-Steuerung (sanfter)
|
||||
const targetX = mouseX - boat.width / 2;
|
||||
const diff = targetX - boat.x;
|
||||
boat.x += diff * 0.1;
|
||||
|
||||
// Grenzen
|
||||
if (boat.x < 0) boat.x = 0;
|
||||
if (boat.x + boat.width > canvas.width) boat.x = canvas.width - boat.width;
|
||||
|
||||
// Netz-Animation
|
||||
if (boat.netAnimation > 0) {
|
||||
boat.netAnimation--;
|
||||
boat.netActive = true;
|
||||
} else {
|
||||
boat.netActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fische updaten
|
||||
function updateFish() {
|
||||
for (let i = fish.length - 1; i >= 0; i--) {
|
||||
const f = fish[i];
|
||||
|
||||
// Bewegung
|
||||
f.y -= f.speed;
|
||||
f.wiggle += f.wiggleSpeed;
|
||||
f.x += Math.sin(f.wiggle) * 0.5;
|
||||
|
||||
// Aus dem Bildschirm entfernt
|
||||
if (f.y + f.height < 0) {
|
||||
fish.splice(i, 1);
|
||||
lives--;
|
||||
createSplash(f.x + f.width/2, 0, '#ff6b6b');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kollision mit Netz
|
||||
const netArea = {
|
||||
x: boat.x + boat.width/2 - boat.netWidth/2,
|
||||
y: boat.y + boat.height,
|
||||
width: boat.netWidth,
|
||||
height: 60
|
||||
};
|
||||
|
||||
if (checkCollision(f, netArea)) {
|
||||
score += f.points;
|
||||
createSplash(f.x + f.width/2, f.y + f.height/2, f.color);
|
||||
boat.netAnimation = 15;
|
||||
fish.splice(i, 1);
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power-ups updaten
|
||||
function updatePowerups() {
|
||||
for (let i = powerups.length - 1; i >= 0; i--) {
|
||||
const p = powerups[i];
|
||||
|
||||
p.y -= p.speed;
|
||||
p.rotation += 0.1;
|
||||
p.pulse += 0.15;
|
||||
|
||||
if (p.y + p.height < 0) {
|
||||
powerups.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Kollision mit Boot
|
||||
if (checkCollision(p, boat)) {
|
||||
if (p.type === 'bignet') {
|
||||
boat.netWidth = Math.min(boat.netWidth + 20, 150);
|
||||
} else if (p.type === 'multiplier') {
|
||||
// Nächste 5 Fische doppelte Punkte (vereinfacht)
|
||||
score += 100;
|
||||
} else if (p.type === 'timeadd') {
|
||||
timeLeft += 10;
|
||||
}
|
||||
|
||||
createSplash(p.x + p.width/2, p.y + p.height/2, '#ffff00');
|
||||
powerups.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Blasen updaten
|
||||
function updateBubbles() {
|
||||
for (let i = bubbles.length - 1; i >= 0; i--) {
|
||||
const bubble = bubbles[i];
|
||||
|
||||
bubble.y -= bubble.speed;
|
||||
bubble.x += Math.sin(bubble.y * 0.01) * 0.3;
|
||||
|
||||
if (bubble.y + bubble.size < 0) {
|
||||
bubbles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Splash-Effekte updaten
|
||||
function updateSplashes() {
|
||||
for (let i = splashes.length - 1; i >= 0; i--) {
|
||||
const splash = splashes[i];
|
||||
|
||||
splash.x += splash.vx;
|
||||
splash.y += splash.vy;
|
||||
splash.vy += 0.3; // Schwerkraft
|
||||
splash.life--;
|
||||
|
||||
if (splash.life <= 0) {
|
||||
splashes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeichnen
|
||||
function draw() {
|
||||
// Himmel/Wasser Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
gradient.addColorStop(0, '#87CEEB');
|
||||
gradient.addColorStop(0.3, '#1e90ff');
|
||||
gradient.addColorStop(0.6, '#0066cc');
|
||||
gradient.addColorStop(1, '#003d82');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Wellen zeichnen
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
||||
for (const wave of waves) {
|
||||
ctx.beginPath();
|
||||
for (let x = 0; x < canvas.width; x += 5) {
|
||||
const y = wave.y + Math.sin((x + wave.offset) * wave.frequency) * wave.amplitude;
|
||||
if (x === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.lineTo(canvas.width, canvas.height);
|
||||
ctx.lineTo(0, canvas.height);
|
||||
ctx.fill();
|
||||
wave.offset += 0.02;
|
||||
}
|
||||
|
||||
// Blasen zeichnen
|
||||
for (const bubble of bubbles) {
|
||||
ctx.globalAlpha = bubble.opacity;
|
||||
ctx.fillStyle = '#87CEEB';
|
||||
ctx.beginPath();
|
||||
ctx.arc(bubble.x, bubble.y, bubble.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
// Fische zeichnen
|
||||
for (const f of fish) {
|
||||
ctx.save();
|
||||
ctx.translate(f.x + f.width/2, f.y + f.height/2);
|
||||
|
||||
// Fisch-Körper
|
||||
ctx.fillStyle = f.color;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, f.width/2, f.height/2, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Fisch-Schwanz
|
||||
ctx.fillStyle = f.color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-f.width/2, 0);
|
||||
ctx.lineTo(-f.width/2 - 8, -f.height/4);
|
||||
ctx.lineTo(-f.width/2 - 8, f.height/4);
|
||||
ctx.fill();
|
||||
|
||||
// Auge
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(f.width/4, -f.height/6, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(f.width/4, -f.height/6, 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Power-ups zeichnen
|
||||
for (const p of powerups) {
|
||||
ctx.save();
|
||||
ctx.translate(p.x + p.width/2, p.y + p.height/2);
|
||||
ctx.rotate(p.rotation);
|
||||
|
||||
const pulseSize = p.width + Math.sin(p.pulse) * 3;
|
||||
|
||||
if (p.type === 'bignet') {
|
||||
ctx.fillStyle = '#4CAF50';
|
||||
} else if (p.type === 'multiplier') {
|
||||
ctx.fillStyle = '#ffff00';
|
||||
} else {
|
||||
ctx.fillStyle = '#ff69b4';
|
||||
}
|
||||
|
||||
ctx.fillRect(-pulseSize/2, -pulseSize/2, pulseSize, pulseSize);
|
||||
|
||||
// Symbol
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.font = '12px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
if (p.type === 'bignet') ctx.fillText('🕸️', 0, 4);
|
||||
else if (p.type === 'multiplier') ctx.fillText('×2', 0, 4);
|
||||
else ctx.fillText('+T', 0, 4);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Boot zeichnen
|
||||
ctx.fillStyle = '#8B4513';
|
||||
ctx.fillRect(boat.x, boat.y, boat.width, boat.height);
|
||||
|
||||
// Boot-Details
|
||||
ctx.fillStyle = '#A0522D';
|
||||
ctx.fillRect(boat.x + 10, boat.y + 5, boat.width - 20, boat.height - 10);
|
||||
|
||||
// Netz zeichnen
|
||||
if (boat.netActive || boat.netAnimation > 0) {
|
||||
const netY = boat.y + boat.height;
|
||||
const netX = boat.x + boat.width/2 - boat.netWidth/2;
|
||||
|
||||
ctx.strokeStyle = '#654321';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.globalAlpha = 0.7;
|
||||
|
||||
// Netz-Muster
|
||||
for (let i = 0; i < 6; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const x = netX + (i * boat.netWidth/5);
|
||||
const y = netY + (j * 15);
|
||||
ctx.strokeRect(x, y, boat.netWidth/5, 15);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Splash-Effekte zeichnen
|
||||
for (const splash of splashes) {
|
||||
ctx.globalAlpha = splash.life / splash.maxLife;
|
||||
ctx.fillStyle = splash.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(splash.x, splash.y, splash.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Spawn-System
|
||||
let fishSpawnTimer = 0;
|
||||
let powerupSpawnTimer = 0;
|
||||
let bubbleSpawnTimer = 0;
|
||||
|
||||
function handleSpawning() {
|
||||
fishSpawnTimer++;
|
||||
powerupSpawnTimer++;
|
||||
bubbleSpawnTimer++;
|
||||
|
||||
// Fische spawnen
|
||||
if (fishSpawnTimer >= 90) {
|
||||
createFish();
|
||||
fishSpawnTimer = 0;
|
||||
}
|
||||
|
||||
// Power-ups spawnen
|
||||
if (powerupSpawnTimer >= 600 && powerups.length < 1) {
|
||||
createPowerup();
|
||||
powerupSpawnTimer = 0;
|
||||
}
|
||||
|
||||
// Blasen spawnen
|
||||
if (bubbleSpawnTimer >= 30) {
|
||||
createBubbles();
|
||||
bubbleSpawnTimer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Spiel-Loop
|
||||
function gameLoop() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
updateBoat();
|
||||
updateFish();
|
||||
updatePowerups();
|
||||
updateBubbles();
|
||||
updateSplashes();
|
||||
handleSpawning();
|
||||
|
||||
draw();
|
||||
|
||||
// UI updaten
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('lives').textContent = lives;
|
||||
document.getElementById('timeLeft').textContent = timeLeft;
|
||||
|
||||
// Spiel beenden
|
||||
if (lives <= 0 || timeLeft <= 0) {
|
||||
gameOver();
|
||||
}
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Timer
|
||||
function startTimer() {
|
||||
gameTimer = setInterval(() => {
|
||||
if (gameRunning && timeLeft > 0) {
|
||||
timeLeft--;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Game Over
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
clearInterval(gameTimer);
|
||||
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
|
||||
let rating = '';
|
||||
if (score >= 500) rating = '🏆 Meister-Angler!';
|
||||
else if (score >= 300) rating = '🥈 Profi-Fischer!';
|
||||
else if (score >= 150) rating = '🥉 Guter Fang!';
|
||||
else rating = '🎣 Weiter üben!';
|
||||
|
||||
document.getElementById('rating').textContent = rating;
|
||||
document.getElementById('gameOver').style.display = 'block';
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 500) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'master_angler',
|
||||
name: 'Master Angler',
|
||||
description: 'Score 500 points in Fish Catcher',
|
||||
icon: '🏆'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (lives === 3 && score >= 300) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'perfect_fishing',
|
||||
name: 'Perfect Fishing',
|
||||
description: 'Score 300 points without losing a life',
|
||||
icon: '🌟'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Neustart
|
||||
function restartGame() {
|
||||
gameRunning = true;
|
||||
score = 0;
|
||||
lives = 3;
|
||||
timeLeft = 60;
|
||||
|
||||
// Arrays leeren
|
||||
fish.length = 0;
|
||||
powerups.length = 0;
|
||||
bubbles.length = 0;
|
||||
splashes.length = 0;
|
||||
|
||||
// Boot zurücksetzen
|
||||
boat.x = canvas.width / 2 - 50;
|
||||
boat.netWidth = 80;
|
||||
boat.netActive = false;
|
||||
boat.netAnimation = 0;
|
||||
|
||||
// Timer zurücksetzen
|
||||
fishSpawnTimer = 0;
|
||||
powerupSpawnTimer = 0;
|
||||
bubbleSpawnTimer = 0;
|
||||
|
||||
clearInterval(gameTimer);
|
||||
startTimer();
|
||||
|
||||
document.getElementById('gameOver').style.display = 'none';
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Spiel starten
|
||||
createWaves();
|
||||
startTimer();
|
||||
gameLoop();
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
489
games/mana-games/apps/web/public/games/flappy_mana.html
Normal file
|
|
@ -0,0 +1,489 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Flappy Mana</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #1a1a2e;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #eee;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
letter-spacing: 2px;
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #f39c12;
|
||||
background: linear-gradient(to bottom, #87CEEB 0%, #98D8E8 100%);
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.start-screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.start-screen h1 {
|
||||
color: #f39c12;
|
||||
margin-bottom: 20px;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.start-button {
|
||||
background: #f39c12;
|
||||
color: #1a1a2e;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
font-size: 18px;
|
||||
font-family: 'Courier New', monospace;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.start-button:hover {
|
||||
background: #e67e22;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(26, 26, 46, 0.95);
|
||||
border: 3px solid #f39c12;
|
||||
padding: 30px;
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
color: #f39c12;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background: #f39c12;
|
||||
color: #1a1a2e;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
background: #e67e22;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="score">SCORE: <span id="score">0</span></div>
|
||||
<canvas id="gameCanvas" width="400" height="600"></canvas>
|
||||
|
||||
<div class="start-screen" id="startScreen">
|
||||
<h1>FLAPPY MANA</h1>
|
||||
<p class="instructions">Klicke oder drücke SPACE zum Fliegen</p>
|
||||
<p class="instructions">Weiche den Röhren aus!</p>
|
||||
<button class="start-button" onclick="startGame()">START</button>
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>GAME OVER</h2>
|
||||
<div>SCORE: <span id="finalScore">0</span></div>
|
||||
<div>BEST: <span id="bestScore">0</span></div>
|
||||
<button class="restart-btn" onclick="restartGame()">RESTART</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const scoreElement = document.getElementById('score');
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const gameOverElement = document.getElementById('gameOver');
|
||||
const finalScoreElement = document.getElementById('finalScore');
|
||||
const bestScoreElement = document.getElementById('bestScore');
|
||||
|
||||
const GAME_ID = 'flappy-mana';
|
||||
|
||||
let gameRunning = false;
|
||||
let gameStarted = false;
|
||||
let score = 0;
|
||||
let bestScore = localStorage.getItem('flappyManaBest') || 0;
|
||||
let animationId = null;
|
||||
|
||||
const bird = {
|
||||
x: 100,
|
||||
y: canvas.height / 2,
|
||||
radius: 15,
|
||||
velocity: 0,
|
||||
gravity: 0.4,
|
||||
jumpPower: -8,
|
||||
color: '#f39c12',
|
||||
rotation: 0
|
||||
};
|
||||
|
||||
const pipes = [];
|
||||
const pipeWidth = 60;
|
||||
const pipeGap = 180;
|
||||
const pipeSpeed = 3;
|
||||
let pipeTimer = 0;
|
||||
|
||||
const particles = [];
|
||||
|
||||
const clouds = [
|
||||
{ x: 100, y: 50, width: 60, height: 30, speed: 0.5 },
|
||||
{ x: 300, y: 100, width: 80, height: 40, speed: 0.3 },
|
||||
{ x: 500, y: 80, width: 70, height: 35, speed: 0.4 }
|
||||
];
|
||||
|
||||
function jump() {
|
||||
if (!gameStarted) {
|
||||
gameStarted = true;
|
||||
gameRunning = true;
|
||||
}
|
||||
if (gameRunning) {
|
||||
bird.velocity = bird.jumpPower;
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
particles.push({
|
||||
x: bird.x - 10,
|
||||
y: bird.y + Math.random() * 10 - 5,
|
||||
vx: -Math.random() * 2 - 1,
|
||||
vy: Math.random() * 2 - 1,
|
||||
life: 1.0,
|
||||
color: '#fff'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createPipe() {
|
||||
const minHeight = 100;
|
||||
const maxHeight = canvas.height - pipeGap - minHeight;
|
||||
const topHeight = Math.random() * (maxHeight - minHeight) + minHeight;
|
||||
|
||||
pipes.push({
|
||||
x: canvas.width,
|
||||
topHeight: topHeight,
|
||||
bottomY: topHeight + pipeGap,
|
||||
passed: false
|
||||
});
|
||||
}
|
||||
|
||||
function updateBird() {
|
||||
if (!gameStarted) return;
|
||||
|
||||
bird.velocity += bird.gravity;
|
||||
bird.y += bird.velocity;
|
||||
|
||||
bird.rotation = Math.min(Math.max(bird.velocity * 3, -30), 90);
|
||||
|
||||
if (bird.y - bird.radius < 0) {
|
||||
bird.y = bird.radius;
|
||||
bird.velocity = 0;
|
||||
}
|
||||
|
||||
if (bird.y + bird.radius > canvas.height) {
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePipes() {
|
||||
if (!gameStarted) return;
|
||||
|
||||
pipeTimer++;
|
||||
if (pipeTimer > 90) {
|
||||
createPipe();
|
||||
pipeTimer = 0;
|
||||
}
|
||||
|
||||
for (let i = pipes.length - 1; i >= 0; i--) {
|
||||
const pipe = pipes[i];
|
||||
pipe.x -= pipeSpeed;
|
||||
|
||||
if (pipe.x + pipeWidth < bird.x && !pipe.passed) {
|
||||
pipe.passed = true;
|
||||
score++;
|
||||
scoreElement.textContent = score;
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
for (let j = 0; j < 10; j++) {
|
||||
particles.push({
|
||||
x: bird.x,
|
||||
y: bird.y,
|
||||
vx: Math.random() * 4 - 2,
|
||||
vy: Math.random() * 4 - 2,
|
||||
life: 1.0,
|
||||
color: '#f39c12'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (pipe.x + pipeWidth < 0) {
|
||||
pipes.splice(i, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bird.x + bird.radius > pipe.x &&
|
||||
bird.x - bird.radius < pipe.x + pipeWidth) {
|
||||
if (bird.y - bird.radius < pipe.topHeight ||
|
||||
bird.y + bird.radius > pipe.bottomY) {
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const particle = particles[i];
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.life -= 0.02;
|
||||
|
||||
if (particle.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateClouds() {
|
||||
clouds.forEach(cloud => {
|
||||
cloud.x -= cloud.speed;
|
||||
if (cloud.x + cloud.width < 0) {
|
||||
cloud.x = canvas.width + Math.random() * 100;
|
||||
cloud.y = Math.random() * 150;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function drawBackground() {
|
||||
// Himmel-Gradient
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
|
||||
gradient.addColorStop(0, '#87CEEB');
|
||||
gradient.addColorStop(1, '#98D8E8');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Wolken
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
clouds.forEach(cloud => {
|
||||
ctx.beginPath();
|
||||
ctx.arc(cloud.x, cloud.y, cloud.width/3, 0, Math.PI * 2);
|
||||
ctx.arc(cloud.x + cloud.width/3, cloud.y, cloud.width/2.5, 0, Math.PI * 2);
|
||||
ctx.arc(cloud.x + cloud.width/1.5, cloud.y, cloud.width/3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
function drawBird() {
|
||||
ctx.save();
|
||||
ctx.translate(bird.x, bird.y);
|
||||
ctx.rotate(bird.rotation * Math.PI / 180);
|
||||
|
||||
ctx.fillStyle = bird.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, bird.radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.beginPath();
|
||||
ctx.arc(5, -5, 5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#000';
|
||||
ctx.beginPath();
|
||||
ctx.arc(7, -5, 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
ctx.fillStyle = '#e67e22';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bird.radius - 5, 0);
|
||||
ctx.lineTo(bird.radius + 5, 3);
|
||||
ctx.lineTo(bird.radius + 5, -3);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawPipes() {
|
||||
pipes.forEach(pipe => {
|
||||
const gradient = ctx.createLinearGradient(pipe.x, 0, pipe.x + pipeWidth, 0);
|
||||
gradient.addColorStop(0, '#2ecc71');
|
||||
gradient.addColorStop(0.5, '#27ae60');
|
||||
gradient.addColorStop(1, '#229954');
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(pipe.x, 0, pipeWidth, pipe.topHeight);
|
||||
ctx.fillRect(pipe.x, pipe.bottomY, pipeWidth, canvas.height - pipe.bottomY);
|
||||
|
||||
ctx.fillStyle = '#27ae60';
|
||||
ctx.fillRect(pipe.x - 5, pipe.topHeight - 30, pipeWidth + 10, 30);
|
||||
ctx.fillRect(pipe.x - 5, pipe.bottomY, pipeWidth + 10, 30);
|
||||
|
||||
ctx.strokeStyle = '#1e7e34';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(pipe.x, 0, pipeWidth, pipe.topHeight);
|
||||
ctx.strokeRect(pipe.x, pipe.bottomY, pipeWidth, canvas.height - pipe.bottomY);
|
||||
});
|
||||
}
|
||||
|
||||
function drawParticles() {
|
||||
particles.forEach(particle => {
|
||||
ctx.globalAlpha = particle.life;
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.fillRect(particle.x - 2, particle.y - 2, 4, 4);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
drawBackground();
|
||||
updateClouds();
|
||||
|
||||
if (gameRunning) {
|
||||
updateBird();
|
||||
updatePipes();
|
||||
}
|
||||
|
||||
updateParticles();
|
||||
|
||||
drawPipes();
|
||||
drawBird();
|
||||
drawParticles();
|
||||
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
if (animationId) {
|
||||
cancelAnimationFrame(animationId);
|
||||
animationId = null;
|
||||
}
|
||||
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
localStorage.setItem('flappyManaBest', bestScore);
|
||||
}
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
if (score >= 50) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'flappy-expert',
|
||||
name: 'Flappy Experte',
|
||||
description: '50 Röhren gemeistert!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
finalScoreElement.textContent = score;
|
||||
bestScoreElement.textContent = bestScore;
|
||||
gameOverElement.style.display = 'block';
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
startScreen.style.display = 'none';
|
||||
gameRunning = false;
|
||||
gameStarted = false;
|
||||
score = 0;
|
||||
scoreElement.textContent = score;
|
||||
bird.x = 100;
|
||||
bird.y = canvas.height / 2;
|
||||
bird.velocity = 0;
|
||||
bird.rotation = 0;
|
||||
pipes.length = 0;
|
||||
particles.length = 0;
|
||||
pipeTimer = 0;
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
|
||||
if (!animationId) {
|
||||
animationId = requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
gameOverElement.style.display = 'none';
|
||||
startGame();
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', jump);
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault();
|
||||
jump();
|
||||
}
|
||||
});
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
202
games/mana-games/apps/web/public/games/game-stats-example.html
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Game Stats Integration Example</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #00ff88;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #00ff88;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
code {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #00ff88;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #00cc6a;
|
||||
}
|
||||
|
||||
.test-buttons {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Game Stats Integration Guide</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>1. Spiel laden</h2>
|
||||
<p>Sende diese Nachricht wenn dein Spiel startet:</p>
|
||||
<pre><code>window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: 'dein-spiel-slug'
|
||||
}, '*');</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>2. Score aktualisieren</h2>
|
||||
<p>Sende diese Nachricht wenn sich der Score ändert:</p>
|
||||
<pre><code>window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'dein-spiel-slug',
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: 1250 }
|
||||
}, '*');</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>3. Game Over</h2>
|
||||
<p>Sende diese Nachricht wenn das Spiel endet:</p>
|
||||
<pre><code>window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'dein-spiel-slug',
|
||||
event: 'GAME_OVER',
|
||||
data: { score: 1250 }
|
||||
}, '*');</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>4. Achievement freischalten</h2>
|
||||
<p>Sende diese Nachricht für Achievements:</p>
|
||||
<pre><code>window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'dein-spiel-slug',
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'first-win',
|
||||
name: 'Erster Sieg',
|
||||
description: 'Gewinne dein erstes Spiel'
|
||||
}
|
||||
}
|
||||
}, '*');</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>5. Spiel beenden</h2>
|
||||
<p>Optional: Sende diese Nachricht beim Verlassen:</p>
|
||||
<pre><code>window.parent.postMessage({
|
||||
type: 'GAME_ENDED',
|
||||
gameId: 'dein-spiel-slug'
|
||||
}, '*');</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Test-Buttons</h2>
|
||||
<p>Teste die Integration mit diesen Buttons:</p>
|
||||
<div class="test-buttons">
|
||||
<button onclick="sendGameLoaded()">Game Loaded</button>
|
||||
<button onclick="sendScore(100)">Score: 100</button>
|
||||
<button onclick="sendScore(500)">Score: 500</button>
|
||||
<button onclick="sendGameOver(750)">Game Over (750)</button>
|
||||
<button onclick="sendAchievement()">Achievement</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Example game ID
|
||||
const GAME_ID = 'test-game';
|
||||
|
||||
// Send game loaded event on page load
|
||||
window.addEventListener('load', () => {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
});
|
||||
|
||||
function sendGameLoaded() {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
console.log('Sent: GAME_LOADED');
|
||||
}
|
||||
|
||||
function sendScore(score) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score }
|
||||
}, '*');
|
||||
console.log('Sent: SCORE_UPDATE', score);
|
||||
}
|
||||
|
||||
function sendGameOver(score) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score }
|
||||
}, '*');
|
||||
console.log('Sent: GAME_OVER', score);
|
||||
}
|
||||
|
||||
function sendAchievement() {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'test-achievement',
|
||||
name: 'Test Erfolg',
|
||||
description: 'Du hast den Test-Button gedrückt!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
console.log('Sent: ACHIEVEMENT_UNLOCKED');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
483
games/mana-games/apps/web/public/games/gravity_painter.html
Normal file
|
|
@ -0,0 +1,483 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gravity Painter</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #0a0a0a, #1a1a2e);
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
#gameContainer {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: radial-gradient(circle at center, #0f0f23, #000);
|
||||
}
|
||||
|
||||
#ui {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
color: #fff;
|
||||
z-index: 10;
|
||||
font-size: 18px;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
#instructions {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 20px;
|
||||
color: #aaa;
|
||||
z-index: 10;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#targetPattern {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border: 2px solid #00ff88;
|
||||
background: rgba(0,255,136,0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.gravity-point {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, #ff0066, #ff0066, transparent);
|
||||
pointer-events: none;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 0.8; }
|
||||
50% { transform: scale(1.5); opacity: 0.4; }
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hit-effect {
|
||||
position: absolute;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, #00ff88, transparent);
|
||||
pointer-events: none;
|
||||
animation: hit 0.5s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes hit {
|
||||
0% { transform: scale(0); opacity: 1; }
|
||||
100% { transform: scale(2); opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="gameContainer">
|
||||
<canvas id="canvas"></canvas>
|
||||
<div id="ui">
|
||||
<div>Score: <span id="score">0</span></div>
|
||||
<div>Level: <span id="level">1</span></div>
|
||||
<div>Particles: <span id="particles">10</span></div>
|
||||
</div>
|
||||
<div id="instructions">
|
||||
Klicke um Gravitationspunkte zu setzen • Leertaste für Partikel • Treffe die grünen Ziele!
|
||||
</div>
|
||||
<canvas id="targetPattern"></canvas>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'gravity-painter';
|
||||
|
||||
class GravityPainter {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.targetCanvas = document.getElementById('targetPattern');
|
||||
this.targetCtx = this.targetCanvas.getContext('2d');
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
|
||||
this.gravityPoints = [];
|
||||
this.particles = [];
|
||||
this.targets = [];
|
||||
this.score = 0;
|
||||
this.level = 1;
|
||||
this.particlesLeft = 10;
|
||||
this.colors = ['#ff0066', '#00ff88', '#0066ff', '#ffff00', '#ff6600', '#9900ff'];
|
||||
|
||||
this.setupEventListeners();
|
||||
this.generateTargets();
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
this.targetCanvas.width = 120;
|
||||
this.targetCanvas.height = 120;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.canvas.addEventListener('click', (e) => {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
this.addGravityPoint(x, y);
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'Space' && this.particlesLeft > 0) {
|
||||
e.preventDefault();
|
||||
this.shootParticle();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addGravityPoint(x, y) {
|
||||
this.gravityPoints.push({
|
||||
x: x,
|
||||
y: y,
|
||||
strength: 20000,
|
||||
life: 500
|
||||
});
|
||||
}
|
||||
|
||||
shootParticle() {
|
||||
if (this.particlesLeft <= 0) return;
|
||||
|
||||
const startX = 50;
|
||||
const startY = this.canvas.height / 2;
|
||||
const angle = (Math.random() - 0.5) * 0.8;
|
||||
const speed = 2;
|
||||
|
||||
this.particles.push({
|
||||
x: startX,
|
||||
y: startY,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed,
|
||||
color: this.colors[Math.floor(Math.random() * this.colors.length)],
|
||||
trail: [],
|
||||
life: 300
|
||||
});
|
||||
|
||||
this.particlesLeft--;
|
||||
document.getElementById('particles').textContent = this.particlesLeft;
|
||||
}
|
||||
|
||||
generateTargets() {
|
||||
this.targets = [];
|
||||
const patterns = [
|
||||
// Kreis
|
||||
() => {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = (i / 8) * Math.PI * 2;
|
||||
const x = this.canvas.width * 0.7 + Math.cos(angle) * 80;
|
||||
const y = this.canvas.height * 0.5 + Math.sin(angle) * 80;
|
||||
this.targets.push({x, y, hit: false, radius: 15});
|
||||
}
|
||||
},
|
||||
// Stern
|
||||
() => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i / 5) * Math.PI * 2;
|
||||
const x = this.canvas.width * 0.7 + Math.cos(angle) * 100;
|
||||
const y = this.canvas.height * 0.5 + Math.sin(angle) * 100;
|
||||
this.targets.push({x, y, hit: false, radius: 15});
|
||||
}
|
||||
},
|
||||
// Spiral
|
||||
() => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const angle = (i / 10) * Math.PI * 4;
|
||||
const radius = 20 + i * 8;
|
||||
const x = this.canvas.width * 0.7 + Math.cos(angle) * radius;
|
||||
const y = this.canvas.height * 0.5 + Math.sin(angle) * radius;
|
||||
this.targets.push({x, y, hit: false, radius: 12});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const pattern = patterns[Math.floor(Math.random() * patterns.length)];
|
||||
pattern();
|
||||
|
||||
this.drawTargetPattern();
|
||||
}
|
||||
|
||||
drawTargetPattern() {
|
||||
this.targetCtx.clearRect(0, 0, 120, 120);
|
||||
this.targetCtx.fillStyle = '#00ff88';
|
||||
|
||||
// Miniaturansicht der Ziele
|
||||
const scaleX = 120 / this.canvas.width;
|
||||
const scaleY = 120 / this.canvas.height;
|
||||
|
||||
this.targets.forEach(target => {
|
||||
const x = target.x * scaleX;
|
||||
const y = target.y * scaleY;
|
||||
|
||||
this.targetCtx.beginPath();
|
||||
this.targetCtx.arc(x, y, 3, 0, Math.PI * 2);
|
||||
this.targetCtx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
update() {
|
||||
// Gravitation Points updaten
|
||||
this.gravityPoints = this.gravityPoints.filter(point => {
|
||||
point.life--;
|
||||
return point.life > 0;
|
||||
});
|
||||
|
||||
// Partikel updaten
|
||||
this.particles.forEach(particle => {
|
||||
// Gravitationseffekt
|
||||
this.gravityPoints.forEach(gp => {
|
||||
const dx = gp.x - particle.x;
|
||||
const dy = gp.y - particle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance > 10 && distance < 400) {
|
||||
const force = gp.strength / (distance * 50);
|
||||
const forceX = (dx / distance) * force * 0.1;
|
||||
const forceY = (dy / distance) * force * 0.1;
|
||||
|
||||
particle.vx += forceX;
|
||||
particle.vy += forceY;
|
||||
}
|
||||
});
|
||||
|
||||
// Trail hinzufügen
|
||||
particle.trail.push({x: particle.x, y: particle.y});
|
||||
if (particle.trail.length > 20) {
|
||||
particle.trail.shift();
|
||||
}
|
||||
|
||||
// Position updaten
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
// Lebensdauer
|
||||
particle.life--;
|
||||
|
||||
// Kollision mit Zielen
|
||||
this.targets.forEach(target => {
|
||||
if (!target.hit) {
|
||||
const dx = target.x - particle.x;
|
||||
const dy = target.y - particle.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < target.radius) {
|
||||
target.hit = true;
|
||||
this.score += 100;
|
||||
document.getElementById('score').textContent = this.score;
|
||||
this.createHitEffect(target.x, target.y);
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: this.score }
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Tote Partikel entfernen
|
||||
this.particles = this.particles.filter(p =>
|
||||
p.life > 0 &&
|
||||
p.x > -50 && p.x < this.canvas.width + 50 &&
|
||||
p.y > -50 && p.y < this.canvas.height + 50
|
||||
);
|
||||
|
||||
// Level prüfen
|
||||
if (this.targets.every(t => t.hit)) {
|
||||
this.nextLevel();
|
||||
} else if (this.particlesLeft <= 0 && this.particles.length === 0) {
|
||||
this.resetLevel();
|
||||
}
|
||||
}
|
||||
|
||||
createHitEffect(x, y) {
|
||||
const effect = document.createElement('div');
|
||||
effect.className = 'hit-effect';
|
||||
effect.style.left = (x - 15) + 'px';
|
||||
effect.style.top = (y - 15) + 'px';
|
||||
document.body.appendChild(effect);
|
||||
|
||||
setTimeout(() => {
|
||||
effect.remove();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
nextLevel() {
|
||||
this.level++;
|
||||
this.particlesLeft = Math.max(5, 15 - this.level);
|
||||
document.getElementById('level').textContent = this.level;
|
||||
document.getElementById('particles').textContent = this.particlesLeft;
|
||||
|
||||
this.gravityPoints = [];
|
||||
this.particles = [];
|
||||
this.generateTargets();
|
||||
}
|
||||
|
||||
resetLevel() {
|
||||
// Game Over wenn keine Partikel mehr
|
||||
if (this.particlesLeft <= 0 && this.particles.length === 0) {
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: this.score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (this.score >= 500) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'gravity_artist',
|
||||
name: 'Gravity Artist',
|
||||
description: 'Score 500 points in Gravity Painter',
|
||||
icon: '🎨'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (this.level >= 5) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'pattern_master',
|
||||
name: 'Pattern Master',
|
||||
description: 'Reach level 5 in Gravity Painter',
|
||||
icon: '🌌'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
this.particlesLeft = Math.max(5, 15 - this.level);
|
||||
document.getElementById('particles').textContent = this.particlesLeft;
|
||||
|
||||
this.gravityPoints = [];
|
||||
this.particles = [];
|
||||
this.targets.forEach(t => t.hit = false);
|
||||
}
|
||||
|
||||
draw() {
|
||||
// Canvas leeren
|
||||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Gravitationspunkte zeichnen
|
||||
this.gravityPoints.forEach(gp => {
|
||||
const alpha = gp.life / 300;
|
||||
this.ctx.fillStyle = `rgba(255, 0, 102, ${alpha * 0.3})`;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(gp.x, gp.y, 30, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = `rgba(255, 0, 102, ${alpha})`;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(gp.x, gp.y, 8, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
});
|
||||
|
||||
// Partikel und Trails zeichnen
|
||||
this.particles.forEach(particle => {
|
||||
// Trail
|
||||
this.ctx.strokeStyle = particle.color + '66';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
|
||||
particle.trail.forEach((point, index) => {
|
||||
if (index === 0) {
|
||||
this.ctx.moveTo(point.x, point.y);
|
||||
} else {
|
||||
this.ctx.lineTo(point.x, point.y);
|
||||
}
|
||||
});
|
||||
this.ctx.stroke();
|
||||
|
||||
// Partikel
|
||||
this.ctx.fillStyle = particle.color;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
// Glühen
|
||||
this.ctx.fillStyle = particle.color + '44';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(particle.x, particle.y, 8, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
});
|
||||
|
||||
// Ziele zeichnen
|
||||
this.targets.forEach(target => {
|
||||
if (!target.hit) {
|
||||
this.ctx.fillStyle = '#00ff88';
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(target.x, target.y, target.radius, 0, Math.PI * 2);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.strokeStyle = '#00ff88';
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(target.x, target.y, target.radius + 5, 0, Math.PI * 2);
|
||||
this.ctx.stroke();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
gameLoop() {
|
||||
this.update();
|
||||
this.draw();
|
||||
requestAnimationFrame(() => this.gameLoop());
|
||||
}
|
||||
}
|
||||
|
||||
// Spiel starten
|
||||
const game = new GravityPainter();
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1124
games/mana-games/apps/web/public/games/mana_defense.html
Normal file
966
games/mana-games/apps/web/public/games/mana_factory.html
Normal file
|
|
@ -0,0 +1,966 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mana Factory</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #0a0a0a;
|
||||
color: #00ffff;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.main-panel {
|
||||
flex: 1;
|
||||
background: rgba(0, 20, 20, 0.8);
|
||||
border: 2px solid #00ffff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px #00ffff;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
width: 350px;
|
||||
background: rgba(0, 20, 20, 0.8);
|
||||
border: 2px solid #00ffff;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 20px #00ffff;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
text-align: center;
|
||||
text-shadow: 0 0 10px #00ffff;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.mana-display {
|
||||
text-align: center;
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
text-shadow: 0 0 15px #00ffff;
|
||||
}
|
||||
|
||||
.mana-per-second {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
color: #00cccc;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.click-button {
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 30px auto;
|
||||
background: radial-gradient(circle, #00ffff, #006666);
|
||||
border: 3px solid #00ffff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 24px;
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 30px #00ffff;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.click-button:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 40px #00ffff;
|
||||
}
|
||||
|
||||
.click-button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.click-button::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.click-button.pulse::after {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.generator {
|
||||
background: rgba(0, 40, 40, 0.6);
|
||||
border: 2px solid #006666;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.generator:hover {
|
||||
border-color: #00ffff;
|
||||
box-shadow: 0 0 10px #00ffff;
|
||||
}
|
||||
|
||||
.generator.affordable {
|
||||
border-color: #00ff00;
|
||||
}
|
||||
|
||||
.generator.maxed {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.generator-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.generator-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.generator-count {
|
||||
font-size: 24px;
|
||||
color: #ffff00;
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.generator-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #00cccc;
|
||||
}
|
||||
|
||||
.generator-cost {
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.generator-cost.affordable {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.tab-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 2px solid #00ffff;
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: 10px 20px;
|
||||
background: rgba(0, 40, 40, 0.6);
|
||||
border: 2px solid #006666;
|
||||
border-bottom: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
background: rgba(0, 60, 60, 0.8);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background: rgba(0, 80, 80, 0.9);
|
||||
border-color: #00ffff;
|
||||
color: #00ffff;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.upgrade {
|
||||
background: rgba(0, 40, 40, 0.6);
|
||||
border: 2px solid #006666;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.upgrade:hover:not(.bought) {
|
||||
border-color: #00ffff;
|
||||
box-shadow: 0 0 10px #00ffff;
|
||||
}
|
||||
|
||||
.upgrade.affordable {
|
||||
border-color: #00ff00;
|
||||
}
|
||||
|
||||
.upgrade.bought {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
border-color: #666666;
|
||||
}
|
||||
|
||||
.upgrade-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.upgrade-description {
|
||||
font-size: 12px;
|
||||
color: #00cccc;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.upgrade-cost {
|
||||
font-size: 14px;
|
||||
color: #ff6666;
|
||||
}
|
||||
|
||||
.upgrade-cost.affordable {
|
||||
color: #00ff00;
|
||||
}
|
||||
|
||||
.prestige-button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
background: linear-gradient(45deg, #ff00ff, #00ffff);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #000;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 0 20px #ff00ff;
|
||||
}
|
||||
|
||||
.prestige-button:hover:not(:disabled) {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 30px #ff00ff;
|
||||
}
|
||||
|
||||
.prestige-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.achievement {
|
||||
background: rgba(0, 40, 40, 0.6);
|
||||
border: 2px solid #666666;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: 8px 0;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.achievement.unlocked {
|
||||
border-color: #ffff00;
|
||||
box-shadow: 0 0 10px #ffff00;
|
||||
}
|
||||
|
||||
.achievement-name {
|
||||
font-weight: bold;
|
||||
color: #ffff00;
|
||||
}
|
||||
|
||||
.achievement-description {
|
||||
font-size: 12px;
|
||||
color: #00cccc;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #003333;
|
||||
}
|
||||
|
||||
.floating-text {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #00ffff;
|
||||
text-shadow: 0 0 5px #00ffff;
|
||||
animation: float-up 1s ease-out;
|
||||
}
|
||||
|
||||
@keyframes float-up {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
}
|
||||
|
||||
.particles {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.particle {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #00ffff;
|
||||
border-radius: 50%;
|
||||
opacity: 0.6;
|
||||
animation: particle-fall 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes particle-fall {
|
||||
0% {
|
||||
transform: translateY(-100px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(100vh);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="particles" id="particles"></div>
|
||||
|
||||
<div class="container">
|
||||
<div class="main-panel">
|
||||
<h1>🏭 Mana Factory 🏭</h1>
|
||||
|
||||
<div class="mana-display">
|
||||
💎 <span id="mana">0</span> Mana
|
||||
</div>
|
||||
|
||||
<div class="mana-per-second">
|
||||
<span id="mps">0</span> Mana/Sek
|
||||
</div>
|
||||
|
||||
<button class="click-button" id="clickButton">
|
||||
Kristall<br>Ernten
|
||||
</button>
|
||||
|
||||
<div class="tab-container">
|
||||
<div class="tab active" data-tab="generators">Generatoren</div>
|
||||
<div class="tab" data-tab="upgrades">Upgrades</div>
|
||||
<div class="tab" data-tab="prestige">Aufstieg</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content active" id="generators">
|
||||
<div class="generator" data-generator="fountain">
|
||||
<div class="generator-header">
|
||||
<span class="generator-name">💧 Mana-Brunnen</span>
|
||||
<span class="generator-count">0</span>
|
||||
</div>
|
||||
<div class="generator-info">
|
||||
<span>Produziert: <span class="production">1</span> Mana/s</span>
|
||||
<span class="generator-cost">Kosten: <span class="cost">10</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="generator" data-generator="mine">
|
||||
<div class="generator-header">
|
||||
<span class="generator-name">⛏️ Kristall-Mine</span>
|
||||
<span class="generator-count">0</span>
|
||||
</div>
|
||||
<div class="generator-info">
|
||||
<span>Produziert: <span class="production">10</span> Mana/s</span>
|
||||
<span class="generator-cost">Kosten: <span class="cost">100</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="generator" data-generator="reactor">
|
||||
<div class="generator-header">
|
||||
<span class="generator-name">⚡ Mana-Reaktor</span>
|
||||
<span class="generator-count">0</span>
|
||||
</div>
|
||||
<div class="generator-info">
|
||||
<span>Produziert: <span class="production">100</span> Mana/s</span>
|
||||
<span class="generator-cost">Kosten: <span class="cost">1000</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="generator" data-generator="portal">
|
||||
<div class="generator-header">
|
||||
<span class="generator-name">🌀 Dimensions-Portal</span>
|
||||
<span class="generator-count">0</span>
|
||||
</div>
|
||||
<div class="generator-info">
|
||||
<span>Produziert: <span class="production">1000</span> Mana/s</span>
|
||||
<span class="generator-cost">Kosten: <span class="cost">10000</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="generator" data-generator="nexus">
|
||||
<div class="generator-header">
|
||||
<span class="generator-name">🌟 Mana-Nexus</span>
|
||||
<span class="generator-count">0</span>
|
||||
</div>
|
||||
<div class="generator-info">
|
||||
<span>Produziert: <span class="production">10000</span> Mana/s</span>
|
||||
<span class="generator-cost">Kosten: <span class="cost">100000</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="upgrades">
|
||||
<h3>Verbesserungen</h3>
|
||||
<div id="upgradesList"></div>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="prestige">
|
||||
<h3>Aufstieg</h3>
|
||||
<p style="text-align: center; margin: 20px 0;">
|
||||
Setze deinen Fortschritt zurück und erhalte Prestige-Punkte für permanente Boni!
|
||||
</p>
|
||||
<p style="text-align: center; font-size: 24px; color: #ff00ff;">
|
||||
Prestige-Punkte: <span id="prestigePoints">0</span>
|
||||
</p>
|
||||
<p style="text-align: center; font-size: 18px; color: #00ffff;">
|
||||
Nächster Aufstieg: <span id="nextPrestige">0</span> Punkte
|
||||
</p>
|
||||
<p style="text-align: center; font-size: 16px; color: #00cccc;">
|
||||
Multiplikator: x<span id="prestigeMultiplier">1</span>
|
||||
</p>
|
||||
<button class="prestige-button" id="prestigeButton" disabled>
|
||||
Aufstieg durchführen<br>
|
||||
(Benötigt 1,000,000 Mana)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-panel">
|
||||
<h2>Statistiken</h2>
|
||||
<div class="stats">
|
||||
<div class="stats-row">
|
||||
<span>Gesamtes Mana:</span>
|
||||
<span id="totalMana">0</span>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>Mana durch Klicks:</span>
|
||||
<span id="clickMana">0</span>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>Mana durch Generatoren:</span>
|
||||
<span id="generatorMana">0</span>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>Klicks gesamt:</span>
|
||||
<span id="totalClicks">0</span>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>Spielzeit:</span>
|
||||
<span id="playTime">0:00</span>
|
||||
</div>
|
||||
<div class="stats-row">
|
||||
<span>Aufstiege:</span>
|
||||
<span id="totalPrestiges">0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 30px;">Erfolge</h3>
|
||||
<div id="achievementsList">
|
||||
<div class="achievement" data-achievement="first-click">
|
||||
<div class="achievement-name">🎯 Erster Klick</div>
|
||||
<div class="achievement-description">Ernte deinen ersten Kristall</div>
|
||||
</div>
|
||||
<div class="achievement" data-achievement="first-generator">
|
||||
<div class="achievement-name">🏗️ Industrialisierung</div>
|
||||
<div class="achievement-description">Kaufe deinen ersten Generator</div>
|
||||
</div>
|
||||
<div class="achievement" data-achievement="hundred-mana">
|
||||
<div class="achievement-name">💯 Hundert!</div>
|
||||
<div class="achievement-description">Erreiche 100 Mana</div>
|
||||
</div>
|
||||
<div class="achievement" data-achievement="thousand-mana">
|
||||
<div class="achievement-name">📈 Tausender</div>
|
||||
<div class="achievement-description">Erreiche 1,000 Mana</div>
|
||||
</div>
|
||||
<div class="achievement" data-achievement="first-prestige">
|
||||
<div class="achievement-name">⭐ Aufgestiegen</div>
|
||||
<div class="achievement-description">Führe deinen ersten Aufstieg durch</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let gameData = {
|
||||
mana: 0,
|
||||
manaPerClick: 1,
|
||||
manaPerSecond: 0,
|
||||
totalMana: 0,
|
||||
clickMana: 0,
|
||||
generatorMana: 0,
|
||||
totalClicks: 0,
|
||||
playTime: 0,
|
||||
prestigePoints: 0,
|
||||
prestigeMultiplier: 1,
|
||||
totalPrestiges: 0,
|
||||
generators: {
|
||||
fountain: { count: 0, baseCost: 10, baseProduction: 1 },
|
||||
mine: { count: 0, baseCost: 100, baseProduction: 10 },
|
||||
reactor: { count: 0, baseCost: 1000, baseProduction: 100 },
|
||||
portal: { count: 0, baseCost: 10000, baseProduction: 1000 },
|
||||
nexus: { count: 0, baseCost: 100000, baseProduction: 10000 }
|
||||
},
|
||||
upgrades: {},
|
||||
achievements: {},
|
||||
lastSave: Date.now(),
|
||||
lastUpdate: Date.now()
|
||||
};
|
||||
|
||||
const upgrades = [
|
||||
{
|
||||
id: 'click1',
|
||||
name: '✨ Verbesserter Griff',
|
||||
description: 'Doppelte Mana pro Klick',
|
||||
cost: 50,
|
||||
effect: () => { gameData.manaPerClick *= 2; }
|
||||
},
|
||||
{
|
||||
id: 'fountain1',
|
||||
name: '💧 Tiefere Brunnen',
|
||||
description: 'Mana-Brunnen sind doppelt so effektiv',
|
||||
cost: 500,
|
||||
requirement: () => gameData.generators.fountain.count >= 5,
|
||||
effect: () => { gameData.generators.fountain.baseProduction *= 2; }
|
||||
},
|
||||
{
|
||||
id: 'mine1',
|
||||
name: '⛏️ Diamant-Spitzhacken',
|
||||
description: 'Kristall-Minen sind doppelt so effektiv',
|
||||
cost: 5000,
|
||||
requirement: () => gameData.generators.mine.count >= 5,
|
||||
effect: () => { gameData.generators.mine.baseProduction *= 2; }
|
||||
},
|
||||
{
|
||||
id: 'click2',
|
||||
name: '🌟 Magische Hände',
|
||||
description: '5x Mana pro Klick',
|
||||
cost: 10000,
|
||||
requirement: () => gameData.totalClicks >= 100,
|
||||
effect: () => { gameData.manaPerClick *= 5; }
|
||||
},
|
||||
{
|
||||
id: 'global1',
|
||||
name: '🌈 Synergie',
|
||||
description: 'Alle Generatoren sind 50% effektiver',
|
||||
cost: 50000,
|
||||
requirement: () => gameData.manaPerSecond >= 100,
|
||||
effect: () => {
|
||||
for (let gen in gameData.generators) {
|
||||
gameData.generators[gen].baseProduction *= 1.5;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'auto1',
|
||||
name: '🤖 Auto-Klicker',
|
||||
description: 'Generiert 10% deiner Klick-Mana pro Sekunde',
|
||||
cost: 100000,
|
||||
requirement: () => gameData.totalClicks >= 1000,
|
||||
effect: () => { }
|
||||
}
|
||||
];
|
||||
|
||||
function formatNumber(num) {
|
||||
if (num < 1000) return Math.floor(num).toString();
|
||||
if (num < 1000000) return (num / 1000).toFixed(1) + 'K';
|
||||
if (num < 1000000000) return (num / 1000000).toFixed(1) + 'M';
|
||||
if (num < 1000000000000) return (num / 1000000000).toFixed(1) + 'B';
|
||||
return (num / 1000000000000).toFixed(1) + 'T';
|
||||
}
|
||||
|
||||
function formatTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${Math.floor(seconds / 60)}:${(seconds % 60).toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function getGeneratorCost(type) {
|
||||
const gen = gameData.generators[type];
|
||||
return Math.floor(gen.baseCost * Math.pow(1.15, gen.count));
|
||||
}
|
||||
|
||||
function getGeneratorProduction(type) {
|
||||
const gen = gameData.generators[type];
|
||||
let production = gen.baseProduction * gen.count;
|
||||
|
||||
if (type === 'fountain' && gameData.upgrades.fountain1) production *= 2;
|
||||
if (type === 'mine' && gameData.upgrades.mine1) production *= 2;
|
||||
if (gameData.upgrades.global1) production *= 1.5;
|
||||
|
||||
return production * gameData.prestigeMultiplier;
|
||||
}
|
||||
|
||||
function calculateManaPerSecond() {
|
||||
let mps = 0;
|
||||
for (let type in gameData.generators) {
|
||||
mps += getGeneratorProduction(type);
|
||||
}
|
||||
|
||||
if (gameData.upgrades.auto1) {
|
||||
mps += gameData.manaPerClick * 0.1;
|
||||
}
|
||||
|
||||
return mps;
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
document.getElementById('mana').textContent = formatNumber(gameData.mana);
|
||||
document.getElementById('mps').textContent = formatNumber(gameData.manaPerSecond);
|
||||
document.getElementById('totalMana').textContent = formatNumber(gameData.totalMana);
|
||||
document.getElementById('clickMana').textContent = formatNumber(gameData.clickMana);
|
||||
document.getElementById('generatorMana').textContent = formatNumber(gameData.generatorMana);
|
||||
document.getElementById('totalClicks').textContent = formatNumber(gameData.totalClicks);
|
||||
document.getElementById('playTime').textContent = formatTime(gameData.playTime);
|
||||
document.getElementById('totalPrestiges').textContent = gameData.totalPrestiges;
|
||||
document.getElementById('prestigePoints').textContent = gameData.prestigePoints;
|
||||
document.getElementById('prestigeMultiplier').textContent = gameData.prestigeMultiplier.toFixed(1);
|
||||
|
||||
const nextPrestige = Math.floor(Math.sqrt(gameData.totalMana / 1000000));
|
||||
document.getElementById('nextPrestige').textContent = nextPrestige;
|
||||
|
||||
const prestigeButton = document.getElementById('prestigeButton');
|
||||
if (gameData.mana >= 1000000) {
|
||||
prestigeButton.disabled = false;
|
||||
} else {
|
||||
prestigeButton.disabled = true;
|
||||
}
|
||||
|
||||
for (let type in gameData.generators) {
|
||||
const gen = gameData.generators[type];
|
||||
const element = document.querySelector(`[data-generator="${type}"]`);
|
||||
const cost = getGeneratorCost(type);
|
||||
|
||||
element.querySelector('.generator-count').textContent = gen.count;
|
||||
element.querySelector('.cost').textContent = formatNumber(cost);
|
||||
element.querySelector('.production').textContent = formatNumber(gen.baseProduction);
|
||||
|
||||
if (gameData.mana >= cost) {
|
||||
element.classList.add('affordable');
|
||||
element.querySelector('.generator-cost').classList.add('affordable');
|
||||
} else {
|
||||
element.classList.remove('affordable');
|
||||
element.querySelector('.generator-cost').classList.remove('affordable');
|
||||
}
|
||||
}
|
||||
|
||||
updateUpgrades();
|
||||
}
|
||||
|
||||
function updateUpgrades() {
|
||||
const upgradesList = document.getElementById('upgradesList');
|
||||
upgradesList.innerHTML = '';
|
||||
|
||||
upgrades.forEach(upgrade => {
|
||||
if (gameData.upgrades[upgrade.id]) return;
|
||||
if (upgrade.requirement && !upgrade.requirement()) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'upgrade';
|
||||
if (gameData.mana >= upgrade.cost) {
|
||||
div.classList.add('affordable');
|
||||
}
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="upgrade-name">${upgrade.name}</div>
|
||||
<div class="upgrade-description">${upgrade.description}</div>
|
||||
<div class="upgrade-cost ${gameData.mana >= upgrade.cost ? 'affordable' : ''}">
|
||||
Kosten: ${formatNumber(upgrade.cost)} Mana
|
||||
</div>
|
||||
`;
|
||||
|
||||
div.onclick = () => buyUpgrade(upgrade);
|
||||
upgradesList.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function buyGenerator(type) {
|
||||
const cost = getGeneratorCost(type);
|
||||
if (gameData.mana >= cost) {
|
||||
gameData.mana -= cost;
|
||||
gameData.generators[type].count++;
|
||||
gameData.manaPerSecond = calculateManaPerSecond();
|
||||
|
||||
checkAchievement('first-generator');
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function buyUpgrade(upgrade) {
|
||||
if (gameData.mana >= upgrade.cost && !gameData.upgrades[upgrade.id]) {
|
||||
gameData.mana -= upgrade.cost;
|
||||
gameData.upgrades[upgrade.id] = true;
|
||||
upgrade.effect();
|
||||
gameData.manaPerSecond = calculateManaPerSecond();
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function clickCrystal(event) {
|
||||
const button = document.getElementById('clickButton');
|
||||
const rect = button.getBoundingClientRect();
|
||||
const x = event.clientX - rect.left;
|
||||
const y = event.clientY - rect.top;
|
||||
|
||||
const manaGained = gameData.manaPerClick * gameData.prestigeMultiplier;
|
||||
gameData.mana += manaGained;
|
||||
gameData.totalMana += manaGained;
|
||||
gameData.clickMana += manaGained;
|
||||
gameData.totalClicks++;
|
||||
|
||||
checkAchievement('first-click');
|
||||
|
||||
button.classList.add('pulse');
|
||||
setTimeout(() => button.classList.remove('pulse'), 600);
|
||||
|
||||
const floatingText = document.createElement('div');
|
||||
floatingText.className = 'floating-text';
|
||||
floatingText.textContent = `+${formatNumber(manaGained)}`;
|
||||
floatingText.style.left = `${event.clientX}px`;
|
||||
floatingText.style.top = `${event.clientY}px`;
|
||||
document.body.appendChild(floatingText);
|
||||
|
||||
setTimeout(() => floatingText.remove(), 1000);
|
||||
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function doPrestige() {
|
||||
if (gameData.mana >= 1000000) {
|
||||
const prestigeGain = Math.floor(Math.sqrt(gameData.totalMana / 1000000));
|
||||
|
||||
gameData.mana = 0;
|
||||
gameData.totalMana = 0;
|
||||
gameData.clickMana = 0;
|
||||
gameData.generatorMana = 0;
|
||||
gameData.totalClicks = 0;
|
||||
gameData.playTime = 0;
|
||||
|
||||
for (let type in gameData.generators) {
|
||||
gameData.generators[type].count = 0;
|
||||
}
|
||||
|
||||
gameData.upgrades = {};
|
||||
|
||||
gameData.prestigePoints += prestigeGain;
|
||||
gameData.prestigeMultiplier = 1 + (gameData.prestigePoints * 0.1);
|
||||
gameData.totalPrestiges++;
|
||||
|
||||
gameData.manaPerSecond = 0;
|
||||
|
||||
checkAchievement('first-prestige');
|
||||
updateDisplay();
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'mana-factory',
|
||||
event: 'PRESTIGE',
|
||||
data: { prestigePoints: gameData.prestigePoints }
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
function checkAchievement(id) {
|
||||
if (gameData.achievements[id]) return;
|
||||
|
||||
let unlocked = false;
|
||||
|
||||
switch(id) {
|
||||
case 'first-click':
|
||||
unlocked = gameData.totalClicks >= 1;
|
||||
break;
|
||||
case 'first-generator':
|
||||
unlocked = Object.values(gameData.generators).some(g => g.count > 0);
|
||||
break;
|
||||
case 'hundred-mana':
|
||||
unlocked = gameData.totalMana >= 100;
|
||||
break;
|
||||
case 'thousand-mana':
|
||||
unlocked = gameData.totalMana >= 1000;
|
||||
break;
|
||||
case 'first-prestige':
|
||||
unlocked = gameData.totalPrestiges >= 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (unlocked) {
|
||||
gameData.achievements[id] = true;
|
||||
const element = document.querySelector(`[data-achievement="${id}"]`);
|
||||
if (element) {
|
||||
element.classList.add('unlocked');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
const now = Date.now();
|
||||
const delta = (now - gameData.lastUpdate) / 1000;
|
||||
|
||||
if (gameData.manaPerSecond > 0) {
|
||||
const manaGained = gameData.manaPerSecond * delta;
|
||||
gameData.mana += manaGained;
|
||||
gameData.totalMana += manaGained;
|
||||
gameData.generatorMana += manaGained;
|
||||
}
|
||||
|
||||
gameData.playTime += delta;
|
||||
gameData.lastUpdate = now;
|
||||
|
||||
checkAchievement('hundred-mana');
|
||||
checkAchievement('thousand-mana');
|
||||
|
||||
updateDisplay();
|
||||
|
||||
if (now - gameData.lastSave > 10000) {
|
||||
saveGame();
|
||||
gameData.lastSave = now;
|
||||
}
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
localStorage.setItem('manaFactorySave', JSON.stringify(gameData));
|
||||
}
|
||||
|
||||
function loadGame() {
|
||||
const saved = localStorage.getItem('manaFactorySave');
|
||||
if (saved) {
|
||||
const loadedData = JSON.parse(saved);
|
||||
Object.assign(gameData, loadedData);
|
||||
|
||||
const offlineTime = (Date.now() - gameData.lastUpdate) / 1000;
|
||||
const maxOfflineTime = 3600 * 24;
|
||||
const actualOfflineTime = Math.min(offlineTime, maxOfflineTime);
|
||||
|
||||
if (gameData.manaPerSecond > 0 && actualOfflineTime > 1) {
|
||||
const offlineMana = gameData.manaPerSecond * actualOfflineTime * 0.5;
|
||||
gameData.mana += offlineMana;
|
||||
gameData.totalMana += offlineMana;
|
||||
gameData.generatorMana += offlineMana;
|
||||
|
||||
alert(`Willkommen zurück! Du hast ${formatNumber(offlineMana)} Mana während deiner Abwesenheit produziert.`);
|
||||
}
|
||||
|
||||
gameData.lastUpdate = Date.now();
|
||||
gameData.manaPerSecond = calculateManaPerSecond();
|
||||
|
||||
for (let id in gameData.achievements) {
|
||||
if (gameData.achievements[id]) {
|
||||
const element = document.querySelector(`[data-achievement="${id}"]`);
|
||||
if (element) element.classList.add('unlocked');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createParticles() {
|
||||
const particlesContainer = document.getElementById('particles');
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const particle = document.createElement('div');
|
||||
particle.className = 'particle';
|
||||
particle.style.left = Math.random() * 100 + '%';
|
||||
particle.style.animationDelay = Math.random() * 10 + 's';
|
||||
particle.style.animationDuration = (10 + Math.random() * 10) + 's';
|
||||
particlesContainer.appendChild(particle);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('clickButton').addEventListener('click', clickCrystal);
|
||||
|
||||
document.querySelectorAll('.generator').forEach(element => {
|
||||
element.addEventListener('click', () => {
|
||||
buyGenerator(element.dataset.generator);
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('prestigeButton').addEventListener('click', doPrestige);
|
||||
|
||||
document.querySelectorAll('.tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||
|
||||
tab.classList.add('active');
|
||||
document.getElementById(tab.dataset.tab).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
saveGame();
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'mana-factory',
|
||||
event: 'GAME_ENDED',
|
||||
data: {
|
||||
totalMana: gameData.totalMana,
|
||||
prestigePoints: gameData.prestigePoints
|
||||
}
|
||||
}, '*');
|
||||
});
|
||||
|
||||
createParticles();
|
||||
loadGame();
|
||||
updateDisplay();
|
||||
setInterval(gameLoop, 100);
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: 'mana-factory'
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
569
games/mana-games/apps/web/public/games/mana_runner.html
Normal file
|
|
@ -0,0 +1,569 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Mana Runner</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: 'Courier New', monospace;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
border: 2px solid #00ffff;
|
||||
box-shadow: 0 0 20px #00ffff;
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
#gameOver {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #00ffff;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
border: 2px solid #00ffff;
|
||||
box-shadow: 0 0 20px #00ffff;
|
||||
}
|
||||
|
||||
#gameOver h2 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 32px;
|
||||
text-shadow: 0 0 10px #00ffff;
|
||||
}
|
||||
|
||||
#gameOver p {
|
||||
margin: 10px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#gameOver button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 30px;
|
||||
font-size: 18px;
|
||||
background: #00ffff;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
#gameOver button:hover {
|
||||
background: #00cccc;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
#startScreen {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
color: #00ffff;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
border: 2px solid #00ffff;
|
||||
box-shadow: 0 0 20px #00ffff;
|
||||
}
|
||||
|
||||
#startScreen h1 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 36px;
|
||||
text-shadow: 0 0 10px #00ffff;
|
||||
}
|
||||
|
||||
#startScreen p {
|
||||
margin: 10px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#startScreen button {
|
||||
margin-top: 20px;
|
||||
padding: 10px 30px;
|
||||
font-size: 20px;
|
||||
background: #00ffff;
|
||||
color: #000;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-family: 'Courier New', monospace;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
#startScreen button:hover {
|
||||
background: #00cccc;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
|
||||
<div id="startScreen">
|
||||
<h1>🏃♂️ Mana Runner 🏃♂️</h1>
|
||||
<p>Sammle Mana-Kristalle und weiche Hindernissen aus!</p>
|
||||
<p><strong>Steuerung:</strong></p>
|
||||
<p>Leertaste = Springen</p>
|
||||
<p>Doppelsprung verfügbar nach 10 Kristallen!</p>
|
||||
<button onclick="startGame()">Spiel Starten</button>
|
||||
</div>
|
||||
|
||||
<div id="gameOver">
|
||||
<h2>Game Over!</h2>
|
||||
<p>Punkte: <span id="finalScore">0</span></p>
|
||||
<p>Kristalle: <span id="finalCrystals">0</span></p>
|
||||
<button onclick="restartGame()">Nochmal spielen</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const gameOverScreen = document.getElementById('gameOver');
|
||||
|
||||
canvas.width = 800;
|
||||
canvas.height = 400;
|
||||
|
||||
let gameStarted = false;
|
||||
let gameRunning = false;
|
||||
let score = 0;
|
||||
let crystals = 0;
|
||||
let highScore = localStorage.getItem('manaRunnerHighScore') || 0;
|
||||
let gameSpeed = 5;
|
||||
let gravity = 0.5;
|
||||
let jumpPower = -12;
|
||||
let doubleJumpUnlocked = false;
|
||||
let canDoubleJump = false;
|
||||
|
||||
const player = {
|
||||
x: 100,
|
||||
y: 200,
|
||||
width: 40,
|
||||
height: 60,
|
||||
velocityY: 0,
|
||||
jumping: false,
|
||||
grounded: false,
|
||||
color: '#00ffff'
|
||||
};
|
||||
|
||||
const ground = {
|
||||
x: 0,
|
||||
y: canvas.height - 60,
|
||||
width: canvas.width,
|
||||
height: 60
|
||||
};
|
||||
|
||||
const obstacles = [];
|
||||
const crystals_array = [];
|
||||
const particles = [];
|
||||
|
||||
let obstacleTimer = 0;
|
||||
let crystalTimer = 0;
|
||||
let backgroundOffset = 0;
|
||||
|
||||
class Particle {
|
||||
constructor(x, y, color) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = (Math.random() - 0.5) * 4;
|
||||
this.vy = (Math.random() - 0.5) * 4;
|
||||
this.size = Math.random() * 3 + 1;
|
||||
this.life = 1;
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.vy += 0.1;
|
||||
this.life -= 0.02;
|
||||
this.size *= 0.98;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.life;
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fillRect(this.x, this.y, this.size, this.size);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
class Obstacle {
|
||||
constructor() {
|
||||
this.width = 40;
|
||||
this.height = Math.random() * 80 + 40;
|
||||
this.x = canvas.width;
|
||||
this.y = ground.y - this.height;
|
||||
this.passed = false;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x -= gameSpeed;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.fillStyle = '#ff0066';
|
||||
ctx.fillRect(this.x, this.y, this.width, this.height);
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.shadowColor = '#ff0066';
|
||||
ctx.fillRect(this.x, this.y, this.width, this.height);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class Crystal {
|
||||
constructor() {
|
||||
this.size = 20;
|
||||
this.x = canvas.width;
|
||||
this.y = Math.random() * (ground.y - 100) + 50;
|
||||
this.collected = false;
|
||||
this.rotation = 0;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x -= gameSpeed;
|
||||
this.rotation += 0.05;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.save();
|
||||
ctx.translate(this.x + this.size/2, this.y + this.size/2);
|
||||
ctx.rotate(this.rotation);
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowColor = '#ffff00';
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -this.size/2);
|
||||
ctx.lineTo(this.size/2, 0);
|
||||
ctx.lineTo(0, this.size/2);
|
||||
ctx.lineTo(-this.size/2, 0);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawBackground() {
|
||||
ctx.fillStyle = '#1a0033';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = '#2a0055';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
let x = (i * 200 - backgroundOffset) % (canvas.width + 200);
|
||||
ctx.fillRect(x, 100, 150, 200);
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#00ffff';
|
||||
ctx.font = '20px Courier New';
|
||||
for (let i = 0; i < 20; i++) {
|
||||
let x = (i * 100 - backgroundOffset * 0.5) % (canvas.width + 100);
|
||||
let y = Math.sin(x * 0.01) * 20 + 50;
|
||||
ctx.fillText('✦', x, y);
|
||||
}
|
||||
}
|
||||
|
||||
function drawGround() {
|
||||
ctx.fillStyle = '#004444';
|
||||
ctx.fillRect(ground.x, ground.y, ground.width, ground.height);
|
||||
|
||||
ctx.strokeStyle = '#00ffff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, ground.y);
|
||||
ctx.lineTo(canvas.width, ground.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawPlayer() {
|
||||
ctx.fillStyle = player.color;
|
||||
ctx.fillRect(player.x, player.y, player.width, player.height);
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(player.x + 10, player.y + 10, 5, 5);
|
||||
ctx.fillRect(player.x + 25, player.y + 10, 5, 5);
|
||||
|
||||
ctx.fillStyle = '#ff00ff';
|
||||
ctx.fillRect(player.x + 15, player.y + 25, 10, 3);
|
||||
|
||||
if (player.velocityY < 0) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
particles.push(new Particle(
|
||||
player.x + player.width/2,
|
||||
player.y + player.height,
|
||||
'#00ffff'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawUI() {
|
||||
ctx.fillStyle = '#00ffff';
|
||||
ctx.font = 'bold 24px Courier New';
|
||||
ctx.fillText(`Punkte: ${score}`, 20, 40);
|
||||
ctx.fillText(`Kristalle: ${crystals}`, 20, 70);
|
||||
|
||||
if (doubleJumpUnlocked) {
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.fillText('Doppelsprung freigeschaltet!', 20, 100);
|
||||
}
|
||||
|
||||
ctx.fillStyle = '#ff00ff';
|
||||
ctx.fillText(`High Score: ${highScore}`, canvas.width - 200, 40);
|
||||
}
|
||||
|
||||
function updatePlayer() {
|
||||
player.velocityY += gravity;
|
||||
player.y += player.velocityY;
|
||||
|
||||
if (player.y + player.height >= ground.y) {
|
||||
player.y = ground.y - player.height;
|
||||
player.velocityY = 0;
|
||||
player.grounded = true;
|
||||
player.jumping = false;
|
||||
canDoubleJump = doubleJumpUnlocked;
|
||||
} else {
|
||||
player.grounded = false;
|
||||
}
|
||||
}
|
||||
|
||||
function jump() {
|
||||
if (player.grounded) {
|
||||
player.velocityY = jumpPower;
|
||||
player.jumping = true;
|
||||
canDoubleJump = doubleJumpUnlocked;
|
||||
} else if (canDoubleJump && player.jumping) {
|
||||
player.velocityY = jumpPower;
|
||||
canDoubleJump = false;
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
particles.push(new Particle(
|
||||
player.x + player.width/2,
|
||||
player.y + player.height,
|
||||
'#ffff00'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkCollisions() {
|
||||
for (let obstacle of obstacles) {
|
||||
if (player.x < obstacle.x + obstacle.width &&
|
||||
player.x + player.width > obstacle.x &&
|
||||
player.y < obstacle.y + obstacle.height &&
|
||||
player.y + player.height > obstacle.y) {
|
||||
endGame();
|
||||
}
|
||||
|
||||
if (!obstacle.passed && player.x > obstacle.x + obstacle.width) {
|
||||
obstacle.passed = true;
|
||||
score += 10;
|
||||
}
|
||||
}
|
||||
|
||||
for (let crystal of crystals_array) {
|
||||
if (!crystal.collected &&
|
||||
player.x < crystal.x + crystal.size &&
|
||||
player.x + player.width > crystal.x &&
|
||||
player.y < crystal.y + crystal.size &&
|
||||
player.y + player.height > crystal.y) {
|
||||
crystal.collected = true;
|
||||
crystals++;
|
||||
score += 50;
|
||||
|
||||
if (crystals >= 10 && !doubleJumpUnlocked) {
|
||||
doubleJumpUnlocked = true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < 15; i++) {
|
||||
particles.push(new Particle(
|
||||
crystal.x + crystal.size/2,
|
||||
crystal.y + crystal.size/2,
|
||||
'#ffff00'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateGame() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
backgroundOffset += gameSpeed * 0.5;
|
||||
updatePlayer();
|
||||
|
||||
obstacleTimer++;
|
||||
if (obstacleTimer > 100 + Math.random() * 50) {
|
||||
obstacles.push(new Obstacle());
|
||||
obstacleTimer = 0;
|
||||
}
|
||||
|
||||
crystalTimer++;
|
||||
if (crystalTimer > 150 + Math.random() * 100) {
|
||||
crystals_array.push(new Crystal());
|
||||
crystalTimer = 0;
|
||||
}
|
||||
|
||||
for (let i = obstacles.length - 1; i >= 0; i--) {
|
||||
obstacles[i].update();
|
||||
if (obstacles[i].x + obstacles[i].width < 0) {
|
||||
obstacles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = crystals_array.length - 1; i >= 0; i--) {
|
||||
if (!crystals_array[i].collected) {
|
||||
crystals_array[i].update();
|
||||
}
|
||||
if (crystals_array[i].x + crystals_array[i].size < 0) {
|
||||
crystals_array.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
particles[i].update();
|
||||
if (particles[i].life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
checkCollisions();
|
||||
|
||||
if (score > 0 && score % 100 === 0) {
|
||||
gameSpeed += 0.1;
|
||||
}
|
||||
}
|
||||
|
||||
function drawGame() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
drawBackground();
|
||||
drawGround();
|
||||
|
||||
for (let crystal of crystals_array) {
|
||||
if (!crystal.collected) {
|
||||
crystal.draw();
|
||||
}
|
||||
}
|
||||
|
||||
for (let obstacle of obstacles) {
|
||||
obstacle.draw();
|
||||
}
|
||||
|
||||
for (let particle of particles) {
|
||||
particle.draw();
|
||||
}
|
||||
|
||||
drawPlayer();
|
||||
drawUI();
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
updateGame();
|
||||
drawGame();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
gameStarted = true;
|
||||
gameRunning = true;
|
||||
startScreen.style.display = 'none';
|
||||
gameLoop();
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: 'mana-runner'
|
||||
}, '*');
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
gameRunning = false;
|
||||
|
||||
if (score > highScore) {
|
||||
highScore = score;
|
||||
localStorage.setItem('manaRunnerHighScore', highScore);
|
||||
}
|
||||
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
document.getElementById('finalCrystals').textContent = crystals;
|
||||
gameOverScreen.style.display = 'block';
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'mana-runner',
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
score = 0;
|
||||
crystals = 0;
|
||||
gameSpeed = 5;
|
||||
player.y = 200;
|
||||
player.velocityY = 0;
|
||||
player.grounded = false;
|
||||
obstacles.length = 0;
|
||||
crystals_array.length = 0;
|
||||
particles.length = 0;
|
||||
obstacleTimer = 0;
|
||||
crystalTimer = 0;
|
||||
backgroundOffset = 0;
|
||||
doubleJumpUnlocked = false;
|
||||
canDoubleJump = false;
|
||||
|
||||
gameOverScreen.style.display = 'none';
|
||||
gameRunning = true;
|
||||
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'mana-runner',
|
||||
event: 'GAME_STARTED',
|
||||
data: {}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.code === 'Space' && gameRunning) {
|
||||
e.preventDefault();
|
||||
jump();
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('click', () => {
|
||||
if (gameRunning) {
|
||||
jump();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (gameStarted) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: 'mana-runner',
|
||||
event: 'GAME_ENDED',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
508
games/mana-games/apps/web/public/games/memory_card_match.html
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Memory Card Match</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, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 15px 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: #667eea;
|
||||
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: #667eea;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.difficulty-group {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
background: #f0f0f0;
|
||||
padding: 3px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.difficulty-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #666;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.difficulty-btn:hover {
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.difficulty-btn.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 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;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.card:hover:not(.matched):not(.flipped) {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.card.flipped {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card.matched {
|
||||
animation: matchAnimation 0.8s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.card.matched::after {
|
||||
content: '✨';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 3rem;
|
||||
animation: sparkle 0.8s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes matchAnimation {
|
||||
0% {
|
||||
transform: scale(1) rotateY(180deg);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
25% {
|
||||
transform: scale(1.15) rotateY(180deg);
|
||||
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2) rotateY(180deg);
|
||||
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.6);
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
75% {
|
||||
transform: scale(1.1) rotateY(180deg);
|
||||
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1) rotateY(180deg);
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(0);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, -50%) scale(1.5) rotate(180deg);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(-50%, -50%) scale(2) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.card-face {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 2.2rem;
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.card-front {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.card-back {
|
||||
background: white;
|
||||
transform: rotateY(180deg);
|
||||
border: 2px solid #667eea;
|
||||
}
|
||||
|
||||
.win-message {
|
||||
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;
|
||||
}
|
||||
|
||||
.win-message h2 {
|
||||
color: #667eea;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.win-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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.top-bar {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
}
|
||||
|
||||
.card-face {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.stat {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.difficulty-btn {
|
||||
font-size: 0.75rem;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 15px;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top-bar">
|
||||
<h1>Memory Card Match</h1>
|
||||
|
||||
<div class="game-controls">
|
||||
<div class="stat-group">
|
||||
<div class="stat">
|
||||
<span class="stat-label">Zeit:</span>
|
||||
<span class="stat-value" id="timer">0:00</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Züge:</span>
|
||||
<span class="stat-value" id="moves">0</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-label">Paare:</span>
|
||||
<span class="stat-value"><span id="matches">0</span>/<span id="totalPairs">8</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="difficulty-group">
|
||||
<button class="difficulty-btn active" data-difficulty="easy">4x4</button>
|
||||
<button class="difficulty-btn" data-difficulty="medium">6x4</button>
|
||||
<button class="difficulty-btn" data-difficulty="hard">6x6</button>
|
||||
</div>
|
||||
|
||||
<button class="btn-new" onclick="newGame()">Neues Spiel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-area">
|
||||
<div class="game-board" id="gameBoard"></div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay"></div>
|
||||
<div class="win-message" id="winMessage">
|
||||
<h2>🎉 Gewonnen! 🎉</h2>
|
||||
<div class="win-stats">
|
||||
<p>Zeit: <span id="finalTime"></span></p>
|
||||
<p>Züge: <span id="finalMoves"></span></p>
|
||||
<p>Effizienz: <span id="efficiency"></span>%</p>
|
||||
</div>
|
||||
<button class="btn" onclick="newGame()">Neues Spiel</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const emojis = {
|
||||
easy: ['🎮', '🎯', '🎨', '🎭', '🎪', '🎬', '🎰', '🎲'],
|
||||
medium: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮'],
|
||||
hard: ['🍎', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍈', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝', '🍅', '🥑', '🥦']
|
||||
};
|
||||
|
||||
let cards = [];
|
||||
let flippedCards = [];
|
||||
let matchedPairs = 0;
|
||||
let moves = 0;
|
||||
let gameStarted = false;
|
||||
let startTime;
|
||||
let timerInterval;
|
||||
let currentDifficulty = 'easy';
|
||||
let boardSize = { easy: [4, 4], medium: [6, 4], hard: [6, 6] };
|
||||
|
||||
function shuffle(array) {
|
||||
const newArray = [...array];
|
||||
for (let i = newArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[newArray[i], newArray[j]] = [newArray[j], newArray[i]];
|
||||
}
|
||||
return newArray;
|
||||
}
|
||||
|
||||
function createBoard() {
|
||||
const board = document.getElementById('gameBoard');
|
||||
board.innerHTML = '';
|
||||
|
||||
const [cols, rows] = boardSize[currentDifficulty];
|
||||
const totalCards = cols * rows;
|
||||
const pairsNeeded = totalCards / 2;
|
||||
|
||||
board.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
|
||||
|
||||
const selectedEmojis = emojis[currentDifficulty].slice(0, pairsNeeded);
|
||||
const cardPairs = [...selectedEmojis, ...selectedEmojis];
|
||||
cards = shuffle(cardPairs);
|
||||
|
||||
document.getElementById('totalPairs').textContent = pairsNeeded;
|
||||
|
||||
cards.forEach((emoji, index) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.dataset.index = index;
|
||||
card.dataset.emoji = emoji;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="card-face card-front">?</div>
|
||||
<div class="card-face card-back">${emoji}</div>
|
||||
`;
|
||||
|
||||
card.addEventListener('click', flipCard);
|
||||
board.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function flipCard() {
|
||||
if (flippedCards.length >= 2) return;
|
||||
if (this.classList.contains('flipped') || this.classList.contains('matched')) return;
|
||||
|
||||
if (!gameStarted) {
|
||||
startGame();
|
||||
}
|
||||
|
||||
this.classList.add('flipped');
|
||||
flippedCards.push(this);
|
||||
|
||||
if (flippedCards.length === 2) {
|
||||
moves++;
|
||||
document.getElementById('moves').textContent = moves;
|
||||
checkMatch();
|
||||
}
|
||||
}
|
||||
|
||||
function checkMatch() {
|
||||
const [card1, card2] = flippedCards;
|
||||
const match = card1.dataset.emoji === card2.dataset.emoji;
|
||||
|
||||
if (match) {
|
||||
card1.classList.add('matched');
|
||||
card2.classList.add('matched');
|
||||
matchedPairs++;
|
||||
document.getElementById('matches').textContent = matchedPairs;
|
||||
flippedCards = [];
|
||||
|
||||
if (matchedPairs === parseInt(document.getElementById('totalPairs').textContent)) {
|
||||
endGame();
|
||||
}
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
card1.classList.remove('flipped');
|
||||
card2.classList.remove('flipped');
|
||||
flippedCards = [];
|
||||
}, 800);
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
gameStarted = true;
|
||||
startTime = Date.now();
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
function updateTimer() {
|
||||
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||
const minutes = Math.floor(elapsed / 60);
|
||||
const seconds = elapsed % 60;
|
||||
document.getElementById('timer').textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function endGame() {
|
||||
clearInterval(timerInterval);
|
||||
const finalTime = document.getElementById('timer').textContent;
|
||||
const totalPairs = parseInt(document.getElementById('totalPairs').textContent);
|
||||
const minMoves = totalPairs;
|
||||
const efficiency = Math.round((minMoves / moves) * 100);
|
||||
|
||||
document.getElementById('finalTime').textContent = finalTime;
|
||||
document.getElementById('finalMoves').textContent = moves;
|
||||
document.getElementById('efficiency').textContent = efficiency;
|
||||
|
||||
document.getElementById('overlay').style.display = 'block';
|
||||
document.getElementById('winMessage').style.display = 'block';
|
||||
}
|
||||
|
||||
function newGame() {
|
||||
clearInterval(timerInterval);
|
||||
gameStarted = false;
|
||||
matchedPairs = 0;
|
||||
moves = 0;
|
||||
flippedCards = [];
|
||||
|
||||
document.getElementById('timer').textContent = '0:00';
|
||||
document.getElementById('moves').textContent = '0';
|
||||
document.getElementById('matches').textContent = '0';
|
||||
document.getElementById('overlay').style.display = 'none';
|
||||
document.getElementById('winMessage').style.display = 'none';
|
||||
|
||||
createBoard();
|
||||
}
|
||||
|
||||
document.querySelectorAll('.difficulty-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
document.querySelectorAll('.difficulty-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
currentDifficulty = this.dataset.difficulty;
|
||||
newGame();
|
||||
});
|
||||
});
|
||||
|
||||
newGame();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
886
games/mana-games/apps/web/public/games/neon_maze_runner.html
Normal file
|
|
@ -0,0 +1,886 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Neon Maze Runner</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: 'Arial', sans-serif;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 10px;
|
||||
width: 600px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.score, .timer, .level {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 10px currentColor;
|
||||
}
|
||||
|
||||
.score {
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.timer {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.level {
|
||||
color: #4ecdc4;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 2px solid #00ff88;
|
||||
box-shadow: 0 0 30px rgba(0, 255, 136, 0.5);
|
||||
background: #0a0a0a;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.game-over, .level-complete, .start-screen {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border: 2px solid #00ff88;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 50px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
|
||||
.start-screen {
|
||||
display: block;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 20px 0;
|
||||
font-size: 32px;
|
||||
text-shadow: 0 0 20px currentColor;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.level-complete h2 {
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.start-screen h2 {
|
||||
color: #4ecdc4;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #00ff88;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
transition: all 0.3s;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #00cc6a;
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 20px rgba(0, 255, 136, 0.8);
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin: 20px 0;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin: 15px 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.collectibles {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.collectible-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.collectible-icon {
|
||||
font-size: 30px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.powerup-indicator {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: 24px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.powerup-indicator.active {
|
||||
opacity: 1;
|
||||
animation: pulse 0.5s infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from { transform: scale(1); }
|
||||
to { transform: scale(1.2); }
|
||||
}
|
||||
|
||||
.trail {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #00ff88;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@keyframes fadeTrail {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="ui-container">
|
||||
<div class="score">PUNKTE: <span id="score">0</span></div>
|
||||
<div class="level">LEVEL: <span id="level">1</span></div>
|
||||
<div class="timer">ZEIT: <span id="timer">60</span>s</div>
|
||||
</div>
|
||||
|
||||
<canvas id="gameCanvas" width="600" height="600"></canvas>
|
||||
|
||||
<div class="powerup-indicator" id="powerupIndicator">⚡</div>
|
||||
|
||||
<div class="start-screen" id="startScreen">
|
||||
<h2>NEON MAZE RUNNER</h2>
|
||||
<div class="instructions">
|
||||
<p><strong>Steuerung:</strong> WASD oder Pfeiltasten</p>
|
||||
<p><strong>Ziel:</strong> Sammle alle Diamanten und finde den Ausgang!</p>
|
||||
<p><strong>Tipp:</strong> Achte auf Power-ups und die Zeit!</p>
|
||||
</div>
|
||||
<div class="collectibles">
|
||||
<div class="collectible-item">
|
||||
<div class="collectible-icon">💎</div>
|
||||
<div>Diamanten<br>+100 Punkte</div>
|
||||
</div>
|
||||
<div class="collectible-item">
|
||||
<div class="collectible-icon">⚡</div>
|
||||
<div>Speed Boost<br>2x Geschwindigkeit</div>
|
||||
</div>
|
||||
<div class="collectible-item">
|
||||
<div class="collectible-icon">🕐</div>
|
||||
<div>Zeitbonus<br>+15 Sekunden</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="startGame()">SPIEL STARTEN</button>
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOverScreen">
|
||||
<h2>GAME OVER</h2>
|
||||
<div class="stats">
|
||||
<p>Erreichte Punkte: <span id="finalScore">0</span></p>
|
||||
<p>Erreichte Level: <span id="finalLevel">1</span></p>
|
||||
</div>
|
||||
<button onclick="restartGame()">NOCHMAL SPIELEN</button>
|
||||
</div>
|
||||
|
||||
<div class="level-complete" id="levelCompleteScreen">
|
||||
<h2>LEVEL GESCHAFFT!</h2>
|
||||
<div class="stats">
|
||||
<p>Level Punkte: <span id="levelScore">0</span></p>
|
||||
<p>Zeit Bonus: <span id="timeBonus">0</span></p>
|
||||
</div>
|
||||
<button onclick="nextLevel()">NÄCHSTES LEVEL</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'neon-maze-runner';
|
||||
|
||||
// Canvas und Kontext
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// UI Elemente
|
||||
const scoreElement = document.getElementById('score');
|
||||
const timerElement = document.getElementById('timer');
|
||||
const levelElement = document.getElementById('level');
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const gameOverScreen = document.getElementById('gameOverScreen');
|
||||
const levelCompleteScreen = document.getElementById('levelCompleteScreen');
|
||||
const powerupIndicator = document.getElementById('powerupIndicator');
|
||||
|
||||
// Spielkonstanten
|
||||
const CELL_SIZE = 30;
|
||||
const GRID_WIDTH = Math.floor(canvas.width / CELL_SIZE);
|
||||
const GRID_HEIGHT = Math.floor(canvas.height / CELL_SIZE);
|
||||
|
||||
// Spielzustand
|
||||
let maze = [];
|
||||
let player = { x: 1, y: 1 };
|
||||
let exit = { x: 1, y: 1 }; // Wird später gesetzt
|
||||
let diamonds = [];
|
||||
let powerups = [];
|
||||
let score = 0;
|
||||
let level = 1;
|
||||
let timeLeft = 60;
|
||||
let gameRunning = false;
|
||||
let timerInterval = null;
|
||||
let speedBoost = false;
|
||||
let speedBoostTimer = 0;
|
||||
let particles = [];
|
||||
|
||||
// Eingabe
|
||||
let keys = {};
|
||||
let moveTimer = 0;
|
||||
const MOVE_DELAY = 150; // Millisekunden zwischen Bewegungen
|
||||
const BOOSTED_MOVE_DELAY = 75;
|
||||
|
||||
// Eingabe-Handler
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
});
|
||||
|
||||
// Maze-Generation (Recursive Backtracking)
|
||||
function generateMaze() {
|
||||
// Initialisiere Gitter mit Wänden
|
||||
maze = Array(GRID_HEIGHT).fill().map(() => Array(GRID_WIDTH).fill(1));
|
||||
|
||||
// Startposition
|
||||
const stack = [];
|
||||
const startX = 1;
|
||||
const startY = 1;
|
||||
maze[startY][startX] = 0;
|
||||
stack.push({ x: startX, y: startY });
|
||||
|
||||
// Richtungen
|
||||
const directions = [
|
||||
{ dx: 0, dy: -2 }, // Oben
|
||||
{ dx: 2, dy: 0 }, // Rechts
|
||||
{ dx: 0, dy: 2 }, // Unten
|
||||
{ dx: -2, dy: 0 } // Links
|
||||
];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const current = stack[stack.length - 1];
|
||||
|
||||
// Finde unbesuchte Nachbarn
|
||||
const neighbors = [];
|
||||
for (const dir of directions) {
|
||||
const nx = current.x + dir.dx;
|
||||
const ny = current.y + dir.dy;
|
||||
|
||||
if (nx > 0 && nx < GRID_WIDTH - 1 &&
|
||||
ny > 0 && ny < GRID_HEIGHT - 1 &&
|
||||
maze[ny][nx] === 1) {
|
||||
neighbors.push({ x: nx, y: ny, dx: dir.dx / 2, dy: dir.dy / 2 });
|
||||
}
|
||||
}
|
||||
|
||||
if (neighbors.length > 0) {
|
||||
// Wähle zufälligen Nachbarn
|
||||
const next = neighbors[Math.floor(Math.random() * neighbors.length)];
|
||||
|
||||
// Entferne Wand zwischen current und next
|
||||
maze[current.y + next.dy][current.x + next.dx] = 0;
|
||||
maze[next.y][next.x] = 0;
|
||||
|
||||
stack.push(next);
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
// Füge viele zusätzliche Pfade hinzu für interessanteres Gameplay
|
||||
const extraPaths = 15 + level * 3;
|
||||
for (let i = 0; i < extraPaths; i++) {
|
||||
const x = Math.floor(Math.random() * (GRID_WIDTH - 2)) + 1;
|
||||
const y = Math.floor(Math.random() * (GRID_HEIGHT - 2)) + 1;
|
||||
if (maze[y][x] === 1) {
|
||||
// Prüfe ob mindestens ein Nachbar ein Pfad ist
|
||||
if (maze[y-1][x] === 0 || maze[y+1][x] === 0 ||
|
||||
maze[y][x-1] === 0 || maze[y][x+1] === 0) {
|
||||
maze[y][x] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finde eine gute Position für den Ausgang (weit vom Start entfernt)
|
||||
let maxDistance = 0;
|
||||
let bestExit = { x: GRID_WIDTH - 2, y: GRID_HEIGHT - 2 };
|
||||
|
||||
// Suche nach dem entferntesten erreichbaren Punkt
|
||||
for (let y = 1; y < GRID_HEIGHT - 1; y++) {
|
||||
for (let x = 1; x < GRID_WIDTH - 1; x++) {
|
||||
if (maze[y][x] === 0) {
|
||||
const distance = Math.abs(x - player.x) + Math.abs(y - player.y);
|
||||
if (distance > maxDistance) {
|
||||
maxDistance = distance;
|
||||
bestExit = { x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exit.x = bestExit.x;
|
||||
exit.y = bestExit.y;
|
||||
maze[exit.y][exit.x] = 0;
|
||||
}
|
||||
|
||||
// Platziere Sammelobjekte
|
||||
function placeCollectibles() {
|
||||
diamonds = [];
|
||||
powerups = [];
|
||||
|
||||
// Anzahl basierend auf Level
|
||||
const diamondCount = 3 + Math.floor(level / 3);
|
||||
const powerupCount = 1 + Math.floor(level / 4);
|
||||
|
||||
// Platziere Diamanten
|
||||
for (let i = 0; i < diamondCount; i++) {
|
||||
let placed = false;
|
||||
while (!placed) {
|
||||
const x = Math.floor(Math.random() * GRID_WIDTH);
|
||||
const y = Math.floor(Math.random() * GRID_HEIGHT);
|
||||
|
||||
if (maze[y][x] === 0 &&
|
||||
!(x === player.x && y === player.y) &&
|
||||
!(x === exit.x && y === exit.y) &&
|
||||
!diamonds.some(d => d.x === x && d.y === y)) {
|
||||
diamonds.push({ x, y, collected: false });
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Platziere Power-ups
|
||||
for (let i = 0; i < powerupCount; i++) {
|
||||
let placed = false;
|
||||
while (!placed) {
|
||||
const x = Math.floor(Math.random() * GRID_WIDTH);
|
||||
const y = Math.floor(Math.random() * GRID_HEIGHT);
|
||||
|
||||
if (maze[y][x] === 0 &&
|
||||
!(x === player.x && y === player.y) &&
|
||||
!(x === exit.x && y === exit.y) &&
|
||||
!diamonds.some(d => d.x === x && d.y === y) &&
|
||||
!powerups.some(p => p.x === x && p.y === y)) {
|
||||
|
||||
const type = Math.random() < 0.7 ? 'speed' : 'time';
|
||||
powerups.push({ x, y, type, collected: false });
|
||||
placed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Partikel-Effekt
|
||||
function createParticle(x, y, color, count = 10) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
particles.push({
|
||||
x: x * CELL_SIZE + CELL_SIZE / 2,
|
||||
y: y * CELL_SIZE + CELL_SIZE / 2,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: (Math.random() - 0.5) * 4,
|
||||
life: 1,
|
||||
color: color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update Partikel
|
||||
function updateParticles() {
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
const p = particles[i];
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= 0.02;
|
||||
p.vx *= 0.98;
|
||||
p.vy *= 0.98;
|
||||
|
||||
if (p.life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bewege Spieler
|
||||
function movePlayer(dx, dy) {
|
||||
const newX = player.x + dx;
|
||||
const newY = player.y + dy;
|
||||
|
||||
// Prüfe Kollision
|
||||
if (newX >= 0 && newX < GRID_WIDTH &&
|
||||
newY >= 0 && newY < GRID_HEIGHT &&
|
||||
maze[newY][newX] === 0) {
|
||||
|
||||
// Trail-Effekt
|
||||
createTrail(player.x * CELL_SIZE + CELL_SIZE / 2,
|
||||
player.y * CELL_SIZE + CELL_SIZE / 2);
|
||||
|
||||
player.x = newX;
|
||||
player.y = newY;
|
||||
|
||||
// Prüfe Diamanten
|
||||
diamonds.forEach(diamond => {
|
||||
if (!diamond.collected && diamond.x === player.x && diamond.y === player.y) {
|
||||
diamond.collected = true;
|
||||
score += 100;
|
||||
scoreElement.textContent = score;
|
||||
createParticle(diamond.x, diamond.y, '#00ff88', 20);
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
|
||||
// Prüfe Power-ups
|
||||
powerups.forEach(powerup => {
|
||||
if (!powerup.collected && powerup.x === player.x && powerup.y === player.y) {
|
||||
powerup.collected = true;
|
||||
|
||||
if (powerup.type === 'speed') {
|
||||
speedBoost = true;
|
||||
speedBoostTimer = 5000; // 5 Sekunden
|
||||
powerupIndicator.classList.add('active');
|
||||
createParticle(powerup.x, powerup.y, '#ffff00', 30);
|
||||
} else if (powerup.type === 'time') {
|
||||
timeLeft += 15;
|
||||
timerElement.textContent = timeLeft;
|
||||
createParticle(powerup.x, powerup.y, '#00ffff', 30);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Prüfe Ausgang
|
||||
if (player.x === exit.x && player.y === exit.y) {
|
||||
const allDiamondsCollected = diamonds.every(d => d.collected);
|
||||
if (allDiamondsCollected) {
|
||||
levelComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trail-Effekt
|
||||
function createTrail(x, y) {
|
||||
const trail = document.createElement('div');
|
||||
trail.className = 'trail';
|
||||
trail.style.left = x + 'px';
|
||||
trail.style.top = y + 'px';
|
||||
trail.style.background = speedBoost ? '#ffff00' : '#ff00ff';
|
||||
document.body.appendChild(trail);
|
||||
|
||||
trail.style.animation = 'fadeTrail 0.5s ease-out forwards';
|
||||
setTimeout(() => trail.remove(), 500);
|
||||
}
|
||||
|
||||
// Update Spiel
|
||||
function update(deltaTime) {
|
||||
if (!gameRunning) return;
|
||||
|
||||
// Update Speed Boost
|
||||
if (speedBoost) {
|
||||
speedBoostTimer -= deltaTime;
|
||||
if (speedBoostTimer <= 0) {
|
||||
speedBoost = false;
|
||||
powerupIndicator.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
// Spielerbewegung
|
||||
moveTimer -= deltaTime;
|
||||
const currentMoveDelay = speedBoost ? BOOSTED_MOVE_DELAY : MOVE_DELAY;
|
||||
|
||||
if (moveTimer <= 0) {
|
||||
let moved = false;
|
||||
|
||||
if (keys['w'] || keys['arrowup']) {
|
||||
movePlayer(0, -1);
|
||||
moved = true;
|
||||
} else if (keys['s'] || keys['arrowdown']) {
|
||||
movePlayer(0, 1);
|
||||
moved = true;
|
||||
} else if (keys['a'] || keys['arrowleft']) {
|
||||
movePlayer(-1, 0);
|
||||
moved = true;
|
||||
} else if (keys['d'] || keys['arrowright']) {
|
||||
movePlayer(1, 0);
|
||||
moved = true;
|
||||
}
|
||||
|
||||
if (moved) {
|
||||
moveTimer = currentMoveDelay;
|
||||
}
|
||||
}
|
||||
|
||||
// Update Partikel
|
||||
updateParticles();
|
||||
}
|
||||
|
||||
// Zeichne Spiel
|
||||
function draw() {
|
||||
// Clear
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Zeichne Maze
|
||||
for (let y = 0; y < GRID_HEIGHT; y++) {
|
||||
for (let x = 0; x < GRID_WIDTH; x++) {
|
||||
if (maze[y][x] === 1) {
|
||||
// Wand mit Neon-Effekt
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE);
|
||||
|
||||
// Neon-Rand
|
||||
ctx.strokeStyle = '#16213e';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(x * CELL_SIZE + 0.5, y * CELL_SIZE + 0.5,
|
||||
CELL_SIZE - 1, CELL_SIZE - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeichne Ausgang
|
||||
ctx.save();
|
||||
ctx.translate(exit.x * CELL_SIZE + CELL_SIZE / 2,
|
||||
exit.y * CELL_SIZE + CELL_SIZE / 2);
|
||||
|
||||
const allDiamondsCollected = diamonds.every(d => d.collected);
|
||||
if (allDiamondsCollected) {
|
||||
// Animierter Ausgang wenn alle Diamanten gesammelt
|
||||
ctx.rotate(Date.now() * 0.002);
|
||||
ctx.fillStyle = '#00ff88';
|
||||
ctx.fillRect(-CELL_SIZE / 3, -CELL_SIZE / 3, CELL_SIZE * 2/3, CELL_SIZE * 2/3);
|
||||
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowColor = '#00ff88';
|
||||
ctx.fillStyle = '#00ff88';
|
||||
ctx.fillRect(-CELL_SIZE / 4, -CELL_SIZE / 4, CELL_SIZE / 2, CELL_SIZE / 2);
|
||||
ctx.shadowBlur = 0;
|
||||
} else {
|
||||
// Inaktiver Ausgang
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-CELL_SIZE / 3, -CELL_SIZE / 3, CELL_SIZE * 2/3, CELL_SIZE * 2/3);
|
||||
ctx.strokeStyle = '#555';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(-CELL_SIZE / 3, -CELL_SIZE / 3, CELL_SIZE * 2/3, CELL_SIZE * 2/3);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
// Zeichne Diamanten
|
||||
diamonds.forEach(diamond => {
|
||||
if (!diamond.collected) {
|
||||
ctx.save();
|
||||
ctx.translate(diamond.x * CELL_SIZE + CELL_SIZE / 2,
|
||||
diamond.y * CELL_SIZE + CELL_SIZE / 2);
|
||||
ctx.rotate(Date.now() * 0.003);
|
||||
|
||||
// Diamant-Form (größer für größere Zellen)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -CELL_SIZE / 2.5);
|
||||
ctx.lineTo(CELL_SIZE / 3, -CELL_SIZE / 5);
|
||||
ctx.lineTo(CELL_SIZE / 3, CELL_SIZE / 5);
|
||||
ctx.lineTo(0, CELL_SIZE / 2.5);
|
||||
ctx.lineTo(-CELL_SIZE / 3, CELL_SIZE / 5);
|
||||
ctx.lineTo(-CELL_SIZE / 3, -CELL_SIZE / 5);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = '#00ff88';
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowColor = '#00ff88';
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
ctx.strokeStyle = '#00cc6a';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// Zeichne Power-ups
|
||||
powerups.forEach(powerup => {
|
||||
if (!powerup.collected) {
|
||||
ctx.save();
|
||||
ctx.translate(powerup.x * CELL_SIZE + CELL_SIZE / 2,
|
||||
powerup.y * CELL_SIZE + CELL_SIZE / 2);
|
||||
|
||||
if (powerup.type === 'speed') {
|
||||
// Blitz-Symbol
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowColor = '#ffff00';
|
||||
ctx.font = '20px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('⚡', 0, 0);
|
||||
} else if (powerup.type === 'time') {
|
||||
// Uhr-Symbol
|
||||
ctx.fillStyle = '#00ffff';
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowColor = '#00ffff';
|
||||
ctx.font = '20px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText('🕐', 0, 0);
|
||||
}
|
||||
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.restore();
|
||||
}
|
||||
});
|
||||
|
||||
// Zeichne Spieler
|
||||
ctx.save();
|
||||
ctx.translate(player.x * CELL_SIZE + CELL_SIZE / 2,
|
||||
player.y * CELL_SIZE + CELL_SIZE / 2);
|
||||
|
||||
// Spieler mit Glow-Effekt (größer für größere Zellen)
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, CELL_SIZE / 2.5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = speedBoost ? '#ffff00' : '#ff00ff';
|
||||
ctx.shadowBlur = speedBoost ? 30 : 25;
|
||||
ctx.shadowColor = speedBoost ? '#ffff00' : '#ff00ff';
|
||||
ctx.fill();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Mittlerer Ring
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, CELL_SIZE / 3, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = speedBoost ? '#ffcc00' : '#ff66ff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
|
||||
// Innerer Kreis
|
||||
ctx.beginPath();
|
||||
ctx.arc(0, 0, CELL_SIZE / 5, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// Zeichne Partikel
|
||||
particles.forEach(p => {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = p.life;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
// Zeichne verbleibende Diamanten-Anzeige
|
||||
const remainingDiamonds = diamonds.filter(d => !d.collected).length;
|
||||
if (remainingDiamonds > 0) {
|
||||
ctx.fillStyle = '#00ff88';
|
||||
ctx.font = 'bold 16px Arial';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(`💎 ${remainingDiamonds}`, 10, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Game Loop
|
||||
let lastTime = 0;
|
||||
function gameLoop(currentTime) {
|
||||
const deltaTime = currentTime - lastTime;
|
||||
lastTime = currentTime;
|
||||
|
||||
update(deltaTime);
|
||||
draw();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Timer
|
||||
function updateTimer() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
timeLeft--;
|
||||
timerElement.textContent = timeLeft;
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
|
||||
// Level abgeschlossen
|
||||
function levelComplete() {
|
||||
gameRunning = false;
|
||||
clearInterval(timerInterval);
|
||||
|
||||
// Berechne Bonus
|
||||
const timeBonus = timeLeft * 10;
|
||||
score += timeBonus;
|
||||
|
||||
document.getElementById('levelScore').textContent = score;
|
||||
document.getElementById('timeBonus').textContent = timeBonus;
|
||||
levelCompleteScreen.style.display = 'block';
|
||||
}
|
||||
|
||||
// Game Over
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
clearInterval(timerInterval);
|
||||
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
document.getElementById('finalLevel').textContent = level;
|
||||
gameOverScreen.style.display = 'block';
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 1000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'maze_explorer',
|
||||
name: 'Maze Explorer',
|
||||
description: 'Score 1000 points in Neon Maze Runner',
|
||||
icon: '🌟'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (level >= 10) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'maze_master',
|
||||
name: 'Maze Master',
|
||||
description: 'Reach level 10 in Neon Maze Runner',
|
||||
icon: '🏆'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Starte Spiel
|
||||
function startGame() {
|
||||
startScreen.style.display = 'none';
|
||||
initLevel();
|
||||
gameRunning = true;
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Initialisiere Level
|
||||
function initLevel() {
|
||||
// Reset Speed Boost
|
||||
speedBoost = false;
|
||||
speedBoostTimer = 0;
|
||||
powerupIndicator.classList.remove('active');
|
||||
|
||||
// Zeit basierend auf Level
|
||||
timeLeft = 60 + (level - 1) * 10;
|
||||
timerElement.textContent = timeLeft;
|
||||
levelElement.textContent = level;
|
||||
|
||||
// Generiere neues Maze
|
||||
generateMaze();
|
||||
|
||||
// Setze Spielerposition
|
||||
player = { x: 1, y: 1 };
|
||||
|
||||
// Platziere Sammelobjekte
|
||||
placeCollectibles();
|
||||
|
||||
// Clear particles
|
||||
particles = [];
|
||||
}
|
||||
|
||||
// Nächstes Level
|
||||
function nextLevel() {
|
||||
levelCompleteScreen.style.display = 'none';
|
||||
level++;
|
||||
initLevel();
|
||||
gameRunning = true;
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
// Neustart
|
||||
function restartGame() {
|
||||
gameOverScreen.style.display = 'none';
|
||||
score = 0;
|
||||
level = 1;
|
||||
scoreElement.textContent = score;
|
||||
initLevel();
|
||||
gameRunning = true;
|
||||
timerInterval = setInterval(updateTimer, 1000);
|
||||
}
|
||||
|
||||
// Initialisierung
|
||||
console.log('Neon Maze Runner geladen!');
|
||||
console.log('Ein prozedural generiertes Labyrinth-Spiel mit Sammelobjekten.');
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
636
games/mana-games/apps/web/public/games/puzzle_blocks.html
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Puzzle Blocks - Mana Games</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #0a0a0a;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: flex-start;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.game-board {
|
||||
position: relative;
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
box-shadow: 0 0 30px rgba(157, 48, 255, 0.3);
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
display: block;
|
||||
background: #0a0a0a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #1a1a1a;
|
||||
border: 2px solid #333;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-box h3 {
|
||||
color: #9d30ff;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: 1.1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.level, .lines {
|
||||
font-size: 1.2rem;
|
||||
color: #aaa;
|
||||
margin: 0.3rem 0;
|
||||
}
|
||||
|
||||
.next-piece {
|
||||
background: #0a0a0a;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#nextCanvas {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.controls kbd {
|
||||
background: #333;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
color: #9d30ff;
|
||||
margin: 0 0.2rem;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(26, 26, 26, 0.95);
|
||||
border: 2px solid #9d30ff;
|
||||
border-radius: 12px;
|
||||
padding: 2rem 3rem;
|
||||
text-align: center;
|
||||
display: none;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 50px rgba(157, 48, 255, 0.5);
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
color: #9d30ff;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.game-over p {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background: #9d30ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 2rem;
|
||||
font-size: 1.1rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
background: #7a20cc;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(157, 48, 255, 0.5);
|
||||
}
|
||||
|
||||
.start-screen {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(26, 26, 26, 0.95);
|
||||
border: 2px solid #9d30ff;
|
||||
border-radius: 12px;
|
||||
padding: 2rem 3rem;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
box-shadow: 0 0 50px rgba(157, 48, 255, 0.5);
|
||||
}
|
||||
|
||||
.start-screen h1 {
|
||||
color: #9d30ff;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
text-shadow: 0 0 20px rgba(157, 48, 255, 0.5);
|
||||
}
|
||||
|
||||
.start-btn {
|
||||
background: #9d30ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 1rem 3rem;
|
||||
font-size: 1.2rem;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.start-btn:hover {
|
||||
background: #7a20cc;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(157, 48, 255, 0.5);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.game-container {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
min-width: auto;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="game-board">
|
||||
<canvas id="gameCanvas"></canvas>
|
||||
<div class="start-screen" id="startScreen">
|
||||
<h1>PUZZLE BLOCKS</h1>
|
||||
<p style="color: #aaa; margin-bottom: 1rem;">Klassisches Tetris-Gameplay</p>
|
||||
<button class="start-btn" onclick="startGame()">SPIEL STARTEN</button>
|
||||
</div>
|
||||
<div class="game-over" id="gameOverScreen">
|
||||
<h2>GAME OVER</h2>
|
||||
<p>Deine Punkte: <span id="finalScore">0</span></p>
|
||||
<button class="restart-btn" onclick="resetGame()">Neues Spiel</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="side-panel">
|
||||
<div class="info-box">
|
||||
<h3>Punkte</h3>
|
||||
<div class="score" id="score">0</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Level</h3>
|
||||
<div class="level">Level <span id="level">1</span></div>
|
||||
<div class="lines">Linien: <span id="lines">0</span></div>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Nächster Block</h3>
|
||||
<div class="next-piece">
|
||||
<canvas id="nextCanvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-box controls">
|
||||
<h3>Steuerung</h3>
|
||||
<p><kbd>←</kbd><kbd>→</kbd> Bewegen</p>
|
||||
<p><kbd>↓</kbd> Schneller fallen</p>
|
||||
<p><kbd>↑</kbd> Drehen</p>
|
||||
<p><kbd>Space</kbd> Sofort fallen</p>
|
||||
<p><kbd>P</kbd> Pause</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const nextCanvas = document.getElementById('nextCanvas');
|
||||
const nextCtx = nextCanvas.getContext('2d');
|
||||
|
||||
// Game dimensions
|
||||
const COLS = 10;
|
||||
const ROWS = 20;
|
||||
const BLOCK_SIZE = 30;
|
||||
const NEXT_BLOCK_SIZE = 20;
|
||||
|
||||
canvas.width = COLS * BLOCK_SIZE;
|
||||
canvas.height = ROWS * BLOCK_SIZE;
|
||||
nextCanvas.width = 4 * NEXT_BLOCK_SIZE;
|
||||
nextCanvas.height = 4 * NEXT_BLOCK_SIZE;
|
||||
|
||||
// Tetromino definitions
|
||||
const PIECES = [
|
||||
// I-piece
|
||||
{
|
||||
shape: [[1,1,1,1]],
|
||||
color: '#00f0f0'
|
||||
},
|
||||
// O-piece
|
||||
{
|
||||
shape: [[1,1],[1,1]],
|
||||
color: '#f0f000'
|
||||
},
|
||||
// T-piece
|
||||
{
|
||||
shape: [[0,1,0],[1,1,1]],
|
||||
color: '#a000f0'
|
||||
},
|
||||
// S-piece
|
||||
{
|
||||
shape: [[0,1,1],[1,1,0]],
|
||||
color: '#00f000'
|
||||
},
|
||||
// Z-piece
|
||||
{
|
||||
shape: [[1,1,0],[0,1,1]],
|
||||
color: '#f00000'
|
||||
},
|
||||
// J-piece
|
||||
{
|
||||
shape: [[1,0,0],[1,1,1]],
|
||||
color: '#0000f0'
|
||||
},
|
||||
// L-piece
|
||||
{
|
||||
shape: [[0,0,1],[1,1,1]],
|
||||
color: '#f0a000'
|
||||
}
|
||||
];
|
||||
|
||||
// Game state
|
||||
let board = [];
|
||||
let currentPiece = null;
|
||||
let nextPiece = null;
|
||||
let score = 0;
|
||||
let lines = 0;
|
||||
let level = 1;
|
||||
let dropTime = 1000;
|
||||
let lastDrop = 0;
|
||||
let gameRunning = false;
|
||||
let gamePaused = false;
|
||||
|
||||
// Initialize board
|
||||
function initBoard() {
|
||||
board = Array(ROWS).fill().map(() => Array(COLS).fill(0));
|
||||
}
|
||||
|
||||
// Create a new piece
|
||||
function createPiece() {
|
||||
const piece = PIECES[Math.floor(Math.random() * PIECES.length)];
|
||||
return {
|
||||
shape: piece.shape.map(row => [...row]),
|
||||
color: piece.color,
|
||||
x: Math.floor(COLS / 2) - Math.floor(piece.shape[0].length / 2),
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Rotate piece
|
||||
function rotatePiece(piece) {
|
||||
const rotated = [];
|
||||
const rows = piece.shape.length;
|
||||
const cols = piece.shape[0].length;
|
||||
|
||||
for (let i = 0; i < cols; i++) {
|
||||
rotated[i] = [];
|
||||
for (let j = rows - 1; j >= 0; j--) {
|
||||
rotated[i].push(piece.shape[j][i]);
|
||||
}
|
||||
}
|
||||
|
||||
return rotated;
|
||||
}
|
||||
|
||||
// Check collision
|
||||
function isValidMove(piece, x, y, shape = piece.shape) {
|
||||
for (let row = 0; row < shape.length; row++) {
|
||||
for (let col = 0; col < shape[row].length; col++) {
|
||||
if (shape[row][col]) {
|
||||
const newX = x + col;
|
||||
const newY = y + row;
|
||||
|
||||
if (newX < 0 || newX >= COLS || newY >= ROWS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (newY >= 0 && board[newY][newX]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Lock piece to board
|
||||
function lockPiece() {
|
||||
for (let row = 0; row < currentPiece.shape.length; row++) {
|
||||
for (let col = 0; col < currentPiece.shape[row].length; col++) {
|
||||
if (currentPiece.shape[row][col]) {
|
||||
const x = currentPiece.x + col;
|
||||
const y = currentPiece.y + row;
|
||||
if (y >= 0) {
|
||||
board[y][x] = currentPiece.color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear completed lines
|
||||
function clearLines() {
|
||||
let linesCleared = 0;
|
||||
|
||||
for (let row = ROWS - 1; row >= 0; row--) {
|
||||
if (board[row].every(cell => cell !== 0)) {
|
||||
board.splice(row, 1);
|
||||
board.unshift(Array(COLS).fill(0));
|
||||
linesCleared++;
|
||||
row++; // Check the same row again
|
||||
}
|
||||
}
|
||||
|
||||
if (linesCleared > 0) {
|
||||
lines += linesCleared;
|
||||
score += linesCleared * 100 * level;
|
||||
|
||||
// Bonus for multiple lines
|
||||
if (linesCleared === 4) {
|
||||
score += 400 * level;
|
||||
}
|
||||
|
||||
// Level up every 10 lines
|
||||
level = Math.floor(lines / 10) + 1;
|
||||
dropTime = Math.max(100, 1000 - (level - 1) * 100);
|
||||
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
// Update UI elements
|
||||
function updateUI() {
|
||||
document.getElementById('score').textContent = score;
|
||||
document.getElementById('level').textContent = level;
|
||||
document.getElementById('lines').textContent = lines;
|
||||
}
|
||||
|
||||
// Draw block
|
||||
function drawBlock(ctx, x, y, color, size = BLOCK_SIZE) {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(x * size, y * size, size - 2, size - 2);
|
||||
|
||||
// Add gradient for 3D effect
|
||||
const gradient = ctx.createLinearGradient(
|
||||
x * size, y * size,
|
||||
x * size + size, y * size + size
|
||||
);
|
||||
gradient.addColorStop(0, 'rgba(255,255,255,0.3)');
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0.3)');
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(x * size, y * size, size - 2, size - 2);
|
||||
}
|
||||
|
||||
// Draw board
|
||||
function drawBoard() {
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw grid
|
||||
ctx.strokeStyle = '#1a1a1a';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i <= COLS; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(i * BLOCK_SIZE, 0);
|
||||
ctx.lineTo(i * BLOCK_SIZE, canvas.height);
|
||||
ctx.stroke();
|
||||
}
|
||||
for (let i = 0; i <= ROWS; i++) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, i * BLOCK_SIZE);
|
||||
ctx.lineTo(canvas.width, i * BLOCK_SIZE);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Draw locked pieces
|
||||
for (let row = 0; row < ROWS; row++) {
|
||||
for (let col = 0; col < COLS; col++) {
|
||||
if (board[row][col]) {
|
||||
drawBlock(ctx, col, row, board[row][col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw piece
|
||||
function drawPiece(ctx, piece, blockSize = BLOCK_SIZE) {
|
||||
for (let row = 0; row < piece.shape.length; row++) {
|
||||
for (let col = 0; col < piece.shape[row].length; col++) {
|
||||
if (piece.shape[row][col]) {
|
||||
drawBlock(ctx, piece.x + col, piece.y + row, piece.color, blockSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw next piece
|
||||
function drawNextPiece() {
|
||||
nextCtx.fillStyle = '#0a0a0a';
|
||||
nextCtx.fillRect(0, 0, nextCanvas.width, nextCanvas.height);
|
||||
|
||||
if (nextPiece) {
|
||||
const offsetX = (4 - nextPiece.shape[0].length) / 2;
|
||||
const offsetY = (4 - nextPiece.shape.length) / 2;
|
||||
|
||||
for (let row = 0; row < nextPiece.shape.length; row++) {
|
||||
for (let col = 0; col < nextPiece.shape[row].length; col++) {
|
||||
if (nextPiece.shape[row][col]) {
|
||||
drawBlock(nextCtx, offsetX + col, offsetY + row, nextPiece.color, NEXT_BLOCK_SIZE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Game over
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
document.getElementById('gameOverScreen').style.display = 'block';
|
||||
}
|
||||
|
||||
// Reset game
|
||||
function resetGame() {
|
||||
initBoard();
|
||||
score = 0;
|
||||
lines = 0;
|
||||
level = 1;
|
||||
dropTime = 1000;
|
||||
updateUI();
|
||||
|
||||
currentPiece = createPiece();
|
||||
nextPiece = createPiece();
|
||||
|
||||
document.getElementById('gameOverScreen').style.display = 'none';
|
||||
gameRunning = true;
|
||||
gamePaused = false;
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Start game
|
||||
function startGame() {
|
||||
document.getElementById('startScreen').style.display = 'none';
|
||||
resetGame();
|
||||
}
|
||||
|
||||
// Game loop
|
||||
function gameLoop(timestamp = 0) {
|
||||
if (!gameRunning || gamePaused) return;
|
||||
|
||||
// Auto drop
|
||||
if (timestamp - lastDrop > dropTime) {
|
||||
if (isValidMove(currentPiece, currentPiece.x, currentPiece.y + 1)) {
|
||||
currentPiece.y++;
|
||||
} else {
|
||||
lockPiece();
|
||||
clearLines();
|
||||
|
||||
currentPiece = nextPiece;
|
||||
nextPiece = createPiece();
|
||||
|
||||
if (!isValidMove(currentPiece, currentPiece.x, currentPiece.y)) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
}
|
||||
lastDrop = timestamp;
|
||||
}
|
||||
|
||||
// Draw everything
|
||||
drawBoard();
|
||||
if (currentPiece) {
|
||||
drawPiece(ctx, currentPiece);
|
||||
}
|
||||
drawNextPiece();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Keyboard controls
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!gameRunning || gamePaused) return;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowLeft':
|
||||
if (isValidMove(currentPiece, currentPiece.x - 1, currentPiece.y)) {
|
||||
currentPiece.x--;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowRight':
|
||||
if (isValidMove(currentPiece, currentPiece.x + 1, currentPiece.y)) {
|
||||
currentPiece.x++;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowDown':
|
||||
if (isValidMove(currentPiece, currentPiece.x, currentPiece.y + 1)) {
|
||||
currentPiece.y++;
|
||||
score++;
|
||||
updateUI();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
const rotated = rotatePiece(currentPiece);
|
||||
if (isValidMove(currentPiece, currentPiece.x, currentPiece.y, rotated)) {
|
||||
currentPiece.shape = rotated;
|
||||
}
|
||||
break;
|
||||
|
||||
case ' ':
|
||||
// Hard drop
|
||||
while (isValidMove(currentPiece, currentPiece.x, currentPiece.y + 1)) {
|
||||
currentPiece.y++;
|
||||
score += 2;
|
||||
}
|
||||
updateUI();
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
case 'P':
|
||||
gamePaused = !gamePaused;
|
||||
if (!gamePaused) {
|
||||
gameLoop();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize
|
||||
initBoard();
|
||||
updateUI();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
138
games/mana-games/apps/web/public/games/reaction_test.html
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Reaction Test</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
font-family: Arial;
|
||||
text-align: center;
|
||||
}
|
||||
#screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: background 0s;
|
||||
}
|
||||
.red { background: #c33; }
|
||||
.green { background: #3c3; }
|
||||
.blue { background: #247; }
|
||||
h1 { font-size: 48px; margin: 20px; }
|
||||
p { font-size: 24px; margin: 10px; }
|
||||
.best { color: #ffd700; font-size: 20px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="screen" class="blue" onclick="handleClick()">
|
||||
<h1 id="title">REACTION TEST</h1>
|
||||
<p id="info">Klicke wenn der Bildschirm GRÜN wird!</p>
|
||||
<p id="result"></p>
|
||||
<p class="best" id="best"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'reaction-test';
|
||||
|
||||
let waiting = false;
|
||||
let startTime = 0;
|
||||
let times = [];
|
||||
let timeout;
|
||||
|
||||
const screen = document.getElementById('screen');
|
||||
const info = document.getElementById('info');
|
||||
const result = document.getElementById('result');
|
||||
const best = document.getElementById('best');
|
||||
|
||||
function start() {
|
||||
screen.className = 'red';
|
||||
info.textContent = 'Warte auf GRÜN...';
|
||||
result.textContent = '';
|
||||
waiting = true;
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
screen.className = 'green';
|
||||
info.textContent = 'KLICK!';
|
||||
startTime = Date.now();
|
||||
}, Math.random() * 4000 + 2000);
|
||||
}
|
||||
|
||||
function handleClick() {
|
||||
if (screen.className === 'blue') {
|
||||
start();
|
||||
} else if (screen.className === 'red' && waiting) {
|
||||
clearTimeout(timeout);
|
||||
screen.className = 'blue';
|
||||
info.textContent = 'Zu früh! Klicke zum Neustart';
|
||||
result.textContent = '❌ Fehlstart!';
|
||||
waiting = false;
|
||||
} else if (screen.className === 'green') {
|
||||
const time = Date.now() - startTime;
|
||||
times.push(time);
|
||||
|
||||
screen.className = 'blue';
|
||||
info.textContent = 'Klicke für nächsten Versuch';
|
||||
result.textContent = `⚡ ${time}ms`;
|
||||
|
||||
const bestTime = Math.min(...times);
|
||||
best.textContent = `Beste Zeit: ${bestTime}ms (${times.length} Versuche)`;
|
||||
|
||||
// Sende Score Update für Statistiken (niedrigere Zeit = bessere Leistung)
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: Math.max(0, 1000 - time) }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (time < 250) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'lightning_reflexes',
|
||||
name: 'Lightning Reflexes',
|
||||
description: 'React in under 250ms',
|
||||
icon: '⚡'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (times.length >= 10 && bestTime < 300) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'consistent_speed',
|
||||
name: 'Consistent Speed',
|
||||
description: 'Best time under 300ms after 10 attempts',
|
||||
icon: '🎯'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
waiting = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
795
games/mana-games/apps/web/public/games/rhythm_defender.html
Normal file
|
|
@ -0,0 +1,795 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Rhythm Defender</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: radial-gradient(circle at center, #1a0033, #000);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: 'Arial', sans-serif;
|
||||
color: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
width: 800px;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #ff00ff;
|
||||
text-shadow: 0 0 20px #ff00ff;
|
||||
}
|
||||
|
||||
.combo {
|
||||
font-size: 20px;
|
||||
color: #00ffff;
|
||||
text-shadow: 0 0 15px #00ffff;
|
||||
}
|
||||
|
||||
.health {
|
||||
font-size: 20px;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.health-bar {
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2px solid #ff6b6b;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.health-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #ff6b6b, #ff4444);
|
||||
transition: width 0.3s ease;
|
||||
box-shadow: 0 0 10px #ff6b6b;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #ff00ff;
|
||||
box-shadow: 0 0 30px rgba(255, 0, 255, 0.5);
|
||||
background: #0a0a0a;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.start-screen, .game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border: 3px solid #ff00ff;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 50px rgba(255, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
.game-over {
|
||||
display: none;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
margin: 0 0 20px 0;
|
||||
background: linear-gradient(45deg, #ff00ff, #00ffff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 30px rgba(255, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 36px;
|
||||
margin: 0 0 20px 0;
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
margin: 20px 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.key-display {
|
||||
display: inline-flex;
|
||||
gap: 10px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.key {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(45deg, #ff00ff, #ff0080);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 15px 30px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin: 10px;
|
||||
border-radius: 30px;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.3s;
|
||||
box-shadow: 0 5px 20px rgba(255, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 30px rgba(255, 0, 255, 0.7);
|
||||
}
|
||||
|
||||
.beat-indicator {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 3px solid #ff00ff;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
background: rgba(255, 0, 255, 0.1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: translateX(-50%) scale(1); opacity: 1; }
|
||||
50% { transform: translateX(-50%) scale(1.2); opacity: 0.8; }
|
||||
100% { transform: translateX(-50%) scale(1); opacity: 0; }
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 0.5s ease-out;
|
||||
}
|
||||
|
||||
.perfect-text {
|
||||
position: absolute;
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #00ff00;
|
||||
text-shadow: 0 0 20px #00ff00;
|
||||
animation: fadeUp 1s ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.good-text {
|
||||
position: absolute;
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: #ffff00;
|
||||
text-shadow: 0 0 20px #ffff00;
|
||||
animation: fadeUp 1s ease-out forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fadeUp {
|
||||
0% { opacity: 1; transform: translateY(0); }
|
||||
100% { opacity: 0; transform: translateY(-50px); }
|
||||
}
|
||||
|
||||
.background-pulse {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, transparent, rgba(255, 0, 255, 0.1));
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes bgPulse {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="ui-top">
|
||||
<div class="score">SCORE: <span id="score">0</span></div>
|
||||
<div class="combo">COMBO: <span id="combo">0</span>x</div>
|
||||
<div class="health-container">
|
||||
<div class="health">LEBEN</div>
|
||||
<div class="health-bar">
|
||||
<div class="health-fill" id="healthFill" style="width: 100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="gameCanvas" width="800" height="500"></canvas>
|
||||
|
||||
<div class="beat-indicator" id="beatIndicator">BEAT</div>
|
||||
<div class="background-pulse" id="bgPulse"></div>
|
||||
|
||||
<div class="start-screen" id="startScreen">
|
||||
<h1>RHYTHM DEFENDER</h1>
|
||||
<div class="instructions">
|
||||
<p>Verteidige dich im Rhythmus der Musik!</p>
|
||||
<p>Drücke die richtigen Tasten im Takt:</p>
|
||||
<div class="key-display">
|
||||
<div class="key" style="border-color: #ff0000;">A</div>
|
||||
<div class="key" style="border-color: #00ff00;">S</div>
|
||||
<div class="key" style="border-color: #0080ff;">D</div>
|
||||
<div class="key" style="border-color: #ffff00;">F</div>
|
||||
</div>
|
||||
<p>Treffe die Noten wenn sie die Ziellinie erreichen!</p>
|
||||
<p><strong>PERFECT</strong> = 100 Punkte + Combo</p>
|
||||
<p><strong>GOOD</strong> = 50 Punkte</p>
|
||||
</div>
|
||||
<button onclick="startGame()">SPIEL STARTEN</button>
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOverScreen">
|
||||
<h2>GAME OVER</h2>
|
||||
<p style="font-size: 24px;">Finaler Score: <span id="finalScore">0</span></p>
|
||||
<p style="font-size: 20px;">Maximale Combo: <span id="maxCombo">0</span></p>
|
||||
<button onclick="restartGame()">NOCHMAL SPIELEN</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'rhythm-defender';
|
||||
|
||||
// Canvas und Context
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// UI Elemente
|
||||
const scoreElement = document.getElementById('score');
|
||||
const comboElement = document.getElementById('combo');
|
||||
const healthFill = document.getElementById('healthFill');
|
||||
const startScreen = document.getElementById('startScreen');
|
||||
const gameOverScreen = document.getElementById('gameOverScreen');
|
||||
const beatIndicator = document.getElementById('beatIndicator');
|
||||
const bgPulse = document.getElementById('bgPulse');
|
||||
|
||||
// Spielkonstanten
|
||||
const LANES = 4;
|
||||
const LANE_WIDTH = canvas.width / LANES;
|
||||
const NOTE_HEIGHT = 20;
|
||||
const NOTE_SPEED = 3;
|
||||
const TARGET_Y = canvas.height - 80;
|
||||
const PERFECT_RANGE = 40;
|
||||
const GOOD_RANGE = 80;
|
||||
const BEAT_INTERVAL = 500; // Millisekunden
|
||||
|
||||
// Spielzustand
|
||||
let notes = [];
|
||||
let score = 0;
|
||||
let combo = 0;
|
||||
let maxCombo = 0;
|
||||
let health = 100;
|
||||
let gameRunning = false;
|
||||
let lastBeatTime = 0;
|
||||
let beatCount = 0;
|
||||
let particles = [];
|
||||
let floatingTexts = [];
|
||||
|
||||
// Tastenzuordnung
|
||||
const laneKeys = ['a', 's', 'd', 'f'];
|
||||
const laneColors = ['#ff0000', '#00ff00', '#0080ff', '#ffff00'];
|
||||
const keyPressed = {};
|
||||
|
||||
// Note Klasse
|
||||
class Note {
|
||||
constructor(lane) {
|
||||
this.lane = lane;
|
||||
this.x = lane * LANE_WIDTH + LANE_WIDTH / 2;
|
||||
this.y = -NOTE_HEIGHT;
|
||||
this.hit = false;
|
||||
this.missed = false;
|
||||
this.color = laneColors[lane];
|
||||
}
|
||||
|
||||
update() {
|
||||
this.y += NOTE_SPEED;
|
||||
|
||||
// Prüfe ob Note verpasst wurde
|
||||
if (this.y > TARGET_Y + GOOD_RANGE && !this.hit && !this.missed) {
|
||||
this.missed = true;
|
||||
missNote();
|
||||
}
|
||||
}
|
||||
|
||||
draw() {
|
||||
if (this.hit || this.missed) return;
|
||||
|
||||
// Note mit Glow-Effekt
|
||||
ctx.save();
|
||||
ctx.translate(this.x, this.y);
|
||||
|
||||
// Äußerer Glow
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowColor = this.color;
|
||||
|
||||
// Note-Form (Diamant)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -NOTE_HEIGHT);
|
||||
ctx.lineTo(NOTE_HEIGHT, 0);
|
||||
ctx.lineTo(0, NOTE_HEIGHT);
|
||||
ctx.lineTo(-NOTE_HEIGHT, 0);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.fill();
|
||||
|
||||
// Innerer heller Teil
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -NOTE_HEIGHT / 2);
|
||||
ctx.lineTo(NOTE_HEIGHT / 2, 0);
|
||||
ctx.lineTo(0, NOTE_HEIGHT / 2);
|
||||
ctx.lineTo(-NOTE_HEIGHT / 2, 0);
|
||||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
|
||||
ctx.fill();
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Partikel Klasse
|
||||
class Particle {
|
||||
constructor(x, y, color) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.vx = (Math.random() - 0.5) * 8;
|
||||
this.vy = (Math.random() - 0.5) * 8;
|
||||
this.life = 1;
|
||||
this.color = color;
|
||||
this.size = Math.random() * 5 + 3;
|
||||
}
|
||||
|
||||
update() {
|
||||
this.x += this.vx;
|
||||
this.y += this.vy;
|
||||
this.vy += 0.2;
|
||||
this.life -= 0.02;
|
||||
this.size *= 0.98;
|
||||
}
|
||||
|
||||
draw() {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = this.life;
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.shadowColor = this.color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Eingabe-Handler
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const key = e.key.toLowerCase();
|
||||
if (!keyPressed[key] && laneKeys.includes(key) && gameRunning) {
|
||||
keyPressed[key] = true;
|
||||
checkHit(laneKeys.indexOf(key));
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keyPressed[e.key.toLowerCase()] = false;
|
||||
});
|
||||
|
||||
// Note-Generierung basierend auf Rhythmus
|
||||
function generateNotes() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Generiere Noten im Beat
|
||||
if (currentTime - lastBeatTime >= BEAT_INTERVAL) {
|
||||
lastBeatTime = currentTime;
|
||||
beatCount++;
|
||||
|
||||
// Beat-Indikator
|
||||
beatIndicator.classList.add('pulse');
|
||||
setTimeout(() => beatIndicator.classList.remove('pulse'), 400);
|
||||
|
||||
// Hintergrund-Puls
|
||||
bgPulse.style.animation = 'bgPulse 0.5s ease-out';
|
||||
setTimeout(() => bgPulse.style.animation = '', 500);
|
||||
|
||||
// Generiere Noten basierend auf Muster
|
||||
const patterns = [
|
||||
[0], [1], [2], [3], // Einzelne Noten
|
||||
[0, 2], [1, 3], // Doppelnoten
|
||||
[0, 1], [2, 3], // Nebeneinander
|
||||
[0, 3], [1, 2], // Außen/Innen
|
||||
[0, 1, 2], [1, 2, 3], // Dreifach
|
||||
[0, 1, 2, 3] // Alle (selten)
|
||||
];
|
||||
|
||||
// Wähle Muster basierend auf Schwierigkeit
|
||||
const difficulty = Math.min(Math.floor(score / 1000), 5);
|
||||
const maxPatternIndex = Math.min(3 + difficulty * 2, patterns.length - 1);
|
||||
const pattern = patterns[Math.floor(Math.random() * (maxPatternIndex + 1))];
|
||||
|
||||
// Manchmal keine Note für Variation
|
||||
if (Math.random() < 0.8) {
|
||||
pattern.forEach(lane => {
|
||||
notes.push(new Note(lane));
|
||||
createLaneGlow(lane);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lane-Glow-Effekt
|
||||
function createLaneGlow(lane) {
|
||||
const x = lane * LANE_WIDTH + LANE_WIDTH / 2;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
particles.push(new Particle(
|
||||
x + (Math.random() - 0.5) * LANE_WIDTH,
|
||||
0,
|
||||
laneColors[lane]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Treffer prüfen
|
||||
function checkHit(lane) {
|
||||
let hitNote = null;
|
||||
let hitQuality = null;
|
||||
|
||||
// Finde die nächste Note in der Lane
|
||||
for (const note of notes) {
|
||||
if (note.lane === lane && !note.hit && !note.missed) {
|
||||
const distance = Math.abs(note.y - TARGET_Y);
|
||||
|
||||
if (distance <= PERFECT_RANGE) {
|
||||
hitNote = note;
|
||||
hitQuality = 'perfect';
|
||||
break;
|
||||
} else if (distance <= GOOD_RANGE) {
|
||||
hitNote = note;
|
||||
hitQuality = 'good';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hitNote) {
|
||||
hitNote.hit = true;
|
||||
|
||||
// Punkte und Combo
|
||||
if (hitQuality === 'perfect') {
|
||||
score += 100 + combo * 10;
|
||||
combo++;
|
||||
createFloatingText(hitNote.x, TARGET_Y, 'PERFECT!', '#00ff00');
|
||||
} else {
|
||||
score += 50;
|
||||
combo = 0;
|
||||
createFloatingText(hitNote.x, TARGET_Y, 'GOOD', '#ffff00');
|
||||
}
|
||||
|
||||
maxCombo = Math.max(maxCombo, combo);
|
||||
scoreElement.textContent = score;
|
||||
comboElement.textContent = combo;
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Partikel-Explosion
|
||||
for (let i = 0; i < 20; i++) {
|
||||
particles.push(new Particle(hitNote.x, TARGET_Y, hitNote.color));
|
||||
}
|
||||
|
||||
// Lane-Effekt
|
||||
drawLaneHit(lane);
|
||||
} else {
|
||||
// Verfehlt
|
||||
combo = 0;
|
||||
comboElement.textContent = combo;
|
||||
health = Math.max(0, health - 5);
|
||||
updateHealthBar();
|
||||
}
|
||||
}
|
||||
|
||||
// Note verfehlt
|
||||
function missNote() {
|
||||
combo = 0;
|
||||
comboElement.textContent = combo;
|
||||
health = Math.max(0, health - 10);
|
||||
updateHealthBar();
|
||||
|
||||
if (health <= 0) {
|
||||
gameOver();
|
||||
}
|
||||
}
|
||||
|
||||
// Gesundheitsanzeige aktualisieren
|
||||
function updateHealthBar() {
|
||||
healthFill.style.width = health + '%';
|
||||
if (health <= 30) {
|
||||
healthFill.style.background = 'linear-gradient(90deg, #ff0000, #cc0000)';
|
||||
}
|
||||
}
|
||||
|
||||
// Schwebender Text
|
||||
function createFloatingText(x, y, text, color) {
|
||||
const textElement = document.createElement('div');
|
||||
textElement.className = color === '#00ff00' ? 'perfect-text' : 'good-text';
|
||||
textElement.textContent = text;
|
||||
textElement.style.left = x + 'px';
|
||||
textElement.style.top = y + 'px';
|
||||
document.body.appendChild(textElement);
|
||||
|
||||
setTimeout(() => textElement.remove(), 1000);
|
||||
}
|
||||
|
||||
// Lane-Hit-Effekt
|
||||
function drawLaneHit(lane) {
|
||||
const x = lane * LANE_WIDTH;
|
||||
|
||||
ctx.save();
|
||||
ctx.fillStyle = laneColors[lane];
|
||||
ctx.globalAlpha = 0.3;
|
||||
ctx.fillRect(x, TARGET_Y - 50, LANE_WIDTH, 100);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Update
|
||||
function update() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
// Update Noten
|
||||
for (let i = notes.length - 1; i >= 0; i--) {
|
||||
notes[i].update();
|
||||
|
||||
// Entferne alte Noten
|
||||
if (notes[i].y > canvas.height + NOTE_HEIGHT) {
|
||||
notes.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Partikel
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
particles[i].update();
|
||||
if (particles[i].life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Generiere neue Noten
|
||||
generateNotes();
|
||||
}
|
||||
|
||||
// Zeichnen
|
||||
function draw() {
|
||||
// Clear
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Zeichne Lanes
|
||||
for (let i = 0; i < LANES; i++) {
|
||||
const x = i * LANE_WIDTH;
|
||||
|
||||
// Lane-Linien
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, canvas.height);
|
||||
ctx.stroke();
|
||||
|
||||
// Lane-Hintergrund (leichter Gradient)
|
||||
const gradient = ctx.createLinearGradient(x, 0, x, canvas.height);
|
||||
gradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
|
||||
gradient.addColorStop(0.8, `${laneColors[i]}20`);
|
||||
gradient.addColorStop(1, `${laneColors[i]}40`);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fillRect(x, 0, LANE_WIDTH, canvas.height);
|
||||
}
|
||||
|
||||
// Zeichne Ziellinie
|
||||
ctx.strokeStyle = '#ffffff';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.shadowBlur = 20;
|
||||
ctx.shadowColor = '#ffffff';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, TARGET_Y);
|
||||
ctx.lineTo(canvas.width, TARGET_Y);
|
||||
ctx.stroke();
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
// Zeichne Zielzonen
|
||||
for (let i = 0; i < LANES; i++) {
|
||||
const x = i * LANE_WIDTH + LANE_WIDTH / 2;
|
||||
|
||||
// Äußerer Ring (Good-Bereich)
|
||||
ctx.strokeStyle = laneColors[i] + '30';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, TARGET_Y, GOOD_RANGE, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Mittlerer Ring (Perfect-Bereich)
|
||||
ctx.strokeStyle = laneColors[i] + '60';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, TARGET_Y, PERFECT_RANGE, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Innerer Kreis (Zielbereich)
|
||||
ctx.fillStyle = laneColors[i] + '40';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, TARGET_Y, 15, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Mittelpunkt
|
||||
ctx.fillStyle = laneColors[i];
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, TARGET_Y, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Taste anzeigen
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = 'bold 24px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(laneKeys[i].toUpperCase(), x, TARGET_Y + 40);
|
||||
}
|
||||
|
||||
// Zeichne Noten
|
||||
notes.forEach(note => note.draw());
|
||||
|
||||
// Zeichne Partikel
|
||||
particles.forEach(particle => particle.draw());
|
||||
|
||||
// Combo-Multiplikator anzeigen
|
||||
if (combo >= 10) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = '#00ffff';
|
||||
ctx.font = 'bold 48px Arial';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.shadowBlur = 30;
|
||||
ctx.shadowColor = '#00ffff';
|
||||
ctx.globalAlpha = 0.3 + Math.sin(Date.now() * 0.005) * 0.2;
|
||||
ctx.fillText(combo + 'x', canvas.width / 2, 100);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
// Game Loop
|
||||
function gameLoop() {
|
||||
update();
|
||||
draw();
|
||||
|
||||
if (gameRunning) {
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
}
|
||||
|
||||
// Spiel starten
|
||||
function startGame() {
|
||||
startScreen.style.display = 'none';
|
||||
gameRunning = true;
|
||||
score = 0;
|
||||
combo = 0;
|
||||
maxCombo = 0;
|
||||
health = 100;
|
||||
notes = [];
|
||||
particles = [];
|
||||
lastBeatTime = Date.now();
|
||||
beatCount = 0;
|
||||
|
||||
scoreElement.textContent = score;
|
||||
comboElement.textContent = combo;
|
||||
updateHealthBar();
|
||||
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Game Over
|
||||
function gameOver() {
|
||||
gameRunning = false;
|
||||
document.getElementById('finalScore').textContent = score;
|
||||
document.getElementById('maxCombo').textContent = maxCombo;
|
||||
gameOverScreen.style.display = 'block';
|
||||
|
||||
// Sende Game Over Event
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 2000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'rhythm_master',
|
||||
name: 'Rhythm Master',
|
||||
description: 'Score 2000 points in Rhythm Defender',
|
||||
icon: '🎵'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (maxCombo >= 50) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'combo_king',
|
||||
name: 'Combo King',
|
||||
description: 'Achieve a 50x combo in Rhythm Defender',
|
||||
icon: '🔥'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Neustart
|
||||
function restartGame() {
|
||||
gameOverScreen.style.display = 'none';
|
||||
startGame();
|
||||
}
|
||||
|
||||
// Debug
|
||||
console.log('Rhythm Defender geladen!');
|
||||
console.log('Ein Rhythmus-basiertes Verteidigungsspiel.');
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
662
games/mana-games/apps/web/public/games/snake_game.html
Normal file
|
|
@ -0,0 +1,662 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Snake Spiel</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #000;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #00ffff;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score {
|
||||
font-size: 16px;
|
||||
margin-bottom: 5px;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 2px solid #00ffff;
|
||||
background: #000;
|
||||
display: block;
|
||||
image-rendering: pixelated;
|
||||
image-rendering: -moz-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
border: 2px solid #00ffff;
|
||||
padding: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background: #000;
|
||||
color: #00ffff;
|
||||
border: 1px solid #00ffff;
|
||||
padding: 8px 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
background: #00ffff;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<div class="score">SCORE: <span id="score">0</span></div>
|
||||
<canvas id="gameCanvas" width="400" height="400"></canvas>
|
||||
<div class="game-over" id="gameOver">
|
||||
<div>GAME OVER</div>
|
||||
<div>SCORE: <span id="finalScore">0</span></div>
|
||||
<button class="restart-btn" onclick="restartGame()">RESTART</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ======================== CANVAS UND UI ELEMENTE ========================
|
||||
// Hole die Canvas und 2D Kontext für das Zeichnen des Spiels
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// UI Elemente für Score-Anzeige und Game Over Screen
|
||||
const scoreElement = document.getElementById('score');
|
||||
const gameOverElement = document.getElementById('gameOver');
|
||||
const finalScoreElement = document.getElementById('finalScore');
|
||||
|
||||
// ======================== SPIEL-KONSTANTEN ========================
|
||||
// Größe eines einzelnen Feldes/Kachel in Pixeln
|
||||
const gridSize = 20;
|
||||
// Anzahl der Kacheln in jeder Richtung (20x20 Grid bei 400px Canvas)
|
||||
const tileCount = canvas.width / gridSize;
|
||||
|
||||
// ======================== SPIEL-ZUSTAND ========================
|
||||
// Snake Array - jedes Element ist ein Objekt mit x,y Koordinaten
|
||||
// Index 0 ist der Kopf der Schlange
|
||||
let snake = [{x: 10, y: 10}];
|
||||
|
||||
// Position des Essens als Objekt mit x,y Koordinaten
|
||||
let food = {};
|
||||
|
||||
// Bewegungsrichtung der Schlange (-1, 0, 1 für jede Achse)
|
||||
let dx = 0; // Horizontale Bewegung: -1 = links, 0 = keine, 1 = rechts
|
||||
let dy = 0; // Vertikale Bewegung: -1 = oben, 0 = keine, 1 = unten
|
||||
|
||||
// Aktueller Punktestand
|
||||
let score = 0;
|
||||
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'snake';
|
||||
|
||||
// Flag ob das Spiel läuft oder pausiert/beendet ist
|
||||
let gameRunning = true;
|
||||
|
||||
// Zeit-Management für konstante Bewegungsgeschwindigkeit
|
||||
let lastMoveTime = 0; // Zeitstempel der letzten Bewegung
|
||||
let moveInterval = 120; // Millisekunden zwischen Bewegungen (Start-Geschwindigkeit)
|
||||
|
||||
// ======================== INPUT STEUERUNG ========================
|
||||
// Queue für Tasteneingaben - ermöglicht schnelle Richtungswechsel ohne Verlust
|
||||
// Maximal 3 Eingaben werden gespeichert
|
||||
let inputQueue = [];
|
||||
// Letzte tatsächliche Bewegungsrichtung (verhindert 180° Drehungen)
|
||||
let lastDirection = { dx: 0, dy: 0 };
|
||||
|
||||
// ======================== GAME OVER ANIMATION ========================
|
||||
// Flag ob die Explosions-Animation läuft
|
||||
let gameOverAnimation = false;
|
||||
// Array mit Partikeln für die Explosion
|
||||
let explosionParticles = [];
|
||||
// Startzeit der Animation für Timing
|
||||
let animationStartTime = 0;
|
||||
|
||||
// ======================== BESUCHTE FELDER TRACKING ========================
|
||||
// 2D Array das speichert, wie oft jedes Feld besucht wurde
|
||||
// 0 = unbesucht (schwarz)
|
||||
// 1 = 1x besucht (blau)
|
||||
// 2 = 2x besucht (rot mit Streifen)
|
||||
// 3 = 3x besucht (magenta mit Kreuz) - tödlich!
|
||||
let visitedGrid = Array(tileCount).fill().map(() => Array(tileCount).fill(0));
|
||||
|
||||
// ======================== FARB-PALETTE ========================
|
||||
// Zentrale Definition aller Farben für konsistentes Design
|
||||
// und bessere Performance (weniger String-Allokationen)
|
||||
const COLORS = {
|
||||
background: '#000', // Schwarzer Hintergrund
|
||||
snakeHead: '#00ffff', // Cyan für Schlangenkopf
|
||||
snakeBody: '#0088aa', // Dunkleres Cyan für Körper
|
||||
food: '#ffff00', // Gelb für Essen
|
||||
border: '#ffffff', // Weiße Ränder
|
||||
visited1: '#4444aa', // Blau für 1x besuchte Felder
|
||||
visited2: '#aa4444', // Rot für 2x besuchte Felder
|
||||
visited3: '#aa44aa', // Magenta für 3x besuchte (tödliche) Felder
|
||||
pattern: '#ffffff' // Weiß für Muster auf besuchten Feldern
|
||||
};
|
||||
|
||||
// ======================== ESSEN GENERATION ========================
|
||||
/**
|
||||
* Generiert eine neue zufällige Position für das Essen.
|
||||
* Stellt sicher, dass das Essen nicht auf der Schlange erscheint.
|
||||
*/
|
||||
function generateFood() {
|
||||
// Wiederhole bis eine freie Position gefunden wird
|
||||
do {
|
||||
food = {
|
||||
x: Math.floor(Math.random() * tileCount),
|
||||
y: Math.floor(Math.random() * tileCount)
|
||||
};
|
||||
// Prüfe ob irgendein Schlangen-Segment auf dieser Position ist
|
||||
} while (snake.some(segment => segment.x === food.x && segment.y === food.y));
|
||||
}
|
||||
|
||||
// ======================== HAUPT-GAME-LOOP ========================
|
||||
/**
|
||||
* Die zentrale Game Loop die kontinuierlich läuft.
|
||||
* Wird von requestAnimationFrame aufgerufen für 60 FPS.
|
||||
*
|
||||
* @param {number} currentTime - Aktuelle Zeit in Millisekunden
|
||||
*/
|
||||
function gameLoop(currentTime) {
|
||||
// Spezialbehandlung während der Game Over Animation
|
||||
if (gameOverAnimation) {
|
||||
updateExplosion(currentTime); // Bewege Explosions-Partikel
|
||||
drawGame(); // Zeichne normales Spielfeld
|
||||
drawExplosion(currentTime); // Zeichne Explosion darüber
|
||||
requestAnimationFrame(gameLoop);
|
||||
return;
|
||||
}
|
||||
|
||||
// Beende Loop wenn Spiel nicht läuft
|
||||
if (!gameRunning) return;
|
||||
|
||||
// Verarbeite gespeicherte Tasteneingaben
|
||||
processInputQueue();
|
||||
|
||||
// Bewegung nur in festgelegten Intervallen (nicht jeden Frame)
|
||||
// Dies erzeugt die klassische "ruckartige" Snake-Bewegung
|
||||
if (currentTime - lastMoveTime >= moveInterval) {
|
||||
moveSnake();
|
||||
lastMoveTime = currentTime;
|
||||
}
|
||||
|
||||
// Zeichne jeden Frame für flüssige Darstellung
|
||||
// (auch wenn Bewegung nur alle 120ms erfolgt)
|
||||
drawGame();
|
||||
|
||||
// Nächsten Frame anfordern
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ======================== INPUT VERARBEITUNG ========================
|
||||
/**
|
||||
* Verarbeitet die nächste Eingabe aus der Input-Queue.
|
||||
* Verhindert 180° Drehungen (Rückwärtsbewegung in sich selbst).
|
||||
*/
|
||||
function processInputQueue() {
|
||||
// Keine Eingaben vorhanden
|
||||
if (inputQueue.length === 0) return;
|
||||
|
||||
// Hole und entferne erste Eingabe aus Queue
|
||||
const nextMove = inputQueue.shift();
|
||||
|
||||
// Prüfe ob die Bewegung gültig ist (keine 180° Drehung)
|
||||
// Beispiel: Wenn Schlange nach rechts (dx=1) läuft,
|
||||
// ist links (dx=-1) nicht erlaubt
|
||||
if ((nextMove.dx !== 0 && lastDirection.dx !== -nextMove.dx) ||
|
||||
(nextMove.dy !== 0 && lastDirection.dy !== -nextMove.dy)) {
|
||||
// Setze neue Bewegungsrichtung
|
||||
dx = nextMove.dx;
|
||||
dy = nextMove.dy;
|
||||
// Speichere als letzte Richtung für nächste Prüfung
|
||||
lastDirection = { dx, dy };
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== HAUPT-ZEICHENFUNKTION ========================
|
||||
/**
|
||||
* Zeichnet das gesamte Spiel.
|
||||
* Reihenfolge ist wichtig für korrekte Überlagerung.
|
||||
*/
|
||||
function drawGame() {
|
||||
clearCanvas(); // 1. Lösche alles und zeichne Hintergrund + besuchte Felder
|
||||
drawFood(); // 2. Zeichne Essen
|
||||
drawSnake(); // 3. Zeichne Schlange (über allem anderen)
|
||||
checkCollision(); // 4. Prüfe Kollisionen
|
||||
}
|
||||
|
||||
// ======================== CANVAS LÖSCHEN UND HINTERGRUND ========================
|
||||
/**
|
||||
* Löscht das Canvas und zeichnet den Hintergrund inklusive besuchter Felder.
|
||||
* Optimiert durch Batch-Rendering gleicher Farben.
|
||||
*/
|
||||
function clearCanvas() {
|
||||
// Lösche gesamtes Canvas mit schwarzem Hintergrund
|
||||
ctx.fillStyle = COLORS.background;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Zeichne alle besuchten Felder nach Level gruppiert
|
||||
// Dies minimiert ctx.fillStyle Änderungen für bessere Performance
|
||||
for (let level = 1; level <= 3; level++) {
|
||||
// Setze Farbe für aktuelles Level
|
||||
ctx.fillStyle = level === 1 ? COLORS.visited1 :
|
||||
level === 2 ? COLORS.visited2 : COLORS.visited3;
|
||||
|
||||
// Durchlaufe gesamtes Grid
|
||||
for (let x = 0; x < tileCount; x++) {
|
||||
for (let y = 0; y < tileCount; y++) {
|
||||
// Zeichne nur wenn Feld dieses Level hat
|
||||
if (visitedGrid[x][y] === level) {
|
||||
// Prüfe ob Schlange auf diesem Feld ist
|
||||
const isSnakeField = snake.some(segment => segment.x === x && segment.y === y);
|
||||
// Zeichne nur wenn Schlange NICHT auf dem Feld ist
|
||||
if (!isSnakeField) {
|
||||
ctx.fillRect(x * gridSize, y * gridSize, gridSize, gridSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zeichne Muster auf besuchten Feldern (Level 2 und 3)
|
||||
// Alle Muster verwenden die gleiche Farbe für Performance
|
||||
ctx.fillStyle = COLORS.pattern;
|
||||
for (let x = 0; x < tileCount; x++) {
|
||||
for (let y = 0; y < tileCount; y++) {
|
||||
const level = visitedGrid[x][y];
|
||||
const isSnakeField = snake.some(segment => segment.x === x && segment.y === y);
|
||||
|
||||
// Nur Muster für Level 2 und 3, nicht wo Schlange ist
|
||||
if (!isSnakeField && level > 1) {
|
||||
// Berechne Pixel-Position des Feldes
|
||||
const baseX = x * gridSize;
|
||||
const baseY = y * gridSize;
|
||||
|
||||
if (level === 2) {
|
||||
// Vertikale Streifen für Level 2
|
||||
ctx.fillRect(baseX + 5, baseY, 2, gridSize); // Linker Streifen
|
||||
ctx.fillRect(baseX + 13, baseY, 2, gridSize); // Rechter Streifen
|
||||
} else if (level === 3) {
|
||||
// Kreuz-Muster für Level 3 (tödliche Felder)
|
||||
ctx.fillRect(baseX + 8, baseY, 4, gridSize); // Vertikaler Balken
|
||||
ctx.fillRect(baseX, baseY + 8, gridSize, 4); // Horizontaler Balken
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== SCHLANGE ZEICHNEN ========================
|
||||
/**
|
||||
* Zeichnet die Schlange mit unterschiedlichen Farben für Kopf und Körper.
|
||||
* Fügt weiße Ränder für bessere Sichtbarkeit hinzu.
|
||||
*/
|
||||
function drawSnake() {
|
||||
// Verstecke Schlange während Explosions-Animation
|
||||
if (gameOverAnimation) return;
|
||||
|
||||
// Zeichne alle Segmente der Schlange
|
||||
snake.forEach((segment, index) => {
|
||||
// Kopf (index 0) ist heller als Körper
|
||||
ctx.fillStyle = index === 0 ? COLORS.snakeHead : COLORS.snakeBody;
|
||||
ctx.fillRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);
|
||||
});
|
||||
|
||||
// Zeichne Ränder für alle Segmente in einem Durchgang
|
||||
// (Performance-Optimierung: nur einmal Stil setzen)
|
||||
ctx.strokeStyle = COLORS.border;
|
||||
ctx.lineWidth = 1;
|
||||
snake.forEach(segment => {
|
||||
ctx.strokeRect(segment.x * gridSize, segment.y * gridSize, gridSize, gridSize);
|
||||
});
|
||||
}
|
||||
|
||||
// ======================== ESSEN ZEICHNEN ========================
|
||||
/**
|
||||
* Zeichnet das Essen als gelbes Quadrat mit weißem Rand.
|
||||
*/
|
||||
function drawFood() {
|
||||
// Gelbes Quadrat für das Essen
|
||||
ctx.fillStyle = COLORS.food;
|
||||
ctx.fillRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);
|
||||
|
||||
// Weißer Rand für bessere Sichtbarkeit
|
||||
ctx.strokeStyle = COLORS.border;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(food.x * gridSize, food.y * gridSize, gridSize, gridSize);
|
||||
}
|
||||
|
||||
// ======================== SCHLANGEN-BEWEGUNG ========================
|
||||
/**
|
||||
* Bewegt die Schlange in die aktuelle Richtung.
|
||||
* Behandelt Wrap-Around an den Rändern, Essen-Aufnahme und Kollisionen.
|
||||
*/
|
||||
function moveSnake() {
|
||||
// Keine Bewegung wenn Schlange stillsteht (Spielstart)
|
||||
if (dx === 0 && dy === 0) return;
|
||||
|
||||
// Berechne neue Kopfposition
|
||||
let head = {x: snake[0].x + dx, y: snake[0].y + dy};
|
||||
|
||||
// Wrap-Around: Schlange erscheint auf der anderen Seite
|
||||
if (head.x < 0) head.x = tileCount - 1; // Links raus -> rechts rein
|
||||
if (head.x >= tileCount) head.x = 0; // Rechts raus -> links rein
|
||||
if (head.y < 0) head.y = tileCount - 1; // Oben raus -> unten rein
|
||||
if (head.y >= tileCount) head.y = 0; // Unten raus -> oben rein
|
||||
|
||||
// Prüfe ob neues Feld tödlich ist (3x besucht = rot mit Kreuz)
|
||||
if (visitedGrid[head.x][head.y] === 3) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
|
||||
// Füge neuen Kopf am Anfang des Arrays hinzu
|
||||
snake.unshift(head);
|
||||
|
||||
// Prüfe ob Essen gegessen wurde
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
// Essen aufgenommen: Score erhöhen, neues Essen generieren
|
||||
score += 10;
|
||||
scoreElement.textContent = score;
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
generateFood();
|
||||
// Spiel wird schneller (min. 80ms zwischen Bewegungen)
|
||||
moveInterval = Math.max(80, moveInterval - 1);
|
||||
// Schwanz wird NICHT entfernt -> Schlange wächst
|
||||
} else {
|
||||
// Kein Essen: Entferne Schwanz (Schlange bleibt gleich lang)
|
||||
const tail = snake.pop();
|
||||
// Erhöhe Besuchszähler für verlassenes Feld (max. 3)
|
||||
visitedGrid[tail.x][tail.y] = Math.min(3, visitedGrid[tail.x][tail.y] + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== KOLLISIONSPRÜFUNG ========================
|
||||
/**
|
||||
* Prüft ob die Schlange mit sich selbst kollidiert.
|
||||
* Wandkollisionen gibt es nicht (Wrap-Around).
|
||||
*/
|
||||
function checkCollision() {
|
||||
const head = snake[0];
|
||||
// Prüfe Kollision mit jedem Körpersegment (nicht mit Kopf selbst)
|
||||
for (let i = 1; i < snake.length; i++) {
|
||||
if (head.x === snake[i].x && head.y === snake[i].y) {
|
||||
// Schlange hat sich selbst gebissen
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================== GAME OVER BEHANDLUNG ========================
|
||||
/**
|
||||
* Beendet das Spiel und startet die Explosions-Animation.
|
||||
* Erstellt Partikel für jeden Teil der Schlange.
|
||||
*/
|
||||
function gameOver() {
|
||||
// Stoppe Spiellogik
|
||||
gameRunning = false;
|
||||
|
||||
// Sende Game Over Event mit Score für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (score >= 500) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'snake-master',
|
||||
name: 'Snake Meister',
|
||||
description: '500 Punkte in einem Spiel erreicht!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (score >= 1000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'snake-legend',
|
||||
name: 'Snake Legende',
|
||||
description: '1000 Punkte in einem Spiel erreicht!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
// Starte Explosions-Animation
|
||||
gameOverAnimation = true;
|
||||
animationStartTime = performance.now();
|
||||
|
||||
// Erstelle Explosions-Partikel für jedes Schlangen-Segment
|
||||
explosionParticles = [];
|
||||
snake.forEach((segment, index) => {
|
||||
// Berechne Zentrum des Segments in Pixeln
|
||||
const centerX = segment.x * gridSize + gridSize / 2;
|
||||
const centerY = segment.y * gridSize + gridSize / 2;
|
||||
const isHead = index === 0;
|
||||
|
||||
// Erstelle 4 Partikel pro Segment (in 4 Richtungen)
|
||||
for (let i = 0; i < 4; i++) {
|
||||
// Berechne Richtung mit leichter Zufälligkeit
|
||||
const angle = (Math.PI * 2 * i) / 4 + Math.random() * 0.3;
|
||||
const speed = Math.random() * 2 + 2;
|
||||
|
||||
// Erstelle Partikel-Objekt
|
||||
explosionParticles.push({
|
||||
x: centerX, // Start-Position X
|
||||
y: centerY, // Start-Position Y
|
||||
vx: Math.cos(angle) * speed, // Geschwindigkeit X
|
||||
vy: Math.sin(angle) * speed, // Geschwindigkeit Y
|
||||
size: gridSize / 4, // Größe des Partikels
|
||||
life: 1.0, // Lebenszeit (1.0 = 100%)
|
||||
color: isHead ? COLORS.snakeHead : COLORS.snakeBody // Farbe
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Setze finalen Score
|
||||
finalScoreElement.textContent = score;
|
||||
|
||||
// Zeige Game Over Dialog nach 1 Sekunde Animation
|
||||
setTimeout(() => {
|
||||
gameOverElement.style.display = 'block';
|
||||
gameOverAnimation = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// ======================== EXPLOSIONS-UPDATE ========================
|
||||
/**
|
||||
* Aktualisiert die Positionen und Eigenschaften der Explosions-Partikel.
|
||||
* Wird jeden Frame während der Game Over Animation aufgerufen.
|
||||
*
|
||||
* @param {number} currentTime - Aktuelle Zeit (wird hier nicht verwendet)
|
||||
*/
|
||||
function updateExplosion(currentTime) {
|
||||
// Aktualisiere jeden Partikel
|
||||
explosionParticles.forEach(particle => {
|
||||
// Bewege Partikel basierend auf Geschwindigkeit
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
|
||||
// Reibung: Verlangsame Partikel (5% pro Frame)
|
||||
particle.vx *= 0.95;
|
||||
particle.vy *= 0.95;
|
||||
|
||||
// Reduziere Lebenszeit (2% pro Frame)
|
||||
particle.life -= 0.02;
|
||||
});
|
||||
|
||||
// Entferne "tote" Partikel (life <= 0) aus dem Array
|
||||
explosionParticles = explosionParticles.filter(particle => particle.life > 0);
|
||||
}
|
||||
|
||||
// ======================== EXPLOSIONS-ZEICHNUNG ========================
|
||||
/**
|
||||
* Zeichnet alle Explosions-Partikel.
|
||||
* Verwendet Alpha-Transparenz für Fade-Out Effekt.
|
||||
*
|
||||
* @param {number} currentTime - Aktuelle Zeit (wird hier nicht verwendet)
|
||||
*/
|
||||
function drawExplosion(currentTime) {
|
||||
// Zeichne jeden Partikel
|
||||
explosionParticles.forEach(particle => {
|
||||
// Setze Transparenz basierend auf Lebenszeit (fade out)
|
||||
ctx.globalAlpha = Math.max(0, particle.life);
|
||||
ctx.fillStyle = particle.color;
|
||||
|
||||
// Zeichne Partikel als Quadrat (zentriert um Position)
|
||||
ctx.fillRect(
|
||||
particle.x - particle.size/2, // X-Position (zentriert)
|
||||
particle.y - particle.size/2, // Y-Position (zentriert)
|
||||
particle.size, // Breite
|
||||
particle.size // Höhe
|
||||
);
|
||||
});
|
||||
|
||||
// Setze Alpha auf Standard zurück für nächste Zeichenoperationen
|
||||
ctx.globalAlpha = 1.0;
|
||||
}
|
||||
|
||||
// ======================== SPIEL NEUSTART ========================
|
||||
/**
|
||||
* Setzt alle Spielvariablen zurück und startet ein neues Spiel.
|
||||
* Wird vom Restart-Button aufgerufen.
|
||||
*/
|
||||
function restartGame() {
|
||||
// Setze Schlange auf Startposition zurück
|
||||
snake = [{x: 10, y: 10}];
|
||||
|
||||
// Keine Bewegung zu Beginn
|
||||
dx = 0;
|
||||
dy = 0;
|
||||
|
||||
// Reset Score und Geschwindigkeit
|
||||
score = 0;
|
||||
moveInterval = 120;
|
||||
|
||||
// Leere Input-Queue und Richtungs-Tracking
|
||||
inputQueue = [];
|
||||
lastDirection = { dx: 0, dy: 0 };
|
||||
|
||||
// Beende Animationen
|
||||
gameOverAnimation = false;
|
||||
explosionParticles = [];
|
||||
|
||||
// Update UI
|
||||
scoreElement.textContent = score;
|
||||
gameRunning = true;
|
||||
gameOverElement.style.display = 'none';
|
||||
|
||||
// Reset besuchte Felder (alle auf 0)
|
||||
visitedGrid = Array(tileCount).fill().map(() => Array(tileCount).fill(0));
|
||||
|
||||
// Generiere neues Essen und starte Game Loop
|
||||
generateFood();
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// ======================== TASTATUR-STEUERUNG ========================
|
||||
/**
|
||||
* Event-Listener für Tastatureingaben.
|
||||
* Unterstützt Pfeiltasten und WASD.
|
||||
* Verwendet eine Queue für responsive Steuerung bei schnellen Eingaben.
|
||||
*/
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Ignoriere Eingaben wenn Spiel nicht läuft
|
||||
if (!gameRunning) return;
|
||||
|
||||
let newMove = null;
|
||||
|
||||
// Mappe Tasten zu Bewegungsrichtungen
|
||||
switch(e.key) {
|
||||
case 'ArrowUp': // Pfeil nach oben
|
||||
case 'w': // W-Taste
|
||||
case 'W':
|
||||
newMove = {dx: 0, dy: -1}; // Nach oben
|
||||
break;
|
||||
case 'ArrowDown': // Pfeil nach unten
|
||||
case 's': // S-Taste
|
||||
case 'S':
|
||||
newMove = {dx: 0, dy: 1}; // Nach unten
|
||||
break;
|
||||
case 'ArrowLeft': // Pfeil nach links
|
||||
case 'a': // A-Taste
|
||||
case 'A':
|
||||
newMove = {dx: -1, dy: 0}; // Nach links
|
||||
break;
|
||||
case 'ArrowRight': // Pfeil nach rechts
|
||||
case 'd': // D-Taste
|
||||
case 'D':
|
||||
newMove = {dx: 1, dy: 0}; // Nach rechts
|
||||
break;
|
||||
}
|
||||
|
||||
// Füge gültige Bewegung zur Queue hinzu
|
||||
if (newMove && inputQueue.length < 3) { // Max. 3 Eingaben speichern
|
||||
// Verhindere identische aufeinanderfolgende Eingaben
|
||||
const lastInQueue = inputQueue[inputQueue.length - 1];
|
||||
if (!lastInQueue || lastInQueue.dx !== newMove.dx || lastInQueue.dy !== newMove.dy) {
|
||||
inputQueue.push(newMove);
|
||||
}
|
||||
// Verhindere Standard-Scrolling bei Pfeiltasten
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// ======================== SPIEL INITIALISIERUNG ========================
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
|
||||
// Generiere erstes Essen
|
||||
generateFood();
|
||||
// Starte Game Loop
|
||||
requestAnimationFrame(gameLoop);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
508
games/mana-games/apps/web/public/games/space_defender_game.html
Normal file
|
|
@ -0,0 +1,508 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Space Defender</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: linear-gradient(135deg, #0c0c1e 0%, #1a0033 50%, #000814 100%);
|
||||
font-family: 'Courier New', monospace;
|
||||
color: #00ff88;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2px solid #00ff88;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 30px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
#gameCanvas {
|
||||
background: linear-gradient(180deg, #000814 0%, #001a2e 100%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ui {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
font-size: 18px;
|
||||
text-shadow: 0 0 10px #00ff88;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 30px;
|
||||
border: 2px solid #ff0044;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 30px rgba(255, 0, 68, 0.5);
|
||||
display: none;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
color: #ff0044;
|
||||
font-size: 28px;
|
||||
margin-bottom: 15px;
|
||||
text-shadow: 0 0 15px #ff0044;
|
||||
}
|
||||
|
||||
.restart-btn {
|
||||
background: linear-gradient(45deg, #00ff88, #00cc6a);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 15px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.restart-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 20px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
<div class="ui">
|
||||
<div>Score: <span id="score">0</span></div>
|
||||
<div>Schwierigkeit: <span id="difficulty">1.0</span></div>
|
||||
<div>Zeit: <span id="time">0</span>s</div>
|
||||
</div>
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2>GAME OVER</h2>
|
||||
<p>Finaler Score: <span id="finalScore">0</span></p>
|
||||
<button class="restart-btn" onclick="restartGame()">Nochmal spielen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<p>🎮 Steuerung: A/D oder ←/→ zum Bewegen • LEERTASTE zum Schießen</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'space-defender';
|
||||
|
||||
// Spielzustand
|
||||
let gameState = {
|
||||
player: { x: 375, y: 550, width: 50, height: 30, speed: 8 },
|
||||
bullets: [],
|
||||
enemies: [],
|
||||
particles: [],
|
||||
score: 0,
|
||||
lives: 1,
|
||||
gameRunning: true,
|
||||
keys: {},
|
||||
enemySpawnTimer: 0,
|
||||
level: 1,
|
||||
difficulty: 1,
|
||||
timeAlive: 0
|
||||
};
|
||||
|
||||
// Tasteneingaben
|
||||
document.addEventListener('keydown', (e) => {
|
||||
gameState.keys[e.key.toLowerCase()] = true;
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
gameState.keys[e.key.toLowerCase()] = false;
|
||||
});
|
||||
|
||||
// Spieler
|
||||
function updatePlayer() {
|
||||
if (gameState.keys['a'] || gameState.keys['arrowleft']) {
|
||||
gameState.player.x -= gameState.player.speed;
|
||||
}
|
||||
if (gameState.keys['d'] || gameState.keys['arrowright']) {
|
||||
gameState.player.x += gameState.player.speed;
|
||||
}
|
||||
|
||||
// Grenzen
|
||||
gameState.player.x = Math.max(0, Math.min(canvas.width - gameState.player.width, gameState.player.x));
|
||||
|
||||
// Schießen
|
||||
if (gameState.keys[' ']) {
|
||||
shootBullet();
|
||||
gameState.keys[' '] = false; // Verhindert Dauerfeuer
|
||||
}
|
||||
}
|
||||
|
||||
function drawPlayer() {
|
||||
const p = gameState.player;
|
||||
|
||||
// Raumschiff-Design
|
||||
ctx.fillStyle = '#00ff88';
|
||||
ctx.fillRect(p.x + 20, p.y, 10, 20);
|
||||
ctx.fillStyle = '#00cc6a';
|
||||
ctx.fillRect(p.x + 15, p.y + 10, 20, 15);
|
||||
ctx.fillStyle = '#0088ff';
|
||||
ctx.fillRect(p.x + 5, p.y + 20, 10, 10);
|
||||
ctx.fillRect(p.x + 35, p.y + 20, 10, 10);
|
||||
|
||||
// Glowing effect
|
||||
ctx.shadowColor = '#00ff88';
|
||||
ctx.shadowBlur = 10;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(p.x + 22, p.y + 2, 6, 8);
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
// Projektile
|
||||
function shootBullet() {
|
||||
gameState.bullets.push({
|
||||
x: gameState.player.x + gameState.player.width / 2 - 2,
|
||||
y: gameState.player.y,
|
||||
width: 4,
|
||||
height: 10,
|
||||
speed: 12
|
||||
});
|
||||
}
|
||||
|
||||
function updateBullets() {
|
||||
gameState.bullets = gameState.bullets.filter(bullet => {
|
||||
bullet.y -= bullet.speed;
|
||||
return bullet.y > -bullet.height;
|
||||
});
|
||||
}
|
||||
|
||||
function drawBullets() {
|
||||
gameState.bullets.forEach(bullet => {
|
||||
ctx.fillStyle = '#ffff00';
|
||||
ctx.shadowColor = '#ffff00';
|
||||
ctx.shadowBlur = 5;
|
||||
ctx.fillRect(bullet.x, bullet.y, bullet.width, bullet.height);
|
||||
ctx.shadowBlur = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Gegner
|
||||
function spawnEnemy() {
|
||||
const baseSpeed = 1 + (gameState.difficulty * 0.3);
|
||||
const types = [
|
||||
{ width: 40, height: 30, speed: baseSpeed * 1.2, color: '#ff0044', points: 10 },
|
||||
{ width: 30, height: 25, speed: baseSpeed * 1.8, color: '#ff8800', points: 15 },
|
||||
{ width: 35, height: 35, speed: baseSpeed * 0.8, color: '#8800ff', points: 20 },
|
||||
{ width: 25, height: 20, speed: baseSpeed * 2.5, color: '#00ff44', points: 25 }, // Schneller grüner Gegner
|
||||
{ width: 50, height: 40, speed: baseSpeed * 0.6, color: '#ff00ff', points: 35 } // Großer langsamer Boss
|
||||
];
|
||||
|
||||
// Mehr Gegnertypen freischalten mit steigender Schwierigkeit
|
||||
const availableTypes = types.slice(0, Math.min(3 + Math.floor(gameState.difficulty / 2), types.length));
|
||||
const type = availableTypes[Math.floor(Math.random() * availableTypes.length)];
|
||||
|
||||
gameState.enemies.push({
|
||||
x: Math.random() * (canvas.width - type.width),
|
||||
y: -type.height,
|
||||
...type
|
||||
});
|
||||
}
|
||||
|
||||
function updateEnemies() {
|
||||
// Schwierigkeit erhöhen über Zeit
|
||||
gameState.timeAlive++;
|
||||
if (gameState.timeAlive % 300 === 0) { // Alle 5 Sekunden
|
||||
gameState.difficulty += 0.5;
|
||||
}
|
||||
|
||||
// Dynamische Spawn-Rate basierend auf Schwierigkeit
|
||||
const spawnRate = Math.max(8 - Math.floor(gameState.difficulty), 3);
|
||||
gameState.enemySpawnTimer++;
|
||||
|
||||
if (gameState.enemySpawnTimer > spawnRate) {
|
||||
spawnEnemy();
|
||||
|
||||
// Bei höherer Schwierigkeit manchmal 2 Gegner gleichzeitig spawnen
|
||||
if (gameState.difficulty > 3 && Math.random() < 0.3) {
|
||||
spawnEnemy();
|
||||
}
|
||||
|
||||
// Bei sehr hoher Schwierigkeit gelegentlich 3 Gegner
|
||||
if (gameState.difficulty > 6 && Math.random() < 0.15) {
|
||||
spawnEnemy();
|
||||
}
|
||||
|
||||
gameState.enemySpawnTimer = 0;
|
||||
}
|
||||
|
||||
// Gegner bewegen
|
||||
gameState.enemies = gameState.enemies.filter(enemy => {
|
||||
enemy.y += enemy.speed;
|
||||
|
||||
// Kollision mit Spieler
|
||||
if (isColliding(enemy, gameState.player)) {
|
||||
createExplosion(enemy.x + enemy.width/2, enemy.y + enemy.height/2);
|
||||
gameState.lives = 0; // Sofortiges Game Over
|
||||
return false;
|
||||
}
|
||||
|
||||
return enemy.y < canvas.height + enemy.height;
|
||||
});
|
||||
}
|
||||
|
||||
function drawEnemies() {
|
||||
gameState.enemies.forEach(enemy => {
|
||||
ctx.fillStyle = enemy.color;
|
||||
ctx.shadowColor = enemy.color;
|
||||
ctx.shadowBlur = 8;
|
||||
|
||||
// Alien-Design
|
||||
ctx.fillRect(enemy.x + 5, enemy.y, enemy.width - 10, enemy.height - 5);
|
||||
ctx.fillRect(enemy.x, enemy.y + 10, enemy.width, enemy.height - 15);
|
||||
|
||||
// Augen
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(enemy.x + 8, enemy.y + 5, 6, 6);
|
||||
ctx.fillRect(enemy.x + enemy.width - 14, enemy.y + 5, 6, 6);
|
||||
|
||||
ctx.shadowBlur = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// Kollisionserkennung
|
||||
function isColliding(rect1, rect2) {
|
||||
return rect1.x < rect2.x + rect2.width &&
|
||||
rect1.x + rect1.width > rect2.x &&
|
||||
rect1.y < rect2.y + rect2.height &&
|
||||
rect1.y + rect1.height > rect2.y;
|
||||
}
|
||||
|
||||
// Kollisionen zwischen Projektilen und Gegnern
|
||||
function checkCollisions() {
|
||||
gameState.bullets.forEach((bullet, bulletIndex) => {
|
||||
gameState.enemies.forEach((enemy, enemyIndex) => {
|
||||
if (isColliding(bullet, enemy)) {
|
||||
// Explosion
|
||||
createExplosion(enemy.x + enemy.width/2, enemy.y + enemy.height/2);
|
||||
|
||||
// Score und Entfernung
|
||||
gameState.score += Math.floor(enemy.points * gameState.difficulty);
|
||||
|
||||
// Sende Score Update für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'SCORE_UPDATE',
|
||||
data: { score: gameState.score }
|
||||
}, '*');
|
||||
|
||||
gameState.bullets.splice(bulletIndex, 1);
|
||||
gameState.enemies.splice(enemyIndex, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Partikeleffekte
|
||||
function createExplosion(x, y) {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
gameState.particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: (Math.random() - 0.5) * 8,
|
||||
vy: (Math.random() - 0.5) * 8,
|
||||
life: 30,
|
||||
color: `hsl(${Math.random() * 60 + 10}, 100%, 60%)`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateParticles() {
|
||||
gameState.particles = gameState.particles.filter(particle => {
|
||||
particle.x += particle.vx;
|
||||
particle.y += particle.vy;
|
||||
particle.life--;
|
||||
particle.vx *= 0.98;
|
||||
particle.vy *= 0.98;
|
||||
return particle.life > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function drawParticles() {
|
||||
gameState.particles.forEach(particle => {
|
||||
ctx.globalAlpha = particle.life / 30;
|
||||
ctx.fillStyle = particle.color;
|
||||
ctx.fillRect(particle.x, particle.y, 3, 3);
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Hintergrund mit Sternen
|
||||
function drawBackground() {
|
||||
ctx.fillStyle = '#000814';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Bewegende Sterne
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const x = (i * 7 + Date.now() * 0.01) % canvas.width;
|
||||
const y = (i * 11 + Date.now() * 0.005) % canvas.height;
|
||||
const opacity = Math.sin(Date.now() * 0.001 + i) * 0.5 + 0.5;
|
||||
|
||||
ctx.globalAlpha = opacity * 0.7;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// UI Update
|
||||
function updateUI() {
|
||||
document.getElementById('score').textContent = gameState.score;
|
||||
document.getElementById('difficulty').textContent = gameState.difficulty.toFixed(1);
|
||||
document.getElementById('time').textContent = Math.floor(gameState.timeAlive / 60);
|
||||
}
|
||||
|
||||
// Game Over
|
||||
function gameOver() {
|
||||
gameState.gameRunning = false;
|
||||
document.getElementById('finalScore').textContent = gameState.score;
|
||||
document.getElementById('gameOver').style.display = 'block';
|
||||
|
||||
// Sende Game Over Event mit Score für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: { score: gameState.score }
|
||||
}, '*');
|
||||
|
||||
// Achievement prüfen
|
||||
if (gameState.score >= 1000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'space-defender-1000',
|
||||
name: 'Weltraum Veteran',
|
||||
description: '1000 Punkte erreicht!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (gameState.score >= 5000) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'space-defender-5000',
|
||||
name: 'Weltraum Legende',
|
||||
description: '5000 Punkte erreicht!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (gameState.timeAlive >= 300) { // 5 Minuten
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievement: {
|
||||
id: 'space-survivor',
|
||||
name: 'Überlebenskünstler',
|
||||
description: '5 Minuten überlebt!'
|
||||
}
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
function restartGame() {
|
||||
gameState = {
|
||||
player: { x: 375, y: 550, width: 50, height: 30, speed: 8 },
|
||||
bullets: [],
|
||||
enemies: [],
|
||||
particles: [],
|
||||
score: 0,
|
||||
lives: 1,
|
||||
gameRunning: true,
|
||||
keys: {},
|
||||
enemySpawnTimer: 0,
|
||||
level: 1,
|
||||
difficulty: 1,
|
||||
timeAlive: 0
|
||||
};
|
||||
document.getElementById('gameOver').style.display = 'none';
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Hauptspiel-Loop
|
||||
function gameLoop() {
|
||||
if (!gameState.gameRunning) return;
|
||||
|
||||
// Update
|
||||
updatePlayer();
|
||||
updateBullets();
|
||||
updateEnemies();
|
||||
updateParticles();
|
||||
checkCollisions();
|
||||
updateUI();
|
||||
|
||||
// Game Over prüfen
|
||||
if (gameState.lives <= 0) {
|
||||
gameOver();
|
||||
return;
|
||||
}
|
||||
|
||||
// Zeichnen
|
||||
drawBackground();
|
||||
drawPlayer();
|
||||
drawBullets();
|
||||
drawEnemies();
|
||||
drawParticles();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Sende Game Loaded Event für Statistiken
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
|
||||
// Spiel starten
|
||||
gameLoop();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
791
games/mana-games/apps/web/public/games/turbo_racer.html
Normal file
|
|
@ -0,0 +1,791 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Turbo Racer</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #0a0a0a;
|
||||
color: #fff;
|
||||
font-family: 'Arial Black', sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-container {
|
||||
position: relative;
|
||||
filter: drop-shadow(0 0 30px rgba(255, 0, 100, 0.3));
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 3px solid #ff0066;
|
||||
background: #1a1a1a;
|
||||
box-shadow:
|
||||
inset 0 0 50px rgba(255, 0, 100, 0.1),
|
||||
0 0 30px rgba(0, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ui {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
font-size: 20px;
|
||||
z-index: 10;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
||||
}
|
||||
|
||||
.speed-meter {
|
||||
color: #00ff88;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.lap-counter {
|
||||
color: #ffcc00;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.position {
|
||||
color: #ff0066;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.boost-bar {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
background: rgba(0,0,0,0.5);
|
||||
border: 2px solid #00ffff;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.boost-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00ffff, #ff00ff);
|
||||
width: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.start-screen {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
background: rgba(0,0,0,0.9);
|
||||
padding: 40px;
|
||||
border: 3px solid #ff0066;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 0 30px rgba(255,0,100,0.5);
|
||||
}
|
||||
|
||||
.start-screen h1 {
|
||||
font-size: 48px;
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(45deg, #ff0066, #00ffff);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.start-screen p {
|
||||
font-size: 18px;
|
||||
margin-bottom: 30px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 15px 40px;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(45deg, #ff0066, #ff3388);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 20px rgba(255,0,100,0.5);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.game-over {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
background: rgba(0,0,0,0.95);
|
||||
padding: 40px;
|
||||
border: 3px solid #ffcc00;
|
||||
border-radius: 20px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.game-over h2 {
|
||||
font-size: 36px;
|
||||
margin-bottom: 20px;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.final-time {
|
||||
font-size: 48px;
|
||||
color: #00ff88;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.controls-info {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="game-container">
|
||||
<canvas id="gameCanvas" width="800" height="600"></canvas>
|
||||
|
||||
<div class="ui">
|
||||
<div class="speed-meter">
|
||||
<span id="speed">0</span> km/h
|
||||
</div>
|
||||
<div class="lap-counter">
|
||||
Runde: <span id="lap">0</span>
|
||||
</div>
|
||||
<div class="position">
|
||||
Zeit: <span id="time">0:00</span>
|
||||
</div>
|
||||
<div class="best-lap" style="color: #00ff88; font-size: 18px;">
|
||||
Beste Runde: <span id="bestLap">--:--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="boost-bar">
|
||||
<div class="boost-fill" id="boostFill"></div>
|
||||
</div>
|
||||
|
||||
<div class="start-screen" id="startScreen">
|
||||
<h1>TURBO RACER</h1>
|
||||
<p>Drift durch die Kurven und stelle Bestzeiten auf!</p>
|
||||
<p>🏁 Endlos-Runden • ⚡ Nitro-Boost • 🏆 Drift-Punkte</p>
|
||||
<button onclick="startGame()">RENNEN STARTEN</button>
|
||||
</div>
|
||||
|
||||
<div class="game-over" id="gameOver">
|
||||
<h2 id="gameOverTitle">ZEIT-HERAUSFORDERUNG BEENDET!</h2>
|
||||
<div class="final-time" id="finalTime">0 Runden</div>
|
||||
<p id="finalPosition">Beste Runde: --:--</p>
|
||||
<button onclick="restartGame()">NOCHMAL FAHREN</button>
|
||||
</div>
|
||||
|
||||
<div class="controls-info">
|
||||
↑↓ oder WS: Gas/Bremse | ←→ oder AD: Lenken | Leertaste: Boost
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Game ID für Statistiken
|
||||
const GAME_ID = 'turbo-racer';
|
||||
|
||||
const canvas = document.getElementById('gameCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Spielvariablen
|
||||
let gameRunning = false;
|
||||
let raceStartTime = 0;
|
||||
let currentLap = 1;
|
||||
|
||||
// Eingabe
|
||||
const keys = {};
|
||||
|
||||
// Spieler Auto mit Drift-Physik
|
||||
const player = {
|
||||
x: 400,
|
||||
y: 400,
|
||||
angle: -Math.PI / 2, // Nach oben zeigend
|
||||
velocity: { x: 0, y: 0 },
|
||||
speed: 0,
|
||||
maxSpeed: 4, // Noch langsamer
|
||||
acceleration: 0.2, // Noch sanftere Beschleunigung
|
||||
deceleration: 0.15,
|
||||
turnSpeed: 0.08, // Noch weniger aggressiv
|
||||
width: 30,
|
||||
height: 20,
|
||||
boost: 100,
|
||||
boosting: false,
|
||||
color: '#ff0066',
|
||||
trail: [],
|
||||
driftFactor: 0,
|
||||
driftAngle: 0,
|
||||
lapCount: 0,
|
||||
bestLapTime: null,
|
||||
currentLapStart: 0,
|
||||
lastAngle: 0,
|
||||
crossed: false
|
||||
};
|
||||
|
||||
// Streckenmitte und Radien
|
||||
const trackCenter = { x: 400, y: 300 };
|
||||
const outerRadius = 250;
|
||||
const innerRadius = 120;
|
||||
const trackWidth = outerRadius - innerRadius;
|
||||
|
||||
// Zeit und Runden
|
||||
let currentTime = 0;
|
||||
let lastLapTime = 0;
|
||||
let bestLapTime = Infinity;
|
||||
|
||||
// Partikel für Effekte
|
||||
const particles = [];
|
||||
|
||||
// Sterne für Geschwindigkeitseffekt
|
||||
const stars = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
stars.push({
|
||||
x: Math.random() * canvas.width,
|
||||
y: Math.random() * canvas.height,
|
||||
size: Math.random() * 2
|
||||
});
|
||||
}
|
||||
|
||||
// Input Handling
|
||||
document.addEventListener('keydown', (e) => {
|
||||
keys[e.key.toLowerCase()] = true;
|
||||
if (e.key === ' ') e.preventDefault();
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
keys[e.key.toLowerCase()] = false;
|
||||
});
|
||||
|
||||
// Auto zeichnen
|
||||
function drawCar(car, isPlayer = false) {
|
||||
ctx.save();
|
||||
ctx.translate(car.x, car.y);
|
||||
ctx.rotate(car.angle);
|
||||
|
||||
// Drift-Rauch bei starkem Drift
|
||||
if (isPlayer && player.driftFactor > 0.5) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = player.driftFactor * 0.3;
|
||||
ctx.fillStyle = '#666';
|
||||
ctx.beginPath();
|
||||
ctx.arc(-car.width/2 - 5, 0, 8, 0, Math.PI * 2);
|
||||
ctx.arc(car.width/2 + 5, 0, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Auto Body
|
||||
ctx.fillStyle = car.color;
|
||||
ctx.fillRect(-car.width/2, -car.height/2, car.width, car.height);
|
||||
|
||||
// Windschutzscheibe
|
||||
ctx.fillStyle = 'rgba(100,100,100,0.8)';
|
||||
ctx.fillRect(-car.width/4, -car.height/3, car.width/2, car.height/3);
|
||||
|
||||
// Räder
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.fillRect(-car.width/2 - 2, -car.height/2 + 2, 4, 6);
|
||||
ctx.fillRect(-car.width/2 - 2, car.height/2 - 8, 4, 6);
|
||||
ctx.fillRect(car.width/2 - 2, -car.height/2 + 2, 4, 6);
|
||||
ctx.fillRect(car.width/2 - 2, car.height/2 - 8, 4, 6);
|
||||
|
||||
// Spieler-Indikator
|
||||
if (isPlayer) {
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, -car.height);
|
||||
ctx.lineTo(-5, -car.height - 10);
|
||||
ctx.lineTo(5, -car.height - 10);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
// Kreisförmige Strecke zeichnen
|
||||
function drawTrack() {
|
||||
// Hintergrund
|
||||
ctx.fillStyle = '#0a3d0a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Äußerer Kreis (Strecke)
|
||||
ctx.fillStyle = '#333';
|
||||
ctx.beginPath();
|
||||
ctx.arc(trackCenter.x, trackCenter.y, outerRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Innerer Kreis (Gras)
|
||||
ctx.fillStyle = '#0a3d0a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(trackCenter.x, trackCenter.y, innerRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Streckenbegrenzungen
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 4;
|
||||
ctx.setLineDash([20, 10]);
|
||||
|
||||
// Äußere Linie
|
||||
ctx.beginPath();
|
||||
ctx.arc(trackCenter.x, trackCenter.y, outerRadius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
// Innere Linie
|
||||
ctx.beginPath();
|
||||
ctx.arc(trackCenter.x, trackCenter.y, innerRadius, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.setLineDash([]);
|
||||
|
||||
// Start/Ziel Linie
|
||||
ctx.strokeStyle = '#fff';
|
||||
ctx.lineWidth = 8;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(trackCenter.x + innerRadius, trackCenter.y);
|
||||
ctx.lineTo(trackCenter.x + outerRadius, trackCenter.y);
|
||||
ctx.stroke();
|
||||
|
||||
// Schachbrettmuster auf Ziellinie
|
||||
const lineWidth = outerRadius - innerRadius;
|
||||
for (let i = 0; i < lineWidth / 10; i++) {
|
||||
for (let j = 0; j < 2; j++) {
|
||||
if ((i + j) % 2 === 0) {
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(trackCenter.x + innerRadius + i * 10, trackCenter.y - 4 + j * 4, 10, 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Driftspuren
|
||||
if (player.driftFactor > 0.3) {
|
||||
ctx.globalAlpha = player.driftFactor * 0.3;
|
||||
ctx.strokeStyle = '#000';
|
||||
ctx.lineWidth = 2;
|
||||
player.trail.forEach((point, i) => {
|
||||
if (i > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(player.trail[i-1].x, player.trail[i-1].y);
|
||||
ctx.lineTo(point.x, point.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Kollisionserkennung mit kreisförmiger Strecke
|
||||
function checkTrackCollision() {
|
||||
const dx = player.x - trackCenter.x;
|
||||
const dy = player.y - trackCenter.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Prüfe ob Auto außerhalb der Strecke
|
||||
if (distance > outerRadius - 15 || distance < innerRadius + 15) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Rundenzählung
|
||||
function checkLapCrossing() {
|
||||
// Prüfe ob Ziellinie überquert wurde
|
||||
const angle = Math.atan2(player.y - trackCenter.y, player.x - trackCenter.x);
|
||||
const normalizedAngle = angle < 0 ? angle + Math.PI * 2 : angle;
|
||||
|
||||
// Ziellinie ist bei 0° (rechts)
|
||||
if (normalizedAngle < 0.1 && player.lastAngle > 6.2) {
|
||||
if (!player.crossed) {
|
||||
player.crossed = true;
|
||||
player.lapCount++;
|
||||
|
||||
// Rundenzeit berechnen
|
||||
if (player.currentLapStart > 0) {
|
||||
const lapTime = currentTime - player.currentLapStart;
|
||||
if (lapTime < bestLapTime) {
|
||||
bestLapTime = lapTime;
|
||||
document.getElementById('bestLap').textContent = formatTime(bestLapTime);
|
||||
}
|
||||
}
|
||||
player.currentLapStart = currentTime;
|
||||
|
||||
document.getElementById('lap').textContent = player.lapCount;
|
||||
|
||||
// Effekt für neue Runde
|
||||
createParticles(player.x, player.y, '#00ff88');
|
||||
}
|
||||
} else if (normalizedAngle > 0.2) {
|
||||
player.crossed = false;
|
||||
}
|
||||
|
||||
player.lastAngle = normalizedAngle;
|
||||
}
|
||||
|
||||
// Hilfsfunktion für Linien-Kreuzung
|
||||
function lineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
|
||||
const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
||||
if (denom === 0) return false;
|
||||
|
||||
const t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
|
||||
const u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
|
||||
|
||||
return t >= 0 && t <= 1 && u >= 0 && u <= 1;
|
||||
}
|
||||
|
||||
// Spieler Update mit Drift-Physik
|
||||
function updatePlayer() {
|
||||
const oldAngle = player.angle;
|
||||
|
||||
// Steuerung
|
||||
if (keys['arrowup'] || keys['w']) {
|
||||
player.speed = Math.min(player.speed + player.acceleration, player.maxSpeed);
|
||||
} else if (keys['arrowdown'] || keys['s']) {
|
||||
player.speed = Math.max(player.speed - player.deceleration * 2, -player.maxSpeed / 2);
|
||||
} else {
|
||||
// Automatisches Abbremsen
|
||||
if (player.speed > 0) {
|
||||
player.speed = Math.max(0, player.speed - player.deceleration);
|
||||
} else {
|
||||
player.speed = Math.min(0, player.speed + player.deceleration);
|
||||
}
|
||||
}
|
||||
|
||||
// Lenken mit Drift
|
||||
let turnAmount = 0;
|
||||
if (Math.abs(player.speed) > 0.5) {
|
||||
if (keys['arrowleft'] || keys['a']) {
|
||||
turnAmount = -player.turnSpeed * (player.speed / player.maxSpeed);
|
||||
player.angle += turnAmount;
|
||||
}
|
||||
if (keys['arrowright'] || keys['d']) {
|
||||
turnAmount = player.turnSpeed * (player.speed / player.maxSpeed);
|
||||
player.angle += turnAmount;
|
||||
}
|
||||
}
|
||||
|
||||
// Drift-Berechnung
|
||||
const angleDiff = Math.abs(player.angle - oldAngle);
|
||||
if (angleDiff > 0.04 && player.speed > 2.5) { // Angepasst für langsamere Geschwindigkeit
|
||||
player.driftFactor = Math.min(1, player.driftFactor + 0.1);
|
||||
player.driftAngle = player.angle - Math.atan2(player.velocity.y, player.velocity.x);
|
||||
} else {
|
||||
player.driftFactor = Math.max(0, player.driftFactor - 0.05);
|
||||
}
|
||||
|
||||
// Velocity mit Drift
|
||||
const targetVx = Math.cos(player.angle) * player.speed;
|
||||
const targetVy = Math.sin(player.angle) * player.speed;
|
||||
|
||||
// Drift-Effekt: Velocity passt sich langsamer an die Richtung an
|
||||
const driftStrength = 0.15 + (1 - player.driftFactor) * 0.1;
|
||||
player.velocity.x += (targetVx - player.velocity.x) * driftStrength;
|
||||
player.velocity.y += (targetVy - player.velocity.y) * driftStrength;
|
||||
|
||||
// Boost
|
||||
if (keys[' '] && player.boost > 0 && player.speed > 2) {
|
||||
player.boosting = true;
|
||||
player.boost -= 2;
|
||||
player.speed = Math.min(player.speed + 0.2, player.maxSpeed * 1.4); // Angepasster Boost für langsamere Geschwindigkeit
|
||||
|
||||
// Boost Partikel
|
||||
createParticles(
|
||||
player.x - Math.cos(player.angle) * 20,
|
||||
player.y - Math.sin(player.angle) * 20,
|
||||
'#00ffff'
|
||||
);
|
||||
} else {
|
||||
player.boosting = false;
|
||||
// Boost regeneriert langsam
|
||||
player.boost = Math.min(100, player.boost + 0.3);
|
||||
}
|
||||
|
||||
// Position update mit Velocity
|
||||
const oldX = player.x;
|
||||
const oldY = player.y;
|
||||
player.x += player.velocity.x;
|
||||
player.y += player.velocity.y;
|
||||
|
||||
// Kollision prüfen mit Abprall-Effekt
|
||||
if (checkTrackCollision()) {
|
||||
// Zurück zur alten Position
|
||||
player.x = oldX;
|
||||
player.y = oldY;
|
||||
|
||||
// Berechne Abprall-Richtung vom Streckenzentrum
|
||||
const dx = player.x - trackCenter.x;
|
||||
const dy = player.y - trackCenter.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
// Normalisiere und kehre Richtung um
|
||||
let bounceX = dx / distance;
|
||||
let bounceY = dy / distance;
|
||||
|
||||
// Wenn zu nah am Inneren, kehre um
|
||||
if (distance < innerRadius + 15) {
|
||||
bounceX = -bounceX;
|
||||
bounceY = -bounceY;
|
||||
}
|
||||
|
||||
// Wende Abprall-Kraft an
|
||||
player.velocity.x = bounceX * player.speed * 0.6;
|
||||
player.velocity.y = bounceY * player.speed * 0.6;
|
||||
player.speed *= 0.5;
|
||||
|
||||
// Leichte Drehung beim Aufprall
|
||||
player.angle += (Math.random() - 0.5) * 0.3;
|
||||
|
||||
// Kollisions-Partikel
|
||||
createParticles(player.x, player.y, '#ff0066');
|
||||
}
|
||||
|
||||
// Trail für Driftspuren
|
||||
if (player.driftFactor > 0.3 && player.speed > 3) {
|
||||
player.trail.push({x: player.x, y: player.y, life: 30});
|
||||
if (player.trail.length > 100) {
|
||||
player.trail.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// Rundenzählung
|
||||
checkLapCrossing();
|
||||
|
||||
// UI Update
|
||||
document.getElementById('speed').textContent = Math.floor(Math.abs(player.speed) * 30); // Angepasst für noch langsamere Geschwindigkeit
|
||||
document.getElementById('boostFill').style.width = player.boost + '%';
|
||||
|
||||
// Drift-Anzeige
|
||||
if (player.driftFactor > 0.5) {
|
||||
createParticles(player.x - 10, player.y - 10, '#ffcc00');
|
||||
}
|
||||
}
|
||||
|
||||
// Zeit formatieren
|
||||
function formatTime(ms) {
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
const milliseconds = Math.floor((ms % 1000) / 10);
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// Partikel erstellen
|
||||
function createParticles(x, y, color) {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
particles.push({
|
||||
x: x,
|
||||
y: y,
|
||||
vx: (Math.random() - 0.5) * 4,
|
||||
vy: (Math.random() - 0.5) * 4,
|
||||
life: 20,
|
||||
color: color
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Partikel update
|
||||
function updateParticles() {
|
||||
particles.forEach(p => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life--;
|
||||
p.vx *= 0.95;
|
||||
p.vy *= 0.95;
|
||||
});
|
||||
|
||||
// Alte Partikel entfernen
|
||||
for (let i = particles.length - 1; i >= 0; i--) {
|
||||
if (particles[i].life <= 0) {
|
||||
particles.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Partikel zeichnen
|
||||
function drawParticles() {
|
||||
particles.forEach(p => {
|
||||
ctx.globalAlpha = p.life / 20;
|
||||
ctx.fillStyle = p.color;
|
||||
ctx.fillRect(p.x - 2, p.y - 2, 4, 4);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Sterne für Geschwindigkeitseffekt
|
||||
function drawStars() {
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.3)';
|
||||
stars.forEach(star => {
|
||||
// Bewege Sterne basierend auf Spielergeschwindigkeit
|
||||
star.y += player.speed * 0.5;
|
||||
if (star.y > canvas.height) {
|
||||
star.y = 0;
|
||||
star.x = Math.random() * canvas.width;
|
||||
}
|
||||
|
||||
ctx.globalAlpha = player.speed / player.maxSpeed * 0.5;
|
||||
ctx.fillRect(star.x, star.y, star.size, star.size * 3);
|
||||
});
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Zeit-Update
|
||||
function updateTime() {
|
||||
if (gameRunning) {
|
||||
currentTime += 16; // ~60 FPS
|
||||
document.getElementById('time').textContent = formatTime(currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
// Game Loop
|
||||
function gameLoop() {
|
||||
if (!gameRunning) return;
|
||||
|
||||
// Clear
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Draw
|
||||
drawStars();
|
||||
drawTrack();
|
||||
|
||||
// Update
|
||||
updatePlayer();
|
||||
updateParticles();
|
||||
updateTime();
|
||||
|
||||
// Draw car
|
||||
drawCar(player, true);
|
||||
|
||||
// Effects
|
||||
drawParticles();
|
||||
|
||||
requestAnimationFrame(gameLoop);
|
||||
}
|
||||
|
||||
// Spiel starten
|
||||
function startGame() {
|
||||
document.getElementById('startScreen').style.display = 'none';
|
||||
gameRunning = true;
|
||||
currentTime = 0;
|
||||
|
||||
// Reset Spieler
|
||||
player.x = trackCenter.x + (innerRadius + outerRadius) / 2;
|
||||
player.y = trackCenter.y;
|
||||
player.angle = -Math.PI / 2; // Nach oben zeigend
|
||||
player.velocity = { x: 0, y: 0 };
|
||||
player.speed = 0;
|
||||
player.boost = 100;
|
||||
player.lapCount = 0;
|
||||
player.currentLapStart = 0;
|
||||
player.bestLapTime = null;
|
||||
player.trail = [];
|
||||
player.driftFactor = 0;
|
||||
player.lastAngle = 0;
|
||||
player.crossed = false;
|
||||
|
||||
bestLapTime = Infinity;
|
||||
document.getElementById('lap').textContent = '0';
|
||||
document.getElementById('bestLap').textContent = '--:--';
|
||||
|
||||
// Event senden
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_LOADED',
|
||||
gameId: GAME_ID
|
||||
}, '*');
|
||||
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
// Rennen beenden (optional - könnte nach X Runden aufgerufen werden)
|
||||
function endRace() {
|
||||
gameRunning = false;
|
||||
|
||||
document.getElementById('gameOverTitle').textContent = '🏆 ZEIT-HERAUSFORDERUNG BEENDET!';
|
||||
document.getElementById('finalTime').textContent = `${player.lapCount} Runden`;
|
||||
document.getElementById('finalPosition').textContent =
|
||||
`Beste Runde: ${bestLapTime === Infinity ? '--:--' : formatTime(bestLapTime)}`;
|
||||
document.getElementById('gameOver').style.display = 'block';
|
||||
|
||||
// Score basierend auf bester Rundenzeit
|
||||
const score = bestLapTime === Infinity ? 0 : Math.max(0, 50000 - bestLapTime);
|
||||
|
||||
// Events senden
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'GAME_OVER',
|
||||
data: {
|
||||
score: Math.floor(score),
|
||||
laps: player.lapCount,
|
||||
bestLap: bestLapTime
|
||||
}
|
||||
}, '*');
|
||||
|
||||
// Achievements
|
||||
if (bestLapTime < 15000) { // Unter 15 Sekunden
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'drift_master',
|
||||
name: 'Drift Master',
|
||||
description: 'Schaffe eine Runde unter 15 Sekunden',
|
||||
icon: '🏎️'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
|
||||
if (player.lapCount >= 10) {
|
||||
window.parent.postMessage({
|
||||
type: 'GAME_EVENT',
|
||||
gameId: GAME_ID,
|
||||
event: 'ACHIEVEMENT_UNLOCKED',
|
||||
data: {
|
||||
achievementId: 'endurance_racer',
|
||||
name: 'Ausdauer-Rennfahrer',
|
||||
description: 'Fahre 10 Runden in einer Session',
|
||||
icon: '🎯'
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
|
||||
// Neustart
|
||||
function restartGame() {
|
||||
document.getElementById('gameOver').style.display = 'none';
|
||||
startGame();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1274
games/mana-games/apps/web/public/games/word_scramble.html
Normal file
BIN
games/mana-games/apps/web/public/icons/icon-120x120.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-128x128.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-144x144.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-152x152.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-167x167.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-180x180.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-192x192.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-384x384.png
Normal file
|
After Width: | Height: | Size: 8.9 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-512x512.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-72x72.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
games/mana-games/apps/web/public/icons/icon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
10
games/mana-games/apps/web/public/icons/icon-base.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="#0a0a0a"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#00ff88;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#00cc6a;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<text x="256" y="280" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif" font-size="200" font-weight="900" text-anchor="middle" fill="url(#gradient)">MG</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 606 B |
89
games/mana-games/apps/web/public/manifest.json
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"name": "Mana Games - Spiele ohne Grenzen",
|
||||
"short_name": "Mana Games",
|
||||
"description": "Eine Sammlung kostenloser, werbefreier Web-Spiele zum Spielen, Bauen und Lernen",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"theme_color": "#1a1a1a",
|
||||
"background_color": "#0a0a0a",
|
||||
"categories": ["games", "education", "entertainment"],
|
||||
"lang": "de",
|
||||
"dir": "ltr",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "/screenshots/desktop-home.png",
|
||||
"sizes": "1280x720",
|
||||
"type": "image/png",
|
||||
"label": "Mana Games Startseite"
|
||||
},
|
||||
{
|
||||
"src": "/screenshots/mobile-home.png",
|
||||
"sizes": "750x1334",
|
||||
"type": "image/png",
|
||||
"label": "Mobile Ansicht"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Snake Game",
|
||||
"url": "/games/snake",
|
||||
"description": "Klassisches Snake-Spiel spielen"
|
||||
},
|
||||
{
|
||||
"name": "Meine Statistiken",
|
||||
"url": "/stats",
|
||||
"description": "Spielstatistiken anzeigen"
|
||||
}
|
||||
]
|
||||
}
|
||||
184
games/mana-games/apps/web/public/offline.html
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<title>Offline - Mana Games</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-bg: #0a0a0a;
|
||||
--color-bg-secondary: #1a1a1a;
|
||||
--color-text: #ffffff;
|
||||
--color-text-secondary: #b0b0b0;
|
||||
--color-accent: #00ff88;
|
||||
--color-accent-secondary: #00cc6a;
|
||||
--color-border: #2a2a2a;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.offline-container {
|
||||
text-align: center;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.offline-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1.5rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.accent {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.offline-games {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.offline-games h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.games-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.game-item {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.game-item h3 {
|
||||
color: var(--color-text);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.game-item p {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.reload-button {
|
||||
background-color: var(--color-accent);
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.reload-button:hover {
|
||||
background-color: var(--color-accent-secondary);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.offline-icon {
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="offline-container">
|
||||
<div class="offline-icon">📡</div>
|
||||
<h1>Du bist <span class="accent">offline</span></h1>
|
||||
<p>
|
||||
Keine Internetverbindung gefunden. Aber keine Sorge!
|
||||
Einige Spiele, die du bereits gespielt hast, sind möglicherweise
|
||||
noch im Cache verfügbar.
|
||||
</p>
|
||||
<button class="reload-button" onclick="window.location.reload()">
|
||||
Erneut versuchen
|
||||
</button>
|
||||
|
||||
<div class="offline-games" id="offline-games" style="display: none;">
|
||||
<h2>Verfügbare Offline-Spiele</h2>
|
||||
<div class="games-list" id="games-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Prüfe gecachte Spiele
|
||||
if ('caches' in window) {
|
||||
caches.open('mana-games-v1').then(cache => {
|
||||
cache.keys().then(requests => {
|
||||
const gameUrls = requests
|
||||
.map(request => request.url)
|
||||
.filter(url => url.includes('/games/') && url.endsWith('.html'));
|
||||
|
||||
if (gameUrls.length > 0) {
|
||||
document.getElementById('offline-games').style.display = 'block';
|
||||
const gamesList = document.getElementById('games-list');
|
||||
|
||||
gameUrls.forEach(url => {
|
||||
const gameName = url.split('/').pop().replace('.html', '').replace(/_/g, ' ');
|
||||
const gameItem = document.createElement('div');
|
||||
gameItem.className = 'game-item';
|
||||
gameItem.innerHTML = `
|
||||
<h3>${gameName}</h3>
|
||||
<p>Dieses Spiel ist offline verfügbar</p>
|
||||
`;
|
||||
gameItem.style.cursor = 'pointer';
|
||||
gameItem.onclick = () => window.location.href = url;
|
||||
gamesList.appendChild(gameItem);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Automatisch neu laden, wenn wieder online
|
||||
window.addEventListener('online', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
games/mana-games/apps/web/public/screenshots/asteroid-dash.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
games/mana-games/apps/web/public/screenshots/balloon-pop.jpg
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
games/mana-games/apps/web/public/screenshots/bounce-catch.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
games/mana-games/apps/web/public/screenshots/click-race.jpg
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
games/mana-games/apps/web/public/screenshots/color-memory.jpg
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
games/mana-games/apps/web/public/screenshots/fish-catcher.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
games/mana-games/apps/web/public/screenshots/gravity-painter.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 67 KiB |
BIN
games/mana-games/apps/web/public/screenshots/reaction-test.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
games/mana-games/apps/web/public/screenshots/rhythm-defender.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
games/mana-games/apps/web/public/screenshots/snake.jpg
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
games/mana-games/apps/web/public/screenshots/space-defenders.jpg
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-1125x2436.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-1242x2688.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-1536x2048.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-1668x2224.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-2048x2732.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-640x1136.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-750x1334.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
games/mana-games/apps/web/public/splash/splash-828x1792.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
170
games/mana-games/apps/web/public/sw.js
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
const CACHE_NAME = 'mana-games-v1';
|
||||
const OFFLINE_URL = '/offline.html';
|
||||
|
||||
// Assets, die immer gecacht werden sollen
|
||||
const STATIC_CACHE_URLS = [
|
||||
'/',
|
||||
'/offline.html',
|
||||
'/favicon.svg',
|
||||
'/manifest.json'
|
||||
];
|
||||
|
||||
// Cache-Strategien für verschiedene Ressourcen
|
||||
const CACHE_STRATEGIES = {
|
||||
// Netzwerk zuerst, dann Cache (für HTML)
|
||||
networkFirst: [
|
||||
/\/$/,
|
||||
/\.html$/,
|
||||
/\.astro$/
|
||||
],
|
||||
// Cache zuerst, dann Netzwerk (für Assets)
|
||||
cacheFirst: [
|
||||
/\.css$/,
|
||||
/\.js$/,
|
||||
/\.woff2?$/,
|
||||
/\.ttf$/,
|
||||
/\.otf$/,
|
||||
/\.svg$/,
|
||||
/\.png$/,
|
||||
/\.jpg$/,
|
||||
/\.jpeg$/,
|
||||
/\.webp$/,
|
||||
/\.ico$/
|
||||
],
|
||||
// Nur Netzwerk (für API-Calls)
|
||||
networkOnly: [
|
||||
/\/api\//,
|
||||
/\.json$/
|
||||
]
|
||||
};
|
||||
|
||||
// Service Worker Installation
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => {
|
||||
console.log('Service Worker: Caching static assets');
|
||||
return cache.addAll(STATIC_CACHE_URLS);
|
||||
})
|
||||
.then(() => self.skipWaiting())
|
||||
);
|
||||
});
|
||||
|
||||
// Service Worker Aktivierung
|
||||
self.addEventListener('activate', event => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then(cacheNames => {
|
||||
return Promise.all(
|
||||
cacheNames
|
||||
.filter(cacheName => cacheName !== CACHE_NAME)
|
||||
.map(cacheName => caches.delete(cacheName))
|
||||
);
|
||||
})
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
// Fetch-Event Handler
|
||||
self.addEventListener('fetch', event => {
|
||||
const { request } = event;
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Ignoriere Chrome Extension Requests
|
||||
if (url.protocol === 'chrome-extension:') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bestimme die Cache-Strategie
|
||||
const strategy = getStrategy(url.pathname);
|
||||
|
||||
if (strategy === 'networkFirst') {
|
||||
event.respondWith(networkFirst(request));
|
||||
} else if (strategy === 'cacheFirst') {
|
||||
event.respondWith(cacheFirst(request));
|
||||
} else if (strategy === 'networkOnly') {
|
||||
event.respondWith(networkOnly(request));
|
||||
} else {
|
||||
// Standard: Network First
|
||||
event.respondWith(networkFirst(request));
|
||||
}
|
||||
});
|
||||
|
||||
// Cache-Strategien Implementierung
|
||||
async function networkFirst(request) {
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Wenn es eine Navigation ist und wir offline sind, zeige die Offline-Seite
|
||||
if (request.mode === 'navigate') {
|
||||
const offlineResponse = await caches.match(OFFLINE_URL);
|
||||
if (offlineResponse) {
|
||||
return offlineResponse;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
const networkResponse = await fetch(request);
|
||||
if (networkResponse.ok) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
cache.put(request, networkResponse.clone());
|
||||
}
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
console.error('Fetch failed:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function networkOnly(request) {
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
// Hilfsfunktion zur Bestimmung der Cache-Strategie
|
||||
function getStrategy(pathname) {
|
||||
for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) {
|
||||
if (patterns.some(pattern => pattern.test(pathname))) {
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
return 'networkFirst';
|
||||
}
|
||||
|
||||
// Message Handler für Cache-Updates
|
||||
self.addEventListener('message', event => {
|
||||
if (event.data && event.data.type === 'SKIP_WAITING') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
|
||||
if (event.data && event.data.type === 'CACHE_GAME') {
|
||||
const gameUrl = event.data.url;
|
||||
caches.open(CACHE_NAME)
|
||||
.then(cache => cache.add(gameUrl))
|
||||
.then(() => {
|
||||
event.ports[0].postMessage({ cached: true });
|
||||
})
|
||||
.catch(error => {
|
||||
event.ports[0].postMessage({ cached: false, error: error.message });
|
||||
});
|
||||
}
|
||||
});
|
||||
191
games/mana-games/apps/web/src/components/Button.astro
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
---
|
||||
export interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'accent' | 'ghost' | 'danger';
|
||||
size?: 'small' | 'medium' | 'large' | 'icon';
|
||||
href?: string;
|
||||
onclick?: string;
|
||||
id?: string;
|
||||
class?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
type?: 'button' | 'submit' | 'reset';
|
||||
}
|
||||
|
||||
const {
|
||||
variant = 'secondary',
|
||||
size = 'medium',
|
||||
href,
|
||||
onclick,
|
||||
id,
|
||||
class: className = '',
|
||||
title,
|
||||
disabled = false,
|
||||
type = 'button'
|
||||
} = Astro.props;
|
||||
|
||||
const isLink = Boolean(href);
|
||||
const Component = isLink ? 'a' : 'button';
|
||||
|
||||
const classes = [
|
||||
'btn',
|
||||
`btn-${variant}`,
|
||||
`btn-${size}`,
|
||||
className
|
||||
].filter(Boolean).join(' ');
|
||||
|
||||
const props = {
|
||||
class: classes,
|
||||
...(id && { id }),
|
||||
...(title && { title }),
|
||||
...(isLink ? { href } : { type, disabled }),
|
||||
...(onclick && { onclick })
|
||||
};
|
||||
---
|
||||
|
||||
<Component {...props}>
|
||||
<slot />
|
||||
</Component>
|
||||
|
||||
<style>
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
font-family: inherit;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.btn-small {
|
||||
padding: 0.375rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-medium {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* Variants */
|
||||
.btn-primary {
|
||||
background-color: var(--color-accent);
|
||||
color: #000;
|
||||
border: 1px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--color-accent-secondary);
|
||||
border-color: var(--color-accent-secondary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.btn-primary:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: var(--color-bg-secondary);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: #252525;
|
||||
border-color: #404040;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background-color: rgba(0, 255, 136, 0.1);
|
||||
color: var(--color-accent);
|
||||
border: 1px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.btn-accent:hover:not(:disabled) {
|
||||
background-color: var(--color-accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-ghost:hover:not(:disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
color: var(--color-text);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.btn-danger:hover:not(:disabled) {
|
||||
background-color: #ff4444;
|
||||
color: #fff;
|
||||
border-color: #ff4444;
|
||||
}
|
||||
|
||||
/* Special hover effects */
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
/* Mobile optimizations */
|
||||
@media (max-width: 768px) {
|
||||
.btn-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
189
games/mana-games/apps/web/src/components/Footer.astro
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
---
|
||||
// Footer component with compact site navigation
|
||||
---
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-content">
|
||||
<!-- Brand -->
|
||||
<div class="footer-brand">
|
||||
<a href="/" class="footer-logo">
|
||||
<span class="logo-text">MANA</span>
|
||||
<span class="logo-accent">GAMES</span>
|
||||
</a>
|
||||
<p class="footer-tagline">Spiele ohne Grenzen</p>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div class="footer-nav">
|
||||
<div class="footer-section">
|
||||
<h4>Spielen</h4>
|
||||
<ul>
|
||||
<li><a href="/">Alle Spiele</a></li>
|
||||
<li><a href="/create">KI Generator</a></li>
|
||||
<li><a href="/stats">Meine Stats</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h4>Über Uns</h4>
|
||||
<ul>
|
||||
<li><a href="/about">Vision</a></li>
|
||||
<li><a href="/mitmachen">Mitmachen</a></li>
|
||||
<li><a href="https://github.com/anthropics/mana-games" target="_blank" rel="noopener">GitHub</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-section">
|
||||
<h4>Rechtliches</h4>
|
||||
<ul>
|
||||
<li><a href="/impressum">Impressum</a></li>
|
||||
<li><a href="/datenschutz">Datenschutz</a></li>
|
||||
<li><a href="/agb">AGB</a></li>
|
||||
<li><a href="/jugendschutz">Jugendschutz</a></li>
|
||||
<li><a href="/copyright">Copyright</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div class="footer-bottom">
|
||||
<p>© 2024 Mana Games. Alle Rechte vorbehalten.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.site-footer {
|
||||
background: var(--color-bg-secondary);
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin-top: 4rem;
|
||||
padding: 3rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 4rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Brand Section */
|
||||
.footer-brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.05em;
|
||||
display: inline-block;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.footer-logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.logo-accent {
|
||||
color: var(--color-accent);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.footer-tagline {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.footer-nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-section h4 {
|
||||
color: var(--color-text);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.footer-section ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.footer-section a {
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.875rem;
|
||||
transition: color 0.2s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.footer-section a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Bottom Bar */
|
||||
.footer-bottom {
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-bottom p {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.75rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.site-footer {
|
||||
margin-top: 3rem;
|
||||
padding: 2rem 0 1rem;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Full width pages adjustment */
|
||||
body.full-width .site-footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
305
games/mana-games/apps/web/src/components/GameCard.astro
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
---
|
||||
import GameStats from './GameStats.astro';
|
||||
import Button from './Button.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
thumbnail?: string;
|
||||
tags?: string[];
|
||||
complexity?: 'Minimal' | 'Einfach' | 'Mittel' | 'Komplex';
|
||||
codeStats?: {
|
||||
total: number;
|
||||
code: number;
|
||||
comments: number;
|
||||
};
|
||||
}
|
||||
|
||||
const { title, description, slug, thumbnail, tags = [], complexity, codeStats } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="game-card">
|
||||
<a href={`/games/${slug}`} class="card-link">
|
||||
<div class="card-image">
|
||||
{thumbnail ? (
|
||||
<img src={thumbnail} alt={title} />
|
||||
) : (
|
||||
<div class="placeholder">
|
||||
<span>{title.charAt(0)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">{title}</h3>
|
||||
<p class="card-description">{description}</p>
|
||||
|
||||
<div class="card-meta">
|
||||
{complexity && (
|
||||
<span class={`complexity complexity-${complexity.toLowerCase()}`}>
|
||||
{complexity}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{tags.length > 0 && (
|
||||
<div class="card-tags">
|
||||
{tags.map((tag) => (
|
||||
<span class="tag">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{codeStats && (
|
||||
<div class="code-info">
|
||||
<span class="code-lines">
|
||||
{codeStats.total} Zeilen
|
||||
<span class="code-detail">({codeStats.code} Code / {codeStats.comments} Kommentare)</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<GameStats gameId={slug} />
|
||||
</div>
|
||||
|
||||
<div class="hover-buttons">
|
||||
<Button
|
||||
href={`/games/${slug}/playground`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
class="code-btn"
|
||||
onclick="event.stopPropagation(); event.preventDefault(); window.location.href=this.href;"
|
||||
>
|
||||
Code
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
class="play-btn"
|
||||
>
|
||||
Spielen
|
||||
</Button>
|
||||
</div>
|
||||
</a>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* Basis Card Styling */
|
||||
.game-card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.game-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
/* Link Container */
|
||||
.card-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Bild/Placeholder Section */
|
||||
.card-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
background: #0a0a0a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a1a, #0a0a0a);
|
||||
color: var(--color-accent);
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
text-shadow: 0 0 30px rgba(0, 255, 136, 0.5);
|
||||
}
|
||||
|
||||
/* Content Section */
|
||||
.card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
margin: 0 0 0.75rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.card-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20px;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Hover Buttons */
|
||||
.hover-buttons {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.game-card:hover .hover-buttons {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.hover-buttons :global(.play-btn) {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.hover-buttons :global(.code-btn) {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 1rem;
|
||||
border-radius: 20px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.card-image {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code Info */
|
||||
.code-info {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.75rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.code-lines {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-accent);
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.code-lines::before {
|
||||
content: '< >';
|
||||
font-family: monospace;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.code-detail {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: normal;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
/* Card Meta Section */
|
||||
.card-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Complexity Badge */
|
||||
.complexity {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
border-radius: 12px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.complexity-minimal {
|
||||
background: #4ade80;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.complexity-einfach {
|
||||
background: #60a5fa;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.complexity-mittel {
|
||||
background: #fbbf24;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.complexity-komplex {
|
||||
background: #f87171;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Fallback für fehlende Bilder
|
||||
const images = document.querySelectorAll('.card-image img');
|
||||
images.forEach(img => {
|
||||
img.addEventListener('error', function() {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'placeholder';
|
||||
placeholder.innerHTML = `<span>${this.alt.charAt(0)}</span>`;
|
||||
this.parentElement.replaceChild(placeholder, this);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
144
games/mana-games/apps/web/src/components/GameStats.astro
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
import { statsService } from '../services/statsService';
|
||||
|
||||
export interface Props {
|
||||
gameId: string;
|
||||
showDetails?: boolean;
|
||||
}
|
||||
|
||||
const { gameId, showDetails = false } = Astro.props;
|
||||
const stats = statsService.getStats(gameId);
|
||||
---
|
||||
|
||||
{stats && (
|
||||
<div class="game-stats">
|
||||
<div class="stats-row">
|
||||
{stats.highScore > 0 && (
|
||||
<div class="stat-item highscore">
|
||||
<span class="stat-icon">🏆</span>
|
||||
<span class="stat-value">{stats.highScore.toLocaleString('de-DE')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.gamesPlayed > 0 && (
|
||||
<div class="stat-item games-played">
|
||||
<span class="stat-icon">🎮</span>
|
||||
<span class="stat-value">{stats.gamesPlayed}x</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats.totalPlayTime > 0 && (
|
||||
<div class="stat-item play-time">
|
||||
<span class="stat-icon">⏱️</span>
|
||||
<span class="stat-value">{statsService.formatPlayTime(stats.totalPlayTime)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDetails && stats.lastPlayed && (
|
||||
<div class="last-played">
|
||||
Zuletzt gespielt: {statsService.getRelativeTime(stats.lastPlayed)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDetails && stats.achievements && stats.achievements.length > 0 && (
|
||||
<div class="achievements">
|
||||
<h4>Achievements</h4>
|
||||
<div class="achievement-list">
|
||||
{stats.achievements.map(achievement => (
|
||||
<div class="achievement" title={achievement.description}>
|
||||
<span class="achievement-icon">🏅</span>
|
||||
<span class="achievement-name">{achievement.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>
|
||||
.game-stats {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.highscore .stat-value {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.games-played .stat-value {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.play-time .stat-value {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.last-played {
|
||||
margin-top: 0.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.achievements {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.achievements h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.achievement-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.achievement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: rgba(255, 215, 0, 0.1);
|
||||
border: 1px solid rgba(255, 215, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.achievement-icon {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.achievement-name {
|
||||
color: #fbbf24;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,314 @@
|
|||
---
|
||||
import GameCard from './GameCard.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
games: any[];
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const { title, games, id = 'scroller' } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="scroller-section">
|
||||
<div class="scroller-header">
|
||||
<h2>{title}</h2>
|
||||
<div class="scroller-controls">
|
||||
<button class="scroll-btn scroll-left" data-scroller={id} aria-label="Nach links scrollen">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="scroll-btn scroll-right" data-scroller={id} aria-label="Nach rechts scrollen">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="scroller-container">
|
||||
<div class="scroller-gradient-left"></div>
|
||||
<div class="scroller-gradient-right"></div>
|
||||
|
||||
<div class="scroller-track" id={id}>
|
||||
<div class="scroller-content">
|
||||
{games.map((game) => (
|
||||
<div class="scroller-item">
|
||||
<GameCard
|
||||
title={game.title}
|
||||
description={game.description}
|
||||
slug={game.slug}
|
||||
thumbnail={game.thumbnail}
|
||||
tags={game.tags}
|
||||
complexity={game.complexity}
|
||||
codeStats={game.codeStats}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.scroller-section {
|
||||
position: relative;
|
||||
margin-bottom: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scroller-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 0 max(1.5rem, calc((100vw - 1400px) / 2));
|
||||
}
|
||||
|
||||
.scroller-header h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scroller-controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.scroll-btn:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
opacity: 1;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.scroll-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.scroller-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroller-gradient-left,
|
||||
.scroller-gradient-right {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 100px;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.scroller-gradient-left {
|
||||
left: 0;
|
||||
background: linear-gradient(90deg, var(--color-bg) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.scroller-gradient-right {
|
||||
right: 0;
|
||||
background: linear-gradient(270deg, var(--color-bg) 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.scroller-container.has-scroll-left .scroller-gradient-left,
|
||||
.scroller-container.has-scroll-right .scroller-gradient-right {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scroller-track {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: 0.5rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.scroller-track::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.scroller-content {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 0 max(1.5rem, calc((100vw - 1400px) / 2));
|
||||
min-width: min-content;
|
||||
}
|
||||
|
||||
.scroller-item {
|
||||
flex: 0 0 320px;
|
||||
max-width: 320px;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: scrollerItemFadeIn 0.4s ease forwards;
|
||||
}
|
||||
|
||||
.scroller-item:nth-child(1) { animation-delay: 0s; }
|
||||
.scroller-item:nth-child(2) { animation-delay: 0.05s; }
|
||||
.scroller-item:nth-child(3) { animation-delay: 0.1s; }
|
||||
.scroller-item:nth-child(4) { animation-delay: 0.15s; }
|
||||
.scroller-item:nth-child(5) { animation-delay: 0.2s; }
|
||||
.scroller-item:nth-child(6) { animation-delay: 0.25s; }
|
||||
.scroller-item:nth-child(n+7) { animation-delay: 0.3s; }
|
||||
|
||||
@keyframes scrollerItemFadeIn {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.scroller-item {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.scroller-item:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.scroller-header {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.scroller-content {
|
||||
padding: 0 1rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.scroller-item {
|
||||
flex: 0 0 280px;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.scroller-gradient-left,
|
||||
.scroller-gradient-right {
|
||||
width: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.scroller-item {
|
||||
flex: 0 0 240px;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.scroller-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const scrollers = document.querySelectorAll('.scroller-track');
|
||||
|
||||
scrollers.forEach(scroller => {
|
||||
const scrollerId = scroller.id;
|
||||
const container = scroller.closest('.scroller-container');
|
||||
const leftBtn = document.querySelector(`.scroll-left[data-scroller="${scrollerId}"]`) as HTMLButtonElement;
|
||||
const rightBtn = document.querySelector(`.scroll-right[data-scroller="${scrollerId}"]`) as HTMLButtonElement;
|
||||
|
||||
if (!container || !leftBtn || !rightBtn) return;
|
||||
|
||||
const updateButtons = () => {
|
||||
const scrollLeft = scroller.scrollLeft;
|
||||
const scrollWidth = scroller.scrollWidth;
|
||||
const clientWidth = scroller.clientWidth;
|
||||
|
||||
leftBtn.disabled = scrollLeft <= 0;
|
||||
rightBtn.disabled = scrollLeft >= scrollWidth - clientWidth - 1;
|
||||
|
||||
if (scrollLeft > 0) {
|
||||
container.classList.add('has-scroll-left');
|
||||
} else {
|
||||
container.classList.remove('has-scroll-left');
|
||||
}
|
||||
|
||||
if (scrollLeft < scrollWidth - clientWidth - 1) {
|
||||
container.classList.add('has-scroll-right');
|
||||
} else {
|
||||
container.classList.remove('has-scroll-right');
|
||||
}
|
||||
};
|
||||
|
||||
const scrollAmount = () => {
|
||||
const item = scroller.querySelector('.scroller-item') as HTMLElement;
|
||||
if (!item) return 320;
|
||||
return item.offsetWidth + 24;
|
||||
};
|
||||
|
||||
leftBtn.addEventListener('click', () => {
|
||||
scroller.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
|
||||
});
|
||||
|
||||
rightBtn.addEventListener('click', () => {
|
||||
scroller.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
|
||||
});
|
||||
|
||||
scroller.addEventListener('scroll', updateButtons);
|
||||
window.addEventListener('resize', updateButtons);
|
||||
|
||||
setTimeout(updateButtons, 100);
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
let isSwiping = false;
|
||||
|
||||
scroller.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
isSwiping = true;
|
||||
}, { passive: true });
|
||||
|
||||
scroller.addEventListener('touchmove', (e) => {
|
||||
if (!isSwiping) return;
|
||||
touchEndX = e.touches[0].clientX;
|
||||
}, { passive: true });
|
||||
|
||||
scroller.addEventListener('touchend', () => {
|
||||
if (!isSwiping) return;
|
||||
isSwiping = false;
|
||||
|
||||
const swipeDistance = touchEndX - touchStartX;
|
||||
const threshold = 50;
|
||||
|
||||
if (Math.abs(swipeDistance) > threshold) {
|
||||
if (swipeDistance > 0) {
|
||||
scroller.scrollBy({ left: -scrollAmount(), behavior: 'smooth' });
|
||||
} else {
|
||||
scroller.scrollBy({ left: scrollAmount(), behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
183
games/mana-games/apps/web/src/components/InstallPrompt.astro
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---
|
||||
// Keine Props benötigt
|
||||
---
|
||||
|
||||
<div id="install-prompt" class="install-prompt hidden">
|
||||
<div class="prompt-content">
|
||||
<div class="prompt-icon">📱</div>
|
||||
<div class="prompt-text">
|
||||
<h3>App installieren</h3>
|
||||
<p>Installiere Mana Games für schnelleren Zugriff!</p>
|
||||
</div>
|
||||
<div class="prompt-actions">
|
||||
<button id="install-button" class="install-btn">Installieren</button>
|
||||
<button id="dismiss-button" class="dismiss-btn">Später</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.install-prompt {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
width: calc(100% - 2rem);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.install-prompt.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.prompt-icon {
|
||||
font-size: 2.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.prompt-text h3 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-size: 1.1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.prompt-text p {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.prompt-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.install-btn,
|
||||
.dismiss-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.install-btn {
|
||||
background-color: var(--color-accent);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.install-btn:hover {
|
||||
background-color: var(--color-accent-secondary);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dismiss-btn {
|
||||
background-color: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.dismiss-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.install-prompt {
|
||||
bottom: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.prompt-content {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.prompt-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.prompt-actions {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.install-btn,
|
||||
.dismiss-btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
let deferredPrompt: any;
|
||||
const installPrompt = document.getElementById('install-prompt');
|
||||
const installButton = document.getElementById('install-button');
|
||||
const dismissButton = document.getElementById('dismiss-button');
|
||||
|
||||
// Prüfe ob App bereits installiert ist
|
||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
||||
// App ist bereits installiert
|
||||
} else {
|
||||
// Zeige Prompt nach 30 Sekunden oder 3 Seitenaufrufen
|
||||
const promptShown = localStorage.getItem('install-prompt-shown');
|
||||
const pageViews = parseInt(localStorage.getItem('page-views') || '0') + 1;
|
||||
localStorage.setItem('page-views', pageViews.toString());
|
||||
|
||||
if (!promptShown && pageViews >= 3) {
|
||||
setTimeout(() => {
|
||||
if (installPrompt && deferredPrompt) {
|
||||
installPrompt.classList.remove('hidden');
|
||||
}
|
||||
}, 30000);
|
||||
}
|
||||
}
|
||||
|
||||
// Installationsprompt abfangen
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
});
|
||||
|
||||
// Install Button Handler
|
||||
installButton?.addEventListener('click', async () => {
|
||||
if (!deferredPrompt) return;
|
||||
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
|
||||
if (outcome === 'accepted') {
|
||||
console.log('PWA wurde installiert');
|
||||
}
|
||||
|
||||
deferredPrompt = null;
|
||||
installPrompt?.classList.add('hidden');
|
||||
localStorage.setItem('install-prompt-shown', 'true');
|
||||
});
|
||||
|
||||
// Dismiss Button Handler
|
||||
dismissButton?.addEventListener('click', () => {
|
||||
installPrompt?.classList.add('hidden');
|
||||
localStorage.setItem('install-prompt-shown', 'true');
|
||||
});
|
||||
|
||||
// App wurde installiert
|
||||
window.addEventListener('appinstalled', () => {
|
||||
console.log('PWA wurde erfolgreich installiert');
|
||||
installPrompt?.classList.add('hidden');
|
||||
});
|
||||
</script>
|
||||
529
games/mana-games/apps/web/src/components/MyGamesSection.astro
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
---
|
||||
export interface Props {
|
||||
maxGames?: number;
|
||||
}
|
||||
|
||||
const { maxGames = 8 } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="my-games-section">
|
||||
<div class="section-header">
|
||||
<h2>Meine generierten Spiele</h2>
|
||||
<div class="section-actions">
|
||||
<button id="viewAllMyGames" class="action-btn">
|
||||
Alle anzeigen
|
||||
</button>
|
||||
<button id="clearMyGames" class="action-btn danger hidden">
|
||||
Alle löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="myGamesContainer" class="my-games-container">
|
||||
<div class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Lade deine Spiele...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="emptyState" class="empty-state hidden">
|
||||
<div class="empty-content">
|
||||
<p class="empty-icon">🎮</p>
|
||||
<p class="empty-text">Du hast noch keine Spiele erstellt</p>
|
||||
<a href="/create" class="create-btn">
|
||||
Erstelle dein erstes Spiel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
interface SavedGame {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
prompt: string;
|
||||
html: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
thumbnail?: string;
|
||||
stats?: {
|
||||
linesOfCode: number;
|
||||
hasAnimation: boolean;
|
||||
hasSound: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
class MyGamesManager {
|
||||
private dbName = 'ManaGamesDB';
|
||||
private storeName = 'generatedGames';
|
||||
private db: IDBDatabase | null = null;
|
||||
private container: HTMLElement;
|
||||
private emptyState: HTMLElement;
|
||||
private viewAllBtn: HTMLElement;
|
||||
private clearBtn: HTMLElement;
|
||||
private maxGames: number;
|
||||
|
||||
constructor(maxGames: number = 8) {
|
||||
this.container = document.getElementById('myGamesContainer')!;
|
||||
this.emptyState = document.getElementById('emptyState')!;
|
||||
this.viewAllBtn = document.getElementById('viewAllMyGames')!;
|
||||
this.clearBtn = document.getElementById('clearMyGames')!;
|
||||
this.maxGames = maxGames;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.openDB();
|
||||
await this.loadGames();
|
||||
|
||||
// Event listeners
|
||||
this.viewAllBtn.addEventListener('click', () => {
|
||||
window.location.href = '/my-games';
|
||||
});
|
||||
|
||||
this.clearBtn.addEventListener('click', async () => {
|
||||
if (confirm('Bist du sicher, dass du alle deine generierten Spiele löschen möchtest?')) {
|
||||
await this.clearAllGames();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async openDB(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||
store.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
store.createIndex('title', 'title', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async loadGames() {
|
||||
try {
|
||||
const games = await this.getAllGames();
|
||||
|
||||
if (games.length === 0) {
|
||||
this.showEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
games.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
// Show only first maxGames
|
||||
const displayGames = games.slice(0, this.maxGames);
|
||||
this.renderGames(displayGames, games.length);
|
||||
|
||||
// Show/hide buttons
|
||||
if (games.length > this.maxGames) {
|
||||
this.viewAllBtn.classList.remove('hidden');
|
||||
}
|
||||
this.clearBtn.classList.remove('hidden');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading games:', error);
|
||||
this.showError();
|
||||
}
|
||||
}
|
||||
|
||||
async getAllGames(): Promise<SavedGame[]> {
|
||||
if (!this.db) await this.openDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result || []);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteGame(id: string): Promise<void> {
|
||||
if (!this.db) await this.openDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async clearAllGames() {
|
||||
if (!this.db) await this.openDB();
|
||||
|
||||
const transaction = this.db!.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
await store.clear();
|
||||
|
||||
this.showEmptyState();
|
||||
this.clearBtn.classList.add('hidden');
|
||||
this.viewAllBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
renderGames(games: SavedGame[], totalCount: number) {
|
||||
const gamesHTML = games.map(game => this.createGameCard(game)).join('');
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="games-grid">
|
||||
${gamesHTML}
|
||||
</div>
|
||||
${totalCount > this.maxGames ? `
|
||||
<p class="more-games-text">
|
||||
+${totalCount - this.maxGames} weitere Spiele in deiner Bibliothek
|
||||
</p>
|
||||
` : ''}
|
||||
`;
|
||||
|
||||
// Add event listeners to game cards
|
||||
this.container.querySelectorAll('.delete-game-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
const gameId = (e.target as HTMLElement).closest('.delete-game-btn')?.getAttribute('data-game-id');
|
||||
if (gameId) {
|
||||
await this.deleteGame(gameId);
|
||||
await this.loadGames();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.container.querySelectorAll('.my-game-card').forEach(card => {
|
||||
card.addEventListener('click', (e) => {
|
||||
if (!(e.target as HTMLElement).closest('.delete-game-btn')) {
|
||||
const gameId = card.getAttribute('data-game-id');
|
||||
if (gameId) {
|
||||
window.location.href = `/play-generated?id=${gameId}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
createGameCard(game: SavedGame): string {
|
||||
const date = new Date(game.createdAt).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
|
||||
return `
|
||||
<div class="my-game-card" data-game-id="${game.id}">
|
||||
<div class="game-thumbnail">
|
||||
${game.thumbnail
|
||||
? `<img src="${game.thumbnail}" alt="${game.title}" />`
|
||||
: `<div class="placeholder-thumbnail">🎮</div>`
|
||||
}
|
||||
<button class="delete-game-btn" data-game-id="${game.id}" title="Spiel löschen">
|
||||
<span>×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="game-info">
|
||||
<h3>${game.title}</h3>
|
||||
<p class="game-date">${date}</p>
|
||||
${game.stats ? `
|
||||
<div class="game-stats">
|
||||
<span>${game.stats.linesOfCode} Zeilen</span>
|
||||
${game.stats.hasAnimation ? '<span>🎬</span>' : ''}
|
||||
${game.stats.hasSound ? '<span>🔊</span>' : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
showEmptyState() {
|
||||
this.container.classList.add('hidden');
|
||||
this.emptyState.classList.remove('hidden');
|
||||
}
|
||||
|
||||
showError() {
|
||||
this.container.innerHTML = `
|
||||
<div class="error-state">
|
||||
<p>Fehler beim Laden der Spiele</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const maxGames = parseInt(document.querySelector('.my-games-section')?.getAttribute('data-max-games') || '8');
|
||||
new MyGamesManager(maxGames);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.my-games-section {
|
||||
margin: 3rem 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--color-bg);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #ef4444;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.my-games-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 2px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.games-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.my-game-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.my-game-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.game-thumbnail {
|
||||
position: relative;
|
||||
aspect-ratio: 4/3;
|
||||
background: var(--color-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.game-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.placeholder-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.delete-game-btn {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.my-game-card:hover .delete-game-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.delete-game-btn:hover {
|
||||
background: rgba(239, 68, 68, 0.9);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.game-info {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.game-info h3 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.game-date {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.game-stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.game-stats span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.more-games-text {
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 2rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 4rem 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-content {
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin: 0 0 1rem 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 2rem 0;
|
||||
}
|
||||
|
||||
.create-btn {
|
||||
display: inline-block;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.create-btn:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.error-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.games-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
381
games/mana-games/apps/web/src/data/games.ts
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
export interface Game {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
htmlFile: string;
|
||||
thumbnail?: string;
|
||||
tags: string[];
|
||||
difficulty: 'Einfach' | 'Mittel' | 'Schwer';
|
||||
complexity: 'Minimal' | 'Einfach' | 'Mittel' | 'Komplex';
|
||||
controls: string;
|
||||
codeStats?: {
|
||||
total: number;
|
||||
code: number;
|
||||
comments: number;
|
||||
};
|
||||
// Community game fields
|
||||
community?: boolean;
|
||||
author?: string;
|
||||
submittedAt?: string;
|
||||
}
|
||||
|
||||
export const games: Game[] = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Snake',
|
||||
description: 'Der Klassiker! Steuere die Schlange und sammle Nahrung, aber vermeide die roten Felder!',
|
||||
slug: 'snake',
|
||||
htmlFile: '/games/snake_game.html',
|
||||
thumbnail: '/screenshots/snake.jpg',
|
||||
tags: ['Arcade', 'Klassiker', 'Retro'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Komplex',
|
||||
controls: 'Pfeiltasten oder WASD zum Steuern',
|
||||
codeStats: {
|
||||
total: 604,
|
||||
code: 338,
|
||||
comments: 192
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Space Defender',
|
||||
description: 'Verteidige dein Raumschiff gegen Wellen von Aliens. Die Schwierigkeit steigt mit der Zeit!',
|
||||
slug: 'space-defender',
|
||||
htmlFile: '/games/space_defender_game.html',
|
||||
thumbnail: '/screenshots/space-defenders.jpg',
|
||||
tags: ['Shooter', 'Arcade', 'Action'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Mittel',
|
||||
controls: 'A/D oder Pfeiltasten zum Bewegen, Leertaste zum Schießen',
|
||||
codeStats: {
|
||||
total: 436,
|
||||
code: 348,
|
||||
comments: 32
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Gravity Painter',
|
||||
description: 'Ein kreatives Physik-Puzzle! Setze Gravitationspunkte und lenke Partikel zu den Zielen.',
|
||||
slug: 'gravity-painter',
|
||||
htmlFile: '/games/gravity_painter.html',
|
||||
thumbnail: '/screenshots/gravity-painter.jpg',
|
||||
tags: ['Puzzle', 'Physik', 'Kreativ'],
|
||||
difficulty: 'Schwer',
|
||||
complexity: 'Mittel',
|
||||
controls: 'Klicke für Gravitationspunkte, Leertaste für Partikel',
|
||||
codeStats: {
|
||||
total: 426,
|
||||
code: 348,
|
||||
comments: 21
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Bounce & Catch Tutorial',
|
||||
description: 'Ein einfaches Lernspiel, das die Grundlagen der Spieleentwicklung zeigt. Perfekt für Anfänger!',
|
||||
slug: 'bounce-catch-tutorial',
|
||||
htmlFile: '/games/bounce_catch_tutorial.html',
|
||||
thumbnail: '/screenshots/bounce-catch.jpg',
|
||||
tags: ['Tutorial', 'Lernspiel', 'Arcade'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Einfach',
|
||||
controls: 'Mausbewegung zum Steuern des Paddles',
|
||||
codeStats: {
|
||||
total: 437,
|
||||
code: 289,
|
||||
comments: 87
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Neon Maze Runner',
|
||||
description: 'Navigiere durch prozedural generierte Labyrinthe! Sammle Diamanten, nutze Power-ups und finde den Ausgang.',
|
||||
slug: 'neon-maze-runner',
|
||||
htmlFile: '/games/neon_maze_runner.html',
|
||||
thumbnail: '/screenshots/neon-maze-runner.jpg',
|
||||
tags: ['Puzzle', 'Labyrinth', 'Arcade'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Komplex',
|
||||
controls: 'WASD oder Pfeiltasten zum Bewegen',
|
||||
codeStats: {
|
||||
total: 832,
|
||||
code: 644,
|
||||
comments: 69
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Rhythm Defender',
|
||||
description: 'Verteidige dich im Takt der Musik! Drücke die richtigen Tasten im perfekten Timing für maximale Combos.',
|
||||
slug: 'rhythm-defender',
|
||||
htmlFile: '/games/rhythm_defender.html',
|
||||
thumbnail: '/screenshots/rhythm-defender.jpg',
|
||||
tags: ['Rhythmus', 'Musik', 'Arcade'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Komplex',
|
||||
controls: 'A, S, D, F Tasten im Rhythmus drücken',
|
||||
codeStats: {
|
||||
total: 741,
|
||||
code: 584,
|
||||
comments: 56
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'Click Race',
|
||||
description: 'Das schnellste Spiel! Klicke 30 mal so schnell du kannst. Wie schnell bist du?',
|
||||
slug: 'click-race',
|
||||
htmlFile: '/games/click_race.html',
|
||||
thumbnail: '/screenshots/click-race.jpg',
|
||||
tags: ['Geschwindigkeit', 'Minimal', 'Arcade'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Minimal',
|
||||
controls: 'Klicke auf das rote Quadrat',
|
||||
codeStats: {
|
||||
total: 111,
|
||||
code: 88,
|
||||
comments: 23
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Color Memory',
|
||||
description: 'Merke dir die Farbreihenfolge! Ein klassisches Gedächtnisspiel das immer schwerer wird.',
|
||||
slug: 'color-memory',
|
||||
htmlFile: '/games/color_memory.html',
|
||||
thumbnail: '/screenshots/color-memory.jpg',
|
||||
tags: ['Gedächtnis', 'Minimal', 'Puzzle'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Minimal',
|
||||
controls: 'Klicke die Farben in der richtigen Reihenfolge',
|
||||
codeStats: {
|
||||
total: 86,
|
||||
code: 86,
|
||||
comments: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
title: 'Reaction Test',
|
||||
description: 'Wie schnell sind deine Reflexe? Klicke so schnell wie möglich wenn der Bildschirm grün wird!',
|
||||
slug: 'reaction-test',
|
||||
htmlFile: '/games/reaction_test.html',
|
||||
thumbnail: '/screenshots/reaction-test.jpg',
|
||||
tags: ['Reaktion', 'Minimal', 'Test'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Minimal',
|
||||
controls: 'Klicke wenn der Bildschirm grün wird',
|
||||
codeStats: {
|
||||
total: 78,
|
||||
code: 78,
|
||||
comments: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
title: 'Asteroid Dash',
|
||||
description: 'Fliege durch gefährliche Asteroidenfelder! Sammle Energie-Kristalle, nutze Power-ups und weiche den rotierenden Asteroiden aus.',
|
||||
slug: 'asteroid-dash',
|
||||
htmlFile: '/games/asteroid_dash.html',
|
||||
thumbnail: '/screenshots/asteroid-dash.jpg',
|
||||
tags: ['Action', 'Arcade', 'Weltraum'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Mittel',
|
||||
controls: 'WASD oder Pfeiltasten zum Fliegen, Leertaste für Boost',
|
||||
codeStats: {
|
||||
total: 485,
|
||||
code: 428,
|
||||
comments: 57
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '11',
|
||||
title: 'Fish Catcher',
|
||||
description: 'Fange Fische mit deinem Boot! Verschiedene Fischarten bringen unterschiedliche Punkte. Sammle Power-ups für größere Netze und Boni.',
|
||||
slug: 'fish-catcher',
|
||||
htmlFile: '/games/fish_catcher.html',
|
||||
thumbnail: '/screenshots/fish-catcher.jpg',
|
||||
tags: ['Arcade', 'Familie', 'Entspannend'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Einfach',
|
||||
controls: 'A/D oder Pfeiltasten zum Bewegen, Maus für sanfte Steuerung',
|
||||
codeStats: {
|
||||
total: 362,
|
||||
code: 321,
|
||||
comments: 41
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
title: 'Balloon Pop',
|
||||
description: 'Platze bunte Ballons bevor sie entkommen! Verschiedene Ballonarten, Power-ups und Combo-System für maximalen Spaß.',
|
||||
slug: 'balloon-pop',
|
||||
htmlFile: '/games/balloon_pop.html',
|
||||
thumbnail: '/screenshots/balloon-pop.jpg',
|
||||
tags: ['Geschicklichkeit', 'Familie', 'Bunt'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Einfach',
|
||||
controls: 'Maus zum Klicken auf Ballons',
|
||||
codeStats: {
|
||||
total: 398,
|
||||
code: 351,
|
||||
comments: 47
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '13',
|
||||
title: 'Word Scramble',
|
||||
description: 'Entschlüssele durcheinandergewürfelte Wörter! Mit 5 Kategorien, Combo-System und steigender Schwierigkeit.',
|
||||
slug: 'word-scramble',
|
||||
htmlFile: '/games/word_scramble.html',
|
||||
thumbnail: '/screenshots/word-scramble.jpg',
|
||||
tags: ['Puzzle', 'Wortspiel', 'Bildung'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Mittel',
|
||||
controls: 'Tastatur zum Eingeben, Maus zum Klicken auf Buchstaben',
|
||||
codeStats: {
|
||||
total: 850,
|
||||
code: 720,
|
||||
comments: 130
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '14',
|
||||
title: 'Memory Card Match',
|
||||
description: 'Das klassische Memory-Spiel! Finde alle Kartenpaare mit Emojis. Drei Schwierigkeitsstufen für jeden Spieler.',
|
||||
slug: 'memory-card-match',
|
||||
htmlFile: '/games/memory_card_match.html',
|
||||
thumbnail: '/screenshots/memory-card-match.jpg',
|
||||
tags: ['Gedächtnis', 'Kartenspiel', 'Familie'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Einfach',
|
||||
controls: 'Maus zum Aufdecken der Karten',
|
||||
codeStats: {
|
||||
total: 415,
|
||||
code: 350,
|
||||
comments: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '15',
|
||||
title: 'Turbo Racer',
|
||||
description: 'Drift durch die Kurven und stelle Bestzeiten auf! Mit realistischer Drift-Physik und Nitro-Boost.',
|
||||
slug: 'turbo-racer',
|
||||
htmlFile: '/games/turbo_racer.html',
|
||||
thumbnail: '/screenshots/turbo-racer.jpg',
|
||||
tags: ['Rennen', 'Action', 'Arcade'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Mittel',
|
||||
controls: 'WASD oder Pfeiltasten zum Fahren, Leertaste für Boost',
|
||||
codeStats: {
|
||||
total: 680,
|
||||
code: 620,
|
||||
comments: 60
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '16',
|
||||
title: 'Card Stack Rush',
|
||||
description: 'Sortiere Karten blitzschnell auf die richtigen Stapel! Mit wechselnden Regeln, Combo-System und Zeitdruck.',
|
||||
slug: 'card-stack-rush',
|
||||
htmlFile: '/games/card_stack_rush.html',
|
||||
thumbnail: '/screenshots/card-stack-rush.jpg',
|
||||
tags: ['Kartenspiel', 'Geschwindigkeit', 'Arcade'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Einfach',
|
||||
controls: 'Drag & Drop oder Klicken zum Platzieren',
|
||||
codeStats: {
|
||||
total: 520,
|
||||
code: 480,
|
||||
comments: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '17',
|
||||
title: 'Flappy Mana',
|
||||
description: 'Fliege durch Röhren und sammle Punkte! Ein Flappy Bird Klon mit Partikeleffekten und Highscore-System.',
|
||||
slug: 'flappy-mana',
|
||||
htmlFile: '/games/flappy_mana.html',
|
||||
thumbnail: '/screenshots/flappy-mana.jpg',
|
||||
tags: ['Arcade', 'Geschicklichkeit', 'Endless'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Einfach',
|
||||
controls: 'Klick oder Leertaste zum Fliegen',
|
||||
codeStats: {
|
||||
total: 450,
|
||||
code: 430,
|
||||
comments: 20
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '18',
|
||||
title: 'Mana Runner',
|
||||
description: 'Laufe und springe durch magische Welten! Sammle Mana-Kristalle, weiche Hindernissen aus und schalte den Doppelsprung frei.',
|
||||
slug: 'mana-runner',
|
||||
htmlFile: '/games/mana_runner.html',
|
||||
thumbnail: '/screenshots/mana-runner.jpg',
|
||||
tags: ['Jump n Run', 'Arcade', 'Endless'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Mittel',
|
||||
controls: 'Leertaste zum Springen, Doppelsprung nach 10 Kristallen',
|
||||
codeStats: {
|
||||
total: 600,
|
||||
code: 580,
|
||||
comments: 20
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '19',
|
||||
title: 'Mana Defense',
|
||||
description: 'Verteidige deinen Mana-Kristall! Baue Türme, plane deine Strategie und überlebe 20 Wellen von Gegnern.',
|
||||
slug: 'mana-defense',
|
||||
htmlFile: '/games/mana_defense.html',
|
||||
thumbnail: '/screenshots/mana-defense.jpg',
|
||||
tags: ['Tower Defense', 'Strategie', 'Aufbau'],
|
||||
difficulty: 'Schwer',
|
||||
complexity: 'Komplex',
|
||||
controls: 'Maus zum Platzieren, 1-3 für Turmauswahl, S zum Verkaufen',
|
||||
codeStats: {
|
||||
total: 900,
|
||||
code: 850,
|
||||
comments: 50
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '20',
|
||||
title: 'Mana Factory',
|
||||
description: 'Baue die größte Mana-Produktionsanlage! Ein Idle-Game mit Upgrades, Prestige-System und exponentiellem Wachstum.',
|
||||
slug: 'mana-factory',
|
||||
htmlFile: '/games/mana_factory.html',
|
||||
thumbnail: '/screenshots/mana-factory.jpg',
|
||||
tags: ['Idle', 'Incremental', 'Aufbau'],
|
||||
difficulty: 'Einfach',
|
||||
complexity: 'Mittel',
|
||||
controls: 'Maus zum Klicken und Kaufen',
|
||||
codeStats: {
|
||||
total: 800,
|
||||
code: 750,
|
||||
comments: 50
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '21',
|
||||
title: 'Puzzle Blocks',
|
||||
description: 'Klassisches Tetris-Gameplay! Stapele fallende Blöcke, vervollständige Reihen und erreiche den höchsten Score.',
|
||||
slug: 'puzzle-blocks',
|
||||
htmlFile: '/games/puzzle_blocks.html',
|
||||
thumbnail: '/screenshots/puzzle-blocks.jpg',
|
||||
tags: ['Puzzle', 'Klassiker', 'Arcade'],
|
||||
difficulty: 'Mittel',
|
||||
complexity: 'Einfach',
|
||||
controls: '← → zum Bewegen, ↑ zum Drehen, ↓ schneller fallen, Space für Harddrop',
|
||||
codeStats: {
|
||||
total: 450,
|
||||
code: 420,
|
||||
comments: 30
|
||||
}
|
||||
}
|
||||
];
|
||||
713
games/mana-games/apps/web/src/layouts/Layout.astro
Normal file
|
|
@ -0,0 +1,713 @@
|
|||
---
|
||||
import Button from '../components/Button.astro';
|
||||
import InstallPrompt from '../components/InstallPrompt.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
description?: string;
|
||||
isGamePage?: boolean;
|
||||
gameTitle?: string;
|
||||
gameSlug?: string;
|
||||
isPlayground?: boolean;
|
||||
fullWidth?: boolean;
|
||||
hideFooter?: boolean;
|
||||
}
|
||||
|
||||
const { title, description = "Mana Games - Eine Sammlung von Web-basierten Spielen", isGamePage = false, gameTitle, gameSlug, isPlayground = false, fullWidth = false, hideFooter = false } = Astro.props;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title} | Mana Games</title>
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<!-- Theme Color -->
|
||||
<meta name="theme-color" content="#1a1a1a" />
|
||||
|
||||
<!-- iOS Meta Tags -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Mana Games" />
|
||||
|
||||
<!-- iOS Icons -->
|
||||
<link rel="apple-touch-icon" href="/icons/icon-180x180.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/icons/icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/icons/icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="167x167" href="/icons/icon-167x167.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/icons/icon-180x180.png" />
|
||||
|
||||
<!-- iOS Splash Screens -->
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-640x1136.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 667px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-750x1334.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-828x1792.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 375px) and (device-height: 812px) and (-webkit-device-pixel-ratio: 3)" href="/splash/splash-1125x2436.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 414px) and (device-height: 896px) and (-webkit-device-pixel-ratio: 3)" href="/splash/splash-1242x2688.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 768px) and (device-height: 1024px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-1536x2048.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 834px) and (device-height: 1112px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-1668x2224.png" />
|
||||
<link rel="apple-touch-startup-image" media="(device-width: 1024px) and (device-height: 1366px) and (-webkit-device-pixel-ratio: 2)" href="/splash/splash-2048x2732.png" />
|
||||
|
||||
<!-- Microsoft Tiles -->
|
||||
<meta name="msapplication-TileColor" content="#1a1a1a" />
|
||||
<meta name="msapplication-TileImage" content="/icons/icon-144x144.png" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content={title + " | Mana Games"} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image" content="/icons/icon-512x512.png" />
|
||||
</head>
|
||||
<body class={fullWidth ? 'full-width' : ''}>
|
||||
<nav>
|
||||
<div class="nav-container">
|
||||
{isGamePage ? (
|
||||
<div class="breadcrumb">
|
||||
<a href="/" class="breadcrumb-logo">
|
||||
<span class="logo-text">MANA</span>
|
||||
<span class="logo-accent">GAMES</span>
|
||||
</a>
|
||||
<span class="breadcrumb-separator">›</span>
|
||||
<span class="breadcrumb-game">{gameTitle}</span>
|
||||
</div>
|
||||
) : (
|
||||
<a href="/" class="logo">
|
||||
<span class="logo-text">MANA</span>
|
||||
<span class="logo-accent">GAMES</span>
|
||||
</a>
|
||||
)}
|
||||
<div class="nav-links">
|
||||
{isGamePage ? (
|
||||
<div class="game-controls">
|
||||
<Button
|
||||
href="/"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Zurück zur Spieleübersicht"
|
||||
class="back-btn"
|
||||
>
|
||||
<span class="icon">←</span>
|
||||
</Button>
|
||||
<div class="separator"></div>
|
||||
<Button
|
||||
id="menuBtn"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Menü öffnen"
|
||||
>
|
||||
<span class="icon">☰</span>
|
||||
</Button>
|
||||
<Button
|
||||
id="refreshBtn"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Spiel neu laden"
|
||||
>
|
||||
<span class="icon">↻</span>
|
||||
</Button>
|
||||
<Button
|
||||
id="fullscreenBtn"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Vollbild"
|
||||
>
|
||||
<span class="icon">⛶</span>
|
||||
</Button>
|
||||
<div class="separator"></div>
|
||||
{isPlayground ? (
|
||||
<Button
|
||||
href={`/games/${gameSlug}`}
|
||||
variant="accent"
|
||||
size="icon"
|
||||
title="Zum Spiel"
|
||||
>
|
||||
<span class="icon">🎮</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
href={`/games/${gameSlug}/playground`}
|
||||
variant="accent"
|
||||
size="icon"
|
||||
title="Code bearbeiten"
|
||||
>
|
||||
<span class="icon">🔧</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div class="nav-menu">
|
||||
<Button href="/" variant="accent">Spiele</Button>
|
||||
<Button href="/create" variant="accent">KI Generator</Button>
|
||||
<Button href="/community" variant="accent">Community</Button>
|
||||
<Button href="/submit" variant="ghost">Einreichen</Button>
|
||||
<Button href="/stats" variant="ghost">Stats</Button>
|
||||
|
||||
<!-- More Dropdown -->
|
||||
<div class="dropdown">
|
||||
<button class="dropdown-toggle" id="moreDropdown" title="Mehr Optionen">
|
||||
<span class="icon">⋮</span>
|
||||
</button>
|
||||
<div class="dropdown-menu" id="moreDropdownMenu">
|
||||
<button class="dropdown-item" id="debugToggleDropdown">
|
||||
<span class="icon">🐛</span>
|
||||
<span>Debug Borders</span>
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/datenschutz" class="dropdown-item">
|
||||
<span class="icon">🔒</span>
|
||||
<span>Datenschutz</span>
|
||||
</a>
|
||||
<a href="/impressum" class="dropdown-item">
|
||||
<span class="icon">📋</span>
|
||||
<span>Impressum</span>
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a href="/agb" class="dropdown-item">
|
||||
<span class="icon">📜</span>
|
||||
<span>AGB</span>
|
||||
</a>
|
||||
<a href="/jugendschutz" class="dropdown-item">
|
||||
<span class="icon">🛡️</span>
|
||||
<span>Jugendschutz</span>
|
||||
</a>
|
||||
<a href="/copyright" class="dropdown-item">
|
||||
<span class="icon">©️</span>
|
||||
<span>Copyright</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
{!hideFooter && <Footer />}
|
||||
<InstallPrompt />
|
||||
<script>
|
||||
// Service Worker Registration
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', async () => {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register('/sw.js');
|
||||
console.log('Service Worker registriert:', registration);
|
||||
|
||||
// Update gefunden
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
// Neuer Service Worker verfügbar
|
||||
if (confirm('Neue Version verfügbar! Jetzt aktualisieren?')) {
|
||||
newWorker.postMessage({ type: 'SKIP_WAITING' });
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Service Worker Registrierung fehlgeschlagen:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// iOS PWA Detection
|
||||
if (window.navigator.standalone === true) {
|
||||
document.documentElement.classList.add('ios-pwa');
|
||||
}
|
||||
|
||||
// Debug Borders Toggle
|
||||
const debugToggleDropdown = document.getElementById('debugToggleDropdown');
|
||||
const debugState = localStorage.getItem('debugBorders') === 'true';
|
||||
|
||||
// Apply initial state
|
||||
if (debugState) {
|
||||
document.body.classList.add('debug-borders');
|
||||
}
|
||||
|
||||
// Function to toggle debug borders
|
||||
function toggleDebugBorders() {
|
||||
const isEnabled = document.body.classList.toggle('debug-borders');
|
||||
localStorage.setItem('debugBorders', isEnabled.toString());
|
||||
}
|
||||
|
||||
// Add click handler for dropdown button
|
||||
debugToggleDropdown?.addEventListener('click', () => {
|
||||
toggleDebugBorders();
|
||||
// Close dropdown after clicking
|
||||
document.getElementById('moreDropdownMenu')?.classList.remove('show');
|
||||
});
|
||||
|
||||
// Dropdown Menu Toggle
|
||||
const moreDropdown = document.getElementById('moreDropdown');
|
||||
const moreDropdownMenu = document.getElementById('moreDropdownMenu');
|
||||
|
||||
moreDropdown?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
moreDropdownMenu?.classList.toggle('show');
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
document.addEventListener('click', () => {
|
||||
moreDropdownMenu?.classList.remove('show');
|
||||
});
|
||||
|
||||
// Prevent dropdown from closing when clicking inside
|
||||
moreDropdownMenu?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// Install Prompt für Android
|
||||
let deferredPrompt;
|
||||
window.addEventListener('beforeinstallprompt', (e) => {
|
||||
e.preventDefault();
|
||||
deferredPrompt = e;
|
||||
|
||||
// Zeige Install-Button wenn gewünscht
|
||||
const installButton = document.getElementById('install-button');
|
||||
if (installButton) {
|
||||
installButton.style.display = 'block';
|
||||
installButton.addEventListener('click', async () => {
|
||||
deferredPrompt.prompt();
|
||||
const { outcome } = await deferredPrompt.userChoice;
|
||||
console.log(`User ${outcome} the install prompt`);
|
||||
deferredPrompt = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style is:global>
|
||||
:root {
|
||||
--color-bg: #0a0a0a;
|
||||
--color-bg-secondary: #1a1a1a;
|
||||
--color-text: #ffffff;
|
||||
--color-text-secondary: #b0b0b0;
|
||||
--color-accent: #00ff88;
|
||||
--color-accent-secondary: #00cc6a;
|
||||
--color-border: #2a2a2a;
|
||||
--max-width: 1200px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body.no-scroll {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
body.full-width main {
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Override container widths for full-width pages */
|
||||
body.full-width .hero,
|
||||
body.full-width .games-section,
|
||||
body.full-width .stats-section,
|
||||
body.full-width section {
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
body.full-width .games-grid {
|
||||
max-width: none !important;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
body.full-width .section-content,
|
||||
body.full-width .stats-container {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
body.no-scroll main {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
/* Nav Left Container */
|
||||
.nav-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
/* Debug Toggle Button */
|
||||
.debug-toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
opacity: 0.7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Dropdown Styles */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 1.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdown-toggle:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 0.5rem;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
padding: 0.5rem;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.dropdown-menu.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.dropdown-item:hover {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.dropdown-item .icon {
|
||||
font-size: 1.1rem;
|
||||
width: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background: var(--color-border);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.debug-toggle:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.debug-toggle.clicked {
|
||||
animation: pulse 0.3s ease;
|
||||
}
|
||||
|
||||
body.debug-borders .debug-toggle {
|
||||
opacity: 1;
|
||||
color: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% { transform: translateY(-50%) scale(1); }
|
||||
50% { transform: translateY(-50%) scale(1.2); }
|
||||
100% { transform: translateY(-50%) scale(1); }
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.logo-accent {
|
||||
color: var(--color-accent);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-menu {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.game-controls {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background-color: var(--color-border);
|
||||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
/* Special styling for back button */
|
||||
.back-btn:hover {
|
||||
background-color: var(--color-text) !important;
|
||||
color: var(--color-bg) !important;
|
||||
border-color: var(--color-text) !important;
|
||||
}
|
||||
|
||||
/* Breadcrumb Navigation */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.breadcrumb-logo {
|
||||
text-decoration: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.05em;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.breadcrumb-logo:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.breadcrumb-logo .logo-text {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.breadcrumb-logo .logo-accent {
|
||||
color: var(--color-accent);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0.3;
|
||||
font-size: 1.5rem;
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-game {
|
||||
color: var(--color-text);
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-logo {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-game {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.debug-toggle {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.4rem;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
padding: 0.4rem 0.6rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
min-width: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Debug Borders Styles */
|
||||
body.debug-borders * {
|
||||
outline: 1px solid rgba(255, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
body.debug-borders div {
|
||||
outline-color: rgba(0, 255, 0, 0.3);
|
||||
}
|
||||
|
||||
body.debug-borders button,
|
||||
body.debug-borders a {
|
||||
outline-color: rgba(0, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
body.debug-borders section,
|
||||
body.debug-borders article,
|
||||
body.debug-borders main,
|
||||
body.debug-borders nav,
|
||||
body.debug-borders header,
|
||||
body.debug-borders footer {
|
||||
outline-color: rgba(255, 255, 0, 0.4);
|
||||
outline-width: 2px;
|
||||
}
|
||||
|
||||
body.debug-borders form,
|
||||
body.debug-borders input,
|
||||
body.debug-borders textarea,
|
||||
body.debug-borders select {
|
||||
outline-color: rgba(255, 0, 255, 0.4);
|
||||
}
|
||||
|
||||
body.debug-borders img,
|
||||
body.debug-borders video,
|
||||
body.debug-borders iframe,
|
||||
body.debug-borders canvas {
|
||||
outline-color: rgba(255, 128, 0, 0.5);
|
||||
outline-width: 2px;
|
||||
}
|
||||
|
||||
body.debug-borders .container,
|
||||
body.debug-borders .wrapper,
|
||||
body.debug-borders .panel,
|
||||
body.debug-borders .split-container,
|
||||
body.debug-borders .left-panel,
|
||||
body.debug-borders .right-panel {
|
||||
outline-color: rgba(128, 128, 255, 0.5);
|
||||
outline-width: 2px;
|
||||
outline-style: dashed;
|
||||
}
|
||||
|
||||
/* Hover effect for debug mode */
|
||||
body.debug-borders *:hover {
|
||||
outline-width: 2px;
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
/* Exclude debug button from debug borders */
|
||||
body.debug-borders .debug-toggle {
|
||||
outline: none !important;
|
||||
}
|
||||
</style>
|
||||
672
games/mana-games/apps/web/src/pages/about.astro
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
import { games } from '../data/games';
|
||||
|
||||
// Statistiken berechnen
|
||||
const totalGames = games.length;
|
||||
const totalLines = games.reduce((sum, game) => sum + (game.codeStats?.total || 0), 0);
|
||||
const genres = [...new Set(games.flatMap(game => game.tags))].length;
|
||||
const complexityBreakdown = {
|
||||
'Minimal': games.filter(g => g.complexity === 'Minimal').length,
|
||||
'Einfach': games.filter(g => g.complexity === 'Einfach').length,
|
||||
'Mittel': games.filter(g => g.complexity === 'Mittel').length,
|
||||
'Komplex': games.filter(g => g.complexity === 'Komplex').length
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title="Über uns">
|
||||
<div class="about-hero">
|
||||
<div class="hero-background">
|
||||
<div class="floating-element element-1"></div>
|
||||
<div class="floating-element element-2"></div>
|
||||
<div class="floating-element element-3"></div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
<span class="title-line">Mehr als nur</span>
|
||||
<span class="title-highlight">Spiele</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
Eine Plattform für Kreativität, Lernen und Spaß
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="about-container">
|
||||
<!-- Mission Section -->
|
||||
<section class="mission-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">01</span>
|
||||
<h2>Unsere Mission</h2>
|
||||
</div>
|
||||
<div class="mission-grid">
|
||||
<div class="mission-card">
|
||||
<div class="card-icon">🎮</div>
|
||||
<h3>Spielen ohne Grenzen</h3>
|
||||
<p>Keine Downloads, keine Installationen. Einfach spielen - direkt im Browser, auf jedem Gerät.</p>
|
||||
</div>
|
||||
<div class="mission-card">
|
||||
<div class="card-icon">🎨</div>
|
||||
<h3>Kreativität fördern</h3>
|
||||
<p>Jedes Spiel ist ein Kunstwerk aus Code. Von minimalistisch bis komplex - wir zeigen die Vielfalt der Spieleentwicklung.</p>
|
||||
</div>
|
||||
<div class="mission-card">
|
||||
<div class="card-icon">📚</div>
|
||||
<h3>Lernen durch Code</h3>
|
||||
<p>Unsere Spiele sind vollständig dokumentiert und dienen als Lernressource für angehende Entwickler.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="stats-section">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{totalGames}</div>
|
||||
<div class="stat-label">Spiele</div>
|
||||
<div class="stat-detail">und es werden mehr</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{totalLines.toLocaleString('de-DE')}</div>
|
||||
<div class="stat-label">Zeilen Code</div>
|
||||
<div class="stat-detail">handgeschrieben</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{genres}</div>
|
||||
<div class="stat-label">Genres</div>
|
||||
<div class="stat-detail">für jeden Geschmack</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">100%</div>
|
||||
<div class="stat-label">Open Source</div>
|
||||
<div class="stat-detail">lerne vom Code</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Games Showcase -->
|
||||
<section class="showcase-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">02</span>
|
||||
<h2>Unser Spielekatalog</h2>
|
||||
</div>
|
||||
<div class="showcase-content">
|
||||
<div class="complexity-chart">
|
||||
<h3>Komplexität unserer Spiele</h3>
|
||||
<div class="chart-bars">
|
||||
{Object.entries(complexityBreakdown).map(([level, count]) => (
|
||||
<div class="chart-bar">
|
||||
<div class="bar-fill" style={`height: ${(count / totalGames) * 100}%`}>
|
||||
<span class="bar-count">{count}</span>
|
||||
</div>
|
||||
<span class="bar-label">{level}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="featured-games">
|
||||
<h3>Beliebte Kategorien</h3>
|
||||
<div class="category-tags">
|
||||
<span class="tag">🕹️ Arcade</span>
|
||||
<span class="tag">🧩 Puzzle</span>
|
||||
<span class="tag">🚀 Action</span>
|
||||
<span class="tag">🎵 Rhythmus</span>
|
||||
<span class="tag">🏃 Jump'n'Run</span>
|
||||
<span class="tag">🗼 Tower Defense</span>
|
||||
</div>
|
||||
<p class="showcase-text">
|
||||
Von klassischen Arcade-Spielen wie Snake bis zu innovativen Physik-Puzzles wie Gravity Painter -
|
||||
unsere Sammlung wächst stetig. Jedes Spiel ist mit Liebe zum Detail entwickelt und optimiert für
|
||||
flüssige Performance auf allen Geräten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Technology Section -->
|
||||
<section class="tech-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">03</span>
|
||||
<h2>Moderne Technologie</h2>
|
||||
</div>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">
|
||||
<span>HTML5</span>
|
||||
</div>
|
||||
<h4>Canvas API</h4>
|
||||
<p>Flüssige 60 FPS Grafiken direkt im Browser</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">
|
||||
<span>JS</span>
|
||||
</div>
|
||||
<h4>Vanilla JavaScript</h4>
|
||||
<p>Keine Dependencies, pure Performance</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">
|
||||
<span>PWA</span>
|
||||
</div>
|
||||
<h4>Progressive Web App</h4>
|
||||
<p>Installierbar, offline spielbar</p>
|
||||
</div>
|
||||
<div class="tech-card">
|
||||
<div class="tech-icon">
|
||||
<span>📱</span>
|
||||
</div>
|
||||
<h4>Responsive Design</h4>
|
||||
<p>Perfekt auf jedem Bildschirm</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Philosophy Section -->
|
||||
<section class="philosophy-section">
|
||||
<div class="philosophy-content">
|
||||
<h2>Unsere Philosophie</h2>
|
||||
<blockquote>
|
||||
"Spiele sollten mehr sein als nur Unterhaltung. Sie sind interaktive Kunst,
|
||||
technische Meisterwerke und Lernwerkzeuge in einem."
|
||||
</blockquote>
|
||||
<div class="philosophy-points">
|
||||
<div class="point">
|
||||
<span class="point-icon">✨</span>
|
||||
<div>
|
||||
<strong>Qualität vor Quantität</strong>
|
||||
<p>Jedes Spiel wird sorgfältig entwickelt und getestet</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="point">
|
||||
<span class="point-icon">🌍</span>
|
||||
<div>
|
||||
<strong>Zugänglichkeit für alle</strong>
|
||||
<p>Kostenlos, werbefrei und ohne versteckte Kosten</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="point">
|
||||
<span class="point-icon">💡</span>
|
||||
<div>
|
||||
<strong>Innovation fördern</strong>
|
||||
<p>Neue Spielkonzepte und kreative Mechaniken</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<div class="cta-content">
|
||||
<h2>Werde Teil der Community</h2>
|
||||
<p>
|
||||
Hast du Ideen für neue Spiele? Möchtest du zur Plattform beitragen?
|
||||
Oder einfach nur Feedback geben? Wir freuen uns von dir zu hören!
|
||||
</p>
|
||||
<div class="cta-buttons">
|
||||
<Button href="/" variant="primary" size="large">
|
||||
Spiele entdecken
|
||||
</Button>
|
||||
<Button href="/stats" variant="accent" size="large">
|
||||
Deine Statistiken
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Hero Section */
|
||||
.about-hero {
|
||||
position: relative;
|
||||
min-height: 50vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.hero-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.floating-element {
|
||||
position: absolute;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.element-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.element-2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -50px;
|
||||
right: -50px;
|
||||
animation-delay: 5s;
|
||||
}
|
||||
|
||||
.element-3 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 50%;
|
||||
left: 80%;
|
||||
animation-delay: 10s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
||||
33% { transform: translate(30px, -30px) rotate(120deg); }
|
||||
66% { transform: translate(-20px, 20px) rotate(240deg); }
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(3rem, 8vw, 5rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.6s ease forwards;
|
||||
}
|
||||
|
||||
.title-highlight {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.6s ease 0.2s forwards;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.6s ease 0.4s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.about-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
section {
|
||||
margin-bottom: 6rem;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: fadeInUp 0.8s ease forwards;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-number {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
color: var(--color-accent);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* Mission Section */
|
||||
.mission-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.mission-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.mission-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mission-card h3 {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mission-card p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.stats-section {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text);
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.stat-detail {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Showcase Section */
|
||||
.showcase-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 3rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.complexity-chart {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.complexity-chart h3 {
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.chart-bars {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: flex-end;
|
||||
height: 200px;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.chart-bar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
width: 100%;
|
||||
background: linear-gradient(to top, var(--color-accent), var(--color-accent-secondary));
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 0.5rem;
|
||||
transition: height 0.5s ease;
|
||||
}
|
||||
|
||||
.bar-count {
|
||||
color: #000;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.featured-games h3 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.category-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 2rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.showcase-text {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* Tech Section */
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.tech-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tech-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.tech-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1rem;
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
|
||||
border-radius: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
font-size: 1.5rem;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.tech-card h4 {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tech-card p {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Philosophy Section */
|
||||
.philosophy-section {
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 2rem;
|
||||
padding: 4rem;
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.philosophy-content h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: 1.5rem;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 2rem 0 3rem;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.philosophy-points {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.point {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.point-icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.point strong {
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.point p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), transparent);
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.cta-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cta-content p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.showcase-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.philosophy-section {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
470
games/mana-games/apps/web/src/pages/agb.astro
Normal file
|
|
@ -0,0 +1,470 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
---
|
||||
|
||||
<Layout title="Nutzungsbedingungen">
|
||||
<div class="agb-container">
|
||||
<header class="agb-header">
|
||||
<h1>Allgemeine Geschäftsbedingungen</h1>
|
||||
<p class="subtitle">Nutzungsbedingungen für Mana Games</p>
|
||||
<p class="last-updated">Stand: Januar 2024</p>
|
||||
</header>
|
||||
|
||||
<nav class="toc">
|
||||
<h2>Inhaltsverzeichnis</h2>
|
||||
<ol>
|
||||
<li><a href="#geltungsbereich">Geltungsbereich</a></li>
|
||||
<li><a href="#nutzung">Nutzung der Plattform</a></li>
|
||||
<li><a href="#registrierung">Registrierung und Nutzerkonto</a></li>
|
||||
<li><a href="#inhalte">Nutzergenierte Inhalte</a></li>
|
||||
<li><a href="#verhaltensregeln">Verhaltensregeln</a></li>
|
||||
<li><a href="#geistigeseigentum">Geistiges Eigentum</a></li>
|
||||
<li><a href="#haftung">Haftungsausschluss</a></li>
|
||||
<li><a href="#datenschutz">Datenschutz</a></li>
|
||||
<li><a href="#aenderungen">Änderungen der AGB</a></li>
|
||||
<li><a href="#schlussbestimmungen">Schlussbestimmungen</a></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<section id="geltungsbereich" class="content-section">
|
||||
<h2>§ 1 Geltungsbereich</h2>
|
||||
|
||||
<p>
|
||||
(1) Diese Allgemeinen Geschäftsbedingungen (nachfolgend "AGB") gelten für die Nutzung
|
||||
der Website mana-games.netlify.app (nachfolgend "Plattform") und alle darauf angebotenen
|
||||
Dienste und Spiele.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Mit der Nutzung der Plattform akzeptieren Sie diese AGB. Wenn Sie mit diesen
|
||||
Bedingungen nicht einverstanden sind, nutzen Sie bitte unsere Dienste nicht.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(3) Die Plattform richtet sich an Nutzer aller Altersgruppen. Für minderjährige
|
||||
Nutzer gelten zusätzlich unsere <a href="/jugendschutz">Jugendschutzbestimmungen</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="nutzung" class="content-section">
|
||||
<h2>§ 2 Nutzung der Plattform</h2>
|
||||
|
||||
<p>
|
||||
(1) Die Nutzung der Plattform und der darauf angebotenen Spiele ist grundsätzlich
|
||||
kostenlos und ohne Registrierung möglich.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Wir gewähren Ihnen ein nicht-exklusives, nicht übertragbares, widerrufliches
|
||||
Recht zur persönlichen Nutzung der Plattform und der Spiele.
|
||||
</p>
|
||||
|
||||
<div class="highlight-box">
|
||||
<h4>Erlaubte Nutzung umfasst:</h4>
|
||||
<ul>
|
||||
<li>Spielen aller verfügbaren Spiele</li>
|
||||
<li>Erstellen eigener Spiele mit dem KI-Generator</li>
|
||||
<li>Speichern von Spielständen im lokalen Browser-Speicher</li>
|
||||
<li>Teilen von Links zu Spielen</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<h4>Nicht erlaubt ist:</h4>
|
||||
<ul>
|
||||
<li>Kommerzielle Nutzung ohne ausdrückliche Genehmigung</li>
|
||||
<li>Automatisierte Zugriffe (Bots, Scraping)</li>
|
||||
<li>Umgehung von Sicherheitsmaßnahmen</li>
|
||||
<li>Verbreitung von Schadsoftware</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="registrierung" class="content-section">
|
||||
<h2>§ 3 Registrierung und Nutzerkonto</h2>
|
||||
|
||||
<p>
|
||||
(1) Aktuell ist keine Registrierung für die Nutzung der Plattform erforderlich.
|
||||
Alle Daten werden lokal in Ihrem Browser gespeichert.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Sollten wir in Zukunft Nutzerkonten einführen, werden wir Sie rechtzeitig
|
||||
informieren und separate Bedingungen dafür bereitstellen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="inhalte" class="content-section">
|
||||
<h2>§ 4 Nutzergenierte Inhalte</h2>
|
||||
|
||||
<p>
|
||||
(1) Mit unserem KI-Generator können Sie eigene Spiele erstellen. Diese werden
|
||||
ausschließlich lokal in Ihrem Browser gespeichert.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Sie sind für die von Ihnen erstellten Inhalte selbst verantwortlich und
|
||||
stellen sicher, dass diese:
|
||||
</p>
|
||||
|
||||
<ul class="content-list">
|
||||
<li>Keine Rechte Dritter verletzen</li>
|
||||
<li>Keine illegalen Inhalte enthalten</li>
|
||||
<li>Nicht diskriminierend, beleidigend oder anstößig sind</li>
|
||||
<li>Keine Gewaltverherrlichung oder extremistische Inhalte beinhalten</li>
|
||||
<li>Jugendschutzbestimmungen einhalten</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
(3) Wir behalten uns vor, bei Kenntnis von rechtswidrigen Inhalten entsprechende
|
||||
Maßnahmen zu ergreifen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="verhaltensregeln" class="content-section">
|
||||
<h2>§ 5 Verhaltensregeln</h2>
|
||||
|
||||
<div class="rules-grid">
|
||||
<div class="rule-card positive">
|
||||
<h4>✅ Erwünschtes Verhalten</h4>
|
||||
<ul>
|
||||
<li>Respektvoller Umgang mit anderen Nutzern</li>
|
||||
<li>Konstruktives Feedback</li>
|
||||
<li>Melden von Bugs und Problemen</li>
|
||||
<li>Teilen von kreativen Ideen</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="rule-card negative">
|
||||
<h4>❌ Unerwünschtes Verhalten</h4>
|
||||
<ul>
|
||||
<li>Spam oder Werbung</li>
|
||||
<li>Hacking-Versuche</li>
|
||||
<li>Verbreitung falscher Informationen</li>
|
||||
<li>Belästigung anderer Nutzer</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="geistigeseigentum" class="content-section">
|
||||
<h2>§ 6 Geistiges Eigentum</h2>
|
||||
|
||||
<p>
|
||||
(1) Alle Rechte an der Plattform, dem Design, den offiziellen Spielen und dem
|
||||
Quellcode liegen bei uns bzw. unseren Lizenzgebern.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Die Plattform ist Open Source. Details zur Lizenzierung finden Sie auf unserer
|
||||
<a href="/copyright">Copyright-Seite</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(3) An den von Ihnen mit dem KI-Generator erstellten Spielen räumen Sie uns ein
|
||||
einfaches, nicht-exklusives Nutzungsrecht ein, sofern Sie diese öffentlich teilen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="haftung" class="content-section">
|
||||
<h2>§ 7 Haftungsausschluss</h2>
|
||||
|
||||
<div class="info-box">
|
||||
<p>
|
||||
(1) Die Nutzung der Plattform erfolgt auf eigene Gefahr. Wir übernehmen keine
|
||||
Gewähr für die ständige Verfügbarkeit, Fehlerfreiheit oder Vollständigkeit der
|
||||
angebotenen Dienste.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
(2) Wir haften nur für Schäden, die durch vorsätzliches oder grob fahrlässiges
|
||||
Verhalten unsererseits entstehen. Die Haftung für leichte Fahrlässigkeit ist
|
||||
ausgeschlossen, soweit keine wesentlichen Vertragspflichten verletzt werden.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(3) Für Datenverluste, insbesondere von lokal gespeicherten Spielständen oder
|
||||
selbst erstellten Spielen, übernehmen wir keine Haftung. Wir empfehlen regelmäßige
|
||||
Backups wichtiger Daten.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(4) Die Haftung für mittelbare und Folgeschäden ist ausgeschlossen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="datenschutz" class="content-section">
|
||||
<h2>§ 8 Datenschutz</h2>
|
||||
|
||||
<p>
|
||||
Der Schutz Ihrer Daten ist uns wichtig. Einzelheiten zur Datenverarbeitung finden
|
||||
Sie in unserer <a href="/datenschutz">Datenschutzerklärung</a>.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="aenderungen" class="content-section">
|
||||
<h2>§ 9 Änderungen der AGB</h2>
|
||||
|
||||
<p>
|
||||
(1) Wir behalten uns vor, diese AGB jederzeit zu ändern. Änderungen werden auf
|
||||
der Plattform bekannt gegeben.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Die weitere Nutzung der Plattform nach Bekanntgabe von Änderungen gilt als
|
||||
Zustimmung zu den geänderten Bedingungen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section id="schlussbestimmungen" class="content-section">
|
||||
<h2>§ 10 Schlussbestimmungen</h2>
|
||||
|
||||
<p>
|
||||
(1) Es gilt das Recht der Bundesrepublik Deutschland unter Ausschluss des
|
||||
UN-Kaufrechts.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(2) Sollten einzelne Bestimmungen dieser AGB unwirksam sein oder werden, berührt
|
||||
dies die Wirksamkeit der übrigen Bestimmungen nicht.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
(3) Gerichtsstand für alle Streitigkeiten aus diesem Vertragsverhältnis ist,
|
||||
soweit gesetzlich zulässig, [Ihr Ort].
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="footer-actions">
|
||||
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
|
||||
<Button href="/datenschutz" variant="ghost">Datenschutz</Button>
|
||||
<Button href="/impressum" variant="ghost">Impressum</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.agb-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.agb-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.agb-header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--color-text), var(--color-text-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.toc {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.toc h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.toc ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.toc a {
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.toc a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
scroll-margin-top: 2rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.highlight-box, .warning-box, .info-box {
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: rgba(255, 59, 48, 0.1);
|
||||
border: 1px solid rgba(255, 59, 48, 0.3);
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.highlight-box ul, .warning-box ul, .content-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.highlight-box li, .warning-box li, .content-list li {
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.highlight-box li::before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.warning-box li::before {
|
||||
content: "✗";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #ff3b30;
|
||||
}
|
||||
|
||||
.content-list li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.rules-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.rule-card.positive {
|
||||
background: rgba(0, 255, 136, 0.05);
|
||||
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||
}
|
||||
|
||||
.rule-card.negative {
|
||||
background: rgba(255, 59, 48, 0.05);
|
||||
border: 1px solid rgba(255, 59, 48, 0.2);
|
||||
}
|
||||
|
||||
.rule-card h4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rule-card ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.rule-card li {
|
||||
padding: 0.5rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.agb-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.toc {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.rules-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
356
games/mana-games/apps/web/src/pages/community.astro
Normal file
|
|
@ -0,0 +1,356 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import GameCard from '../components/GameCard.astro';
|
||||
import { games } from '../data/games';
|
||||
|
||||
// Filter community games
|
||||
const communityGames = games.filter(game => 'community' in game && game.community === true);
|
||||
|
||||
// Try to load additional community games from JSON file
|
||||
let additionalCommunityGames = [];
|
||||
// TODO: When community-games.json exists, load it here
|
||||
// For now, we'll just use an empty array until the first game is submitted
|
||||
|
||||
// Combine all community games
|
||||
const allCommunityGames = [...communityGames, ...additionalCommunityGames];
|
||||
---
|
||||
|
||||
<Layout
|
||||
title="Community Spiele - MANA Games"
|
||||
description="Von der Community erstellte Spiele"
|
||||
>
|
||||
<main>
|
||||
<div class="page-header">
|
||||
<h1>Community Spiele</h1>
|
||||
<p>Entdecke Spiele, die von unserer talentierten Community erstellt wurden!</p>
|
||||
</div>
|
||||
|
||||
<div class="community-info">
|
||||
<div class="info-card">
|
||||
<h3>🎮 Werde Teil der Community!</h3>
|
||||
<p>Hast du ein eigenes Spiel erstellt? Reiche es ein und teile es mit anderen Spielern!</p>
|
||||
<a href="/submit" class="submit-button">
|
||||
<span class="icon">📤</span>
|
||||
Spiel einreichen
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">{allCommunityGames.length}</div>
|
||||
<div class="stat-label">Community Spiele</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">🏆</div>
|
||||
<div class="stat-label">Top bewertete Spiele</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-number">👥</div>
|
||||
<div class="stat-label">Aktive Entwickler</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{allCommunityGames.length > 0 ? (
|
||||
<div class="games-section">
|
||||
<h2>Eingereichte Spiele</h2>
|
||||
<div class="games-grid">
|
||||
{allCommunityGames.map((game) => (
|
||||
<div class="community-game-card">
|
||||
<GameCard game={game} />
|
||||
{game.author && (
|
||||
<div class="author-info">
|
||||
<span class="author-label">Von:</span>
|
||||
<span class="author-name">{game.author}</span>
|
||||
</div>
|
||||
)}
|
||||
{game.submittedAt && (
|
||||
<div class="submission-date">
|
||||
Eingereicht am {new Date(game.submittedAt).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">🎯</div>
|
||||
<h2>Noch keine Community-Spiele</h2>
|
||||
<p>Sei der Erste, der ein Spiel einreicht!</p>
|
||||
<a href="/submit" class="submit-button">
|
||||
<span class="icon">📤</span>
|
||||
Erstes Spiel einreichen
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="pending-section">
|
||||
<h2>🔄 In Prüfung</h2>
|
||||
<p>Diese Spiele werden gerade von unserem Team geprüft:</p>
|
||||
<div class="pending-list" id="pendingList">
|
||||
<div class="loading">Lade ausstehende Einreichungen...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.community-info {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: #00ff88;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: #00ff88;
|
||||
color: #0a0a0a;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background: #00cc6a;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #00ff88;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.games-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.games-section h2 {
|
||||
color: #00ff88;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.games-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.community-game-card {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.author-info {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid rgba(0, 255, 136, 0.3);
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.author-label {
|
||||
color: #888;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.author-name {
|
||||
color: #00ff88;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submission-date {
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
color: #ffffff;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state p {
|
||||
color: #888;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.pending-section {
|
||||
margin-top: 3rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.pending-section h2 {
|
||||
color: #00ff88;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pending-section p {
|
||||
color: #888;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.pending-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.pending-item {
|
||||
background: #0a0a0a;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 4px;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pending-info h4 {
|
||||
color: #ffffff;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.pending-info p {
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pr-link {
|
||||
color: #00ff88;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.pr-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.games-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Fetch pending PRs from GitHub
|
||||
async function fetchPendingGames() {
|
||||
const pendingList = document.getElementById('pendingList');
|
||||
|
||||
try {
|
||||
// For now, we'll show a placeholder since we need GitHub API access
|
||||
// In production, this would fetch actual PRs
|
||||
pendingList.innerHTML = `
|
||||
<div class="pending-item">
|
||||
<div class="pending-info">
|
||||
<p>Ausstehende Einreichungen werden hier angezeigt, sobald das GitHub-Repository konfiguriert ist.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Example of what it would look like with real data:
|
||||
/*
|
||||
const response = await fetch('/.netlify/functions/get-pending-games');
|
||||
const pendingGames = await response.json();
|
||||
|
||||
if (pendingGames.length === 0) {
|
||||
pendingList.innerHTML = '<p style="color: #888; text-align: center;">Keine ausstehenden Einreichungen</p>';
|
||||
} else {
|
||||
pendingList.innerHTML = pendingGames.map(game => `
|
||||
<div class="pending-item">
|
||||
<div class="pending-info">
|
||||
<h4>${game.title}</h4>
|
||||
<p>Von ${game.author} • ${new Date(game.submittedAt).toLocaleDateString('de-DE')}</p>
|
||||
</div>
|
||||
<a href="${game.prUrl}" target="_blank" class="pr-link">
|
||||
PR #${game.prNumber} →
|
||||
</a>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
*/
|
||||
} catch (error) {
|
||||
console.error('Error fetching pending games:', error);
|
||||
pendingList.innerHTML = '<p style="color: #ff4444;">Fehler beim Laden der ausstehenden Spiele</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load pending games on page load
|
||||
fetchPendingGames();
|
||||
</script>
|
||||
707
games/mana-games/apps/web/src/pages/copyright.astro
Normal file
|
|
@ -0,0 +1,707 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
---
|
||||
|
||||
<Layout title="Copyright & Lizenzen">
|
||||
<div class="copyright-container">
|
||||
<header class="copyright-header">
|
||||
<div class="header-icon">©️</div>
|
||||
<h1>Copyright & Lizenzen</h1>
|
||||
<p class="subtitle">Open Source mit Herz</p>
|
||||
</header>
|
||||
|
||||
<section class="intro-section">
|
||||
<div class="open-source-banner">
|
||||
<div class="banner-icon">🌟</div>
|
||||
<div class="banner-content">
|
||||
<h2>100% Open Source</h2>
|
||||
<p>
|
||||
Mana Games ist ein Open-Source-Projekt. Der gesamte Quellcode ist öffentlich
|
||||
verfügbar und kann frei verwendet, modifiziert und weitergegeben werden.
|
||||
</p>
|
||||
<a href="https://github.com/yourusername/mana-games" target="_blank" rel="noopener noreferrer" class="github-button">
|
||||
<span class="icon">📦</span>
|
||||
Zum GitHub Repository
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Projekt-Lizenz</h2>
|
||||
|
||||
<div class="license-card main-license">
|
||||
<div class="license-header">
|
||||
<h3>MIT License</h3>
|
||||
<span class="license-badge">Hauptlizenz</span>
|
||||
</div>
|
||||
|
||||
<div class="license-content">
|
||||
<p>Copyright (c) 2024 [Ihr Name]</p>
|
||||
|
||||
<p>
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
</p>
|
||||
|
||||
<p>
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="license-explanation">
|
||||
<h4>Was bedeutet das für Sie?</h4>
|
||||
<ul>
|
||||
<li>✅ Sie können den Code frei verwenden</li>
|
||||
<li>✅ Sie können den Code modifizieren</li>
|
||||
<li>✅ Sie können den Code in kommerziellen Projekten nutzen</li>
|
||||
<li>✅ Sie können den Code weitergeben</li>
|
||||
<li>⚠️ Keine Garantie oder Haftung</li>
|
||||
<li>📋 Copyright-Hinweis muss erhalten bleiben</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Spiele-Lizenzen</h2>
|
||||
|
||||
<div class="games-licenses">
|
||||
<div class="license-info">
|
||||
<h3>Offizielle Spiele</h3>
|
||||
<p>
|
||||
Alle offiziellen Spiele auf unserer Plattform unterliegen ebenfalls der MIT-Lizenz.
|
||||
Sie können den Code jedes Spiels frei verwenden, studieren und modifizieren.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="license-info">
|
||||
<h3>Nutzer-generierte Spiele</h3>
|
||||
<p>
|
||||
Spiele, die mit unserem KI-Generator erstellt werden, gehören dem jeweiligen Ersteller.
|
||||
Die Ersteller können selbst entscheiden, unter welcher Lizenz sie ihre Spiele
|
||||
veröffentlichen möchten.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Verwendete Technologien & Bibliotheken</h2>
|
||||
|
||||
<div class="tech-credits">
|
||||
<div class="credit-category">
|
||||
<h3>Framework & Build-Tools</h3>
|
||||
<div class="credit-list">
|
||||
<div class="credit-item">
|
||||
<div class="credit-name">
|
||||
<strong>Astro</strong>
|
||||
<span class="version">v4.x</span>
|
||||
</div>
|
||||
<div class="credit-info">
|
||||
<span class="license">MIT License</span>
|
||||
<a href="https://astro.build" target="_blank" rel="noopener noreferrer">astro.build</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="credit-item">
|
||||
<div class="credit-name">
|
||||
<strong>TypeScript</strong>
|
||||
<span class="version">v5.x</span>
|
||||
</div>
|
||||
<div class="credit-info">
|
||||
<span class="license">Apache-2.0 License</span>
|
||||
<a href="https://www.typescriptlang.org" target="_blank" rel="noopener noreferrer">typescriptlang.org</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="credit-category">
|
||||
<h3>Deployment & Hosting</h3>
|
||||
<div class="credit-list">
|
||||
<div class="credit-item">
|
||||
<div class="credit-name">
|
||||
<strong>Netlify</strong>
|
||||
</div>
|
||||
<div class="credit-info">
|
||||
<span class="license">Hosting Service</span>
|
||||
<a href="https://www.netlify.com" target="_blank" rel="noopener noreferrer">netlify.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="credit-category">
|
||||
<h3>KI-Integration</h3>
|
||||
<div class="credit-list">
|
||||
<div class="credit-item">
|
||||
<div class="credit-name">
|
||||
<strong>OpenRouter API</strong>
|
||||
</div>
|
||||
<div class="credit-info">
|
||||
<span class="license">API Service</span>
|
||||
<a href="https://openrouter.ai" target="_blank" rel="noopener noreferrer">openrouter.ai</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Assets & Medien</h2>
|
||||
|
||||
<div class="assets-info">
|
||||
<div class="asset-category">
|
||||
<h3>Icons & Emojis</h3>
|
||||
<p>
|
||||
Wir verwenden System-Emojis, die je nach Betriebssystem unterschiedlich
|
||||
dargestellt werden können. Diese sind gemeinfrei oder unterliegen den
|
||||
jeweiligen Systemlizenzen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="asset-category">
|
||||
<h3>Schriftarten</h3>
|
||||
<p>
|
||||
Wir nutzen System-Schriftarten für optimale Performance und Lesbarkeit.
|
||||
Keine externen Schriftarten werden geladen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="asset-category">
|
||||
<h3>Grafiken</h3>
|
||||
<p>
|
||||
Alle Spielgrafiken werden programmatisch mit Canvas API erstellt.
|
||||
Es werden keine externen Bilddateien in den Spielen verwendet.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Beitragen zum Projekt</h2>
|
||||
|
||||
<div class="contribute-section">
|
||||
<h3>Wie Sie beitragen können</h3>
|
||||
|
||||
<div class="contribute-steps">
|
||||
<div class="step">
|
||||
<div class="step-number">1</div>
|
||||
<div class="step-content">
|
||||
<h4>Fork erstellen</h4>
|
||||
<p>Erstellen Sie einen Fork des Repositories auf GitHub</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">2</div>
|
||||
<div class="step-content">
|
||||
<h4>Änderungen vornehmen</h4>
|
||||
<p>Entwickeln Sie Ihre Features oder Bugfixes</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="step-number">3</div>
|
||||
<div class="step-content">
|
||||
<h4>Pull Request</h4>
|
||||
<p>Reichen Sie einen Pull Request mit Ihren Änderungen ein</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contribute-note">
|
||||
<p>
|
||||
<strong>Wichtig:</strong> Mit dem Einreichen eines Pull Requests stimmen Sie zu,
|
||||
dass Ihre Beiträge unter der MIT-Lizenz veröffentlicht werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Namensnennung & Credits</h2>
|
||||
|
||||
<div class="credits-section">
|
||||
<h3>Hauptentwickler</h3>
|
||||
<div class="developer-card">
|
||||
<p>[Ihr Name]</p>
|
||||
<p class="role">Projektinitiator & Hauptentwickler</p>
|
||||
</div>
|
||||
|
||||
<h3>Contributors</h3>
|
||||
<p>
|
||||
Eine vollständige Liste aller Contributors finden Sie auf unserer
|
||||
<a href="https://github.com/yourusername/mana-games/graphs/contributors" target="_blank" rel="noopener noreferrer">
|
||||
GitHub Contributors-Seite
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3>Besonderer Dank</h3>
|
||||
<ul class="thanks-list">
|
||||
<li>An die Open-Source-Community für ihre großartigen Tools</li>
|
||||
<li>An alle Spieler, die uns Feedback geben</li>
|
||||
<li>An alle Contributors, die das Projekt verbessern</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Rechtliche Hinweise</h2>
|
||||
|
||||
<div class="legal-notes">
|
||||
<div class="note-card">
|
||||
<h3>Markenrechte</h3>
|
||||
<p>
|
||||
"Mana Games" ist eine eingetragene Marke. Die Verwendung des Namens
|
||||
bedarf unserer ausdrücklichen Genehmigung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="note-card">
|
||||
<h3>Haftungsausschluss</h3>
|
||||
<p>
|
||||
Die Software wird "wie besehen" ohne jegliche Garantie bereitgestellt.
|
||||
Details finden Sie in der MIT-Lizenz.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="note-card">
|
||||
<h3>Externe Links</h3>
|
||||
<p>
|
||||
Wir übernehmen keine Verantwortung für die Inhalte verlinkter externer
|
||||
Websites.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="footer-actions">
|
||||
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
|
||||
<Button href="/impressum" variant="ghost">Impressum</Button>
|
||||
<Button href="/datenschutz" variant="ghost">Datenschutz</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.copyright-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.copyright-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.copyright-header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--color-text), var(--color-accent));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.open-source-banner {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(0, 255, 136, 0.05));
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
font-size: 4rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.banner-content h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.banner-content p {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.github-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-accent);
|
||||
color: #000;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
font-weight: 700;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.github-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 255, 136, 0.3);
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.license-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.main-license {
|
||||
border: 2px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.license-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.license-badge {
|
||||
background: var(--color-accent);
|
||||
color: #000;
|
||||
padding: 0.25rem 1rem;
|
||||
border-radius: 2rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.license-content {
|
||||
background: var(--color-bg);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: var(--color-text-secondary);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.license-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.license-explanation {
|
||||
background: rgba(0, 255, 136, 0.05);
|
||||
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.license-explanation ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.license-explanation li {
|
||||
padding: 0.5rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.games-licenses {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.license-info {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.tech-credits {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.credit-category {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.credit-list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.credit-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
background: var(--color-bg);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.credit-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.credit-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.license {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
color: var(--color-accent);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.credit-info a {
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.credit-info a:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.assets-info {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.asset-category {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.asset-category p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.contribute-section {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.contribute-steps {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.step {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--color-accent);
|
||||
color: #000;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 900;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-content h4 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.step-content p {
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.contribute-note {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.credits-section {
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.developer-card {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.developer-card p {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.developer-card .role {
|
||||
font-weight: 400;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.thanks-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.thanks-list li {
|
||||
padding: 0.5rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.thanks-list li::before {
|
||||
content: "❤️";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.legal-notes {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.note-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.note-card p {
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.copyright-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.open-source-banner {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.credit-item {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1582
games/mana-games/apps/web/src/pages/create.astro
Normal file
451
games/mana-games/apps/web/src/pages/datenschutz.astro
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
---
|
||||
|
||||
<Layout title="Datenschutz">
|
||||
<div class="datenschutz-container">
|
||||
<header class="datenschutz-header">
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p class="last-updated">Stand: Januar 2024</p>
|
||||
</header>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>1. Datenschutz auf einen Blick</h2>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>Allgemeine Hinweise</h3>
|
||||
<p>
|
||||
Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren
|
||||
personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene
|
||||
Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="highlight-box">
|
||||
<div class="highlight-icon">🛡️</div>
|
||||
<div class="highlight-content">
|
||||
<h4>Ihre Daten sind bei uns sicher</h4>
|
||||
<p>
|
||||
Wir erheben nur minimal notwendige Daten. Keine Tracker, keine Werbung,
|
||||
keine versteckten Datensammlungen. Ihre Privatsphäre ist uns wichtig.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>2. Datenerfassung auf dieser Website</h2>
|
||||
|
||||
<h3>Wer ist verantwortlich für die Datenerfassung?</h3>
|
||||
<p>
|
||||
Die Datenverarbeitung auf dieser Website erfolgt durch den Websitebetreiber.
|
||||
Die Kontaktdaten können Sie dem Abschnitt „Hinweis zur verantwortlichen Stelle"
|
||||
in dieser Datenschutzerklärung entnehmen.
|
||||
</p>
|
||||
|
||||
<h3>Wie erfassen wir Ihre Daten?</h3>
|
||||
<p>
|
||||
Ihre Daten werden zum einen dadurch erhoben, dass Sie uns diese mitteilen.
|
||||
Hierbei kann es sich z.B. um Daten handeln, die Sie in ein Kontaktformular eingeben.
|
||||
</p>
|
||||
<p>
|
||||
Andere Daten werden automatisch oder nach Ihrer Einwilligung beim Besuch der Website
|
||||
durch unsere IT-Systeme erfasst. Das sind vor allem technische Daten (z.B. Internetbrowser,
|
||||
Betriebssystem oder Uhrzeit des Seitenaufrufs).
|
||||
</p>
|
||||
|
||||
<h3>Wofür nutzen wir Ihre Daten?</h3>
|
||||
<p>
|
||||
Ein Teil der Daten wird erhoben, um eine fehlerfreie Bereitstellung der Website zu
|
||||
gewährleisten. Andere Daten können zur Analyse Ihres Nutzerverhaltens verwendet werden.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>3. Hosting und Content Delivery Networks (CDN)</h2>
|
||||
|
||||
<div class="service-box">
|
||||
<h3>Netlify</h3>
|
||||
<p>
|
||||
Wir hosten unsere Website bei Netlify. Anbieter ist die Netlify, Inc.,
|
||||
2325 3rd Street, Suite 296, San Francisco, CA 94107, USA.
|
||||
</p>
|
||||
<p>
|
||||
Beim Besuch unserer Website erfasst Netlify verschiedene Logfiles inklusive
|
||||
Ihrer IP-Adressen. Details entnehmen Sie der Datenschutzerklärung von Netlify:
|
||||
<a href="https://www.netlify.com/privacy/" target="_blank" rel="noopener noreferrer">
|
||||
https://www.netlify.com/privacy/
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Die Verwendung von Netlify erfolgt auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO.
|
||||
Wir haben ein berechtigtes Interesse an einer möglichst zuverlässigen Darstellung
|
||||
unserer Website.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>4. Allgemeine Hinweise und Pflichtinformationen</h2>
|
||||
|
||||
<h3>Datenschutz</h3>
|
||||
<p>
|
||||
Die Betreiber dieser Seiten nehmen den Schutz Ihrer persönlichen Daten sehr ernst.
|
||||
Wir behandeln Ihre personenbezogenen Daten vertraulich und entsprechend den
|
||||
gesetzlichen Datenschutzvorschriften sowie dieser Datenschutzerklärung.
|
||||
</p>
|
||||
|
||||
<h3>Hinweis zur verantwortlichen Stelle</h3>
|
||||
<p>Die verantwortliche Stelle für die Datenverarbeitung auf dieser Website ist:</p>
|
||||
|
||||
<div class="contact-box">
|
||||
<p>
|
||||
[Ihr Name/Firma]<br>
|
||||
[Ihre Adresse]<br>
|
||||
[PLZ und Ort]
|
||||
</p>
|
||||
<p>
|
||||
E-Mail: [Ihre E-Mail-Adresse]
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Speicherdauer</h3>
|
||||
<p>
|
||||
Soweit innerhalb dieser Datenschutzerklärung keine speziellere Speicherdauer genannt
|
||||
wurde, verbleiben Ihre personenbezogenen Daten bei uns, bis der Zweck für die
|
||||
Datenverarbeitung entfällt.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>5. Datenerfassung auf dieser Website</h2>
|
||||
|
||||
<h3>Server-Log-Dateien</h3>
|
||||
<p>
|
||||
Der Provider der Seiten erhebt und speichert automatisch Informationen in so
|
||||
genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
|
||||
</p>
|
||||
<ul class="data-list">
|
||||
<li>Browsertyp und Browserversion</li>
|
||||
<li>Verwendetes Betriebssystem</li>
|
||||
<li>Referrer URL</li>
|
||||
<li>Hostname des zugreifenden Rechners</li>
|
||||
<li>Uhrzeit der Serveranfrage</li>
|
||||
<li>IP-Adresse</li>
|
||||
</ul>
|
||||
<p>
|
||||
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.
|
||||
</p>
|
||||
|
||||
<h3>Progressive Web App (PWA)</h3>
|
||||
<p>
|
||||
Diese Website kann als Progressive Web App (PWA) installiert werden. Bei der
|
||||
Installation werden folgende Daten lokal auf Ihrem Gerät gespeichert:
|
||||
</p>
|
||||
<ul class="data-list">
|
||||
<li>App-Manifest und Icons</li>
|
||||
<li>Service Worker für Offline-Funktionalität</li>
|
||||
<li>Spielstände und Einstellungen (im localStorage)</li>
|
||||
</ul>
|
||||
<p>
|
||||
Diese Daten verbleiben ausschließlich auf Ihrem Gerät und werden nicht an uns
|
||||
oder Dritte übertragen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>6. Analyse-Tools und Werbung</h2>
|
||||
|
||||
<div class="highlight-box positive">
|
||||
<div class="highlight-icon">✅</div>
|
||||
<div class="highlight-content">
|
||||
<h4>Keine Analyse-Tools</h4>
|
||||
<p>
|
||||
Wir verwenden keine Analyse-Tools wie Google Analytics oder ähnliche Dienste.
|
||||
Ihre Nutzung unserer Website wird nicht getrackt oder analysiert.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="highlight-box positive">
|
||||
<div class="highlight-icon">🚫</div>
|
||||
<div class="highlight-content">
|
||||
<h4>Keine Werbung</h4>
|
||||
<p>
|
||||
Unsere Website ist komplett werbefrei. Wir verwenden keine Werbenetzwerke
|
||||
oder Display-Werbung jeglicher Art.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>7. Plugins und Tools</h2>
|
||||
|
||||
<h3>Keine externen Plugins</h3>
|
||||
<p>
|
||||
Wir verwenden keine Social Media Plugins, keine eingebetteten Videos von
|
||||
Drittanbietern und keine externen Schriftarten. Alle Ressourcen werden
|
||||
direkt von unserem Server ausgeliefert.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>8. Ihre Rechte</h2>
|
||||
|
||||
<h3>Sie haben folgende Rechte:</h3>
|
||||
<div class="rights-grid">
|
||||
<div class="right-card">
|
||||
<h4>Auskunftsrecht</h4>
|
||||
<p>Sie können Auskunft über Ihre gespeicherten personenbezogenen Daten verlangen.</p>
|
||||
</div>
|
||||
<div class="right-card">
|
||||
<h4>Berichtigung</h4>
|
||||
<p>Sie können die Berichtigung unrichtiger Daten verlangen.</p>
|
||||
</div>
|
||||
<div class="right-card">
|
||||
<h4>Löschung</h4>
|
||||
<p>Sie können die Löschung Ihrer personenbezogenen Daten verlangen.</p>
|
||||
</div>
|
||||
<div class="right-card">
|
||||
<h4>Einschränkung</h4>
|
||||
<p>Sie können die Einschränkung der Verarbeitung verlangen.</p>
|
||||
</div>
|
||||
<div class="right-card">
|
||||
<h4>Widerspruch</h4>
|
||||
<p>Sie können der Verarbeitung Ihrer Daten widersprechen.</p>
|
||||
</div>
|
||||
<div class="right-card">
|
||||
<h4>Datenübertragbarkeit</h4>
|
||||
<p>Sie haben das Recht auf Datenübertragbarkeit.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>9. Änderungen</h2>
|
||||
<p>
|
||||
Wir behalten uns vor, diese Datenschutzerklärung anzupassen, damit sie stets den
|
||||
aktuellen rechtlichen Anforderungen entspricht oder um Änderungen unserer Leistungen
|
||||
in der Datenschutzerklärung umzusetzen, z.B. bei der Einführung neuer Services.
|
||||
Für Ihren erneuten Besuch gilt dann die neue Datenschutzerklärung.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<div class="footer-actions">
|
||||
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.datenschutz-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.datenschutz-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.datenschutz-header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(135deg, var(--color-text), var(--color-text-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.last-updated {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h4 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
background: rgba(0, 255, 136, 0.05);
|
||||
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.highlight-box.positive {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.highlight-icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.highlight-content h4 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.highlight-content p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.service-box {
|
||||
background: var(--color-bg-secondary);
|
||||
border-left: 3px solid var(--color-accent);
|
||||
padding: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.service-box h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.contact-box {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.data-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.data-list li {
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.data-list li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.rights-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.right-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.right-card:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.right-card h4 {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.right-card p {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.datenschutz-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.rights-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1101
games/mana-games/apps/web/src/pages/games/[slug].astro
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
---
|
||||
import Layout from '../../../layouts/Layout.astro';
|
||||
import { games } from '../../../data/games';
|
||||
|
||||
export function getStaticPaths() {
|
||||
return games.map((game) => ({
|
||||
params: { slug: game.slug },
|
||||
props: { game },
|
||||
}));
|
||||
}
|
||||
|
||||
const { game } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`${game.title} - Playground`} description={`Code bearbeiten für ${game.title}`} isGamePage={true} gameTitle={`${game.title} - Playground`} gameSlug={game.slug} isPlayground={true} hideFooter={true}>
|
||||
<div class="playground-page">
|
||||
<div class="playground-container">
|
||||
<div class="editor-panel">
|
||||
<div class="editor-header">
|
||||
<h3>Code Editor</h3>
|
||||
<div class="editor-actions">
|
||||
<button id="resetBtn" class="editor-btn">
|
||||
<span class="icon">↺</span>
|
||||
Reset
|
||||
</button>
|
||||
<button id="runBtn" class="editor-btn primary">
|
||||
<span class="icon">▶</span>
|
||||
Ausführen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editor" class="code-editor"></div>
|
||||
</div>
|
||||
|
||||
<div class="preview-panel">
|
||||
<div class="preview-header">
|
||||
<h3>Vorschau</h3>
|
||||
<button id="fullscreenPreviewBtn" class="editor-btn">
|
||||
<span class="icon">⛶</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="preview-frame">
|
||||
<iframe
|
||||
id="preview"
|
||||
title={`${game.title} Preview`}
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="loadingOverlay" class="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
<p>Code wird geladen...</p>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Add no-scroll class to body
|
||||
document.body.classList.add('no-scroll');
|
||||
|
||||
// Import CodeMirror CSS
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css';
|
||||
document.head.appendChild(link);
|
||||
|
||||
const themeLink = document.createElement('link');
|
||||
themeLink.rel = 'stylesheet';
|
||||
themeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/monokai.min.css';
|
||||
document.head.appendChild(themeLink);
|
||||
|
||||
// Import CodeMirror
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js';
|
||||
document.head.appendChild(script);
|
||||
|
||||
script.onload = () => {
|
||||
// Load modes
|
||||
const modes = ['xml', 'javascript', 'css', 'htmlmixed'];
|
||||
let loadedModes = 0;
|
||||
|
||||
modes.forEach(mode => {
|
||||
const modeScript = document.createElement('script');
|
||||
modeScript.src = `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/${mode}/${mode}.min.js`;
|
||||
document.head.appendChild(modeScript);
|
||||
modeScript.onload = () => {
|
||||
loadedModes++;
|
||||
if (loadedModes === modes.length) {
|
||||
initializePlayground();
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
async function initializePlayground() {
|
||||
const gameUrl = window.location.pathname.replace('/playground', '');
|
||||
const htmlFile = document.querySelector<HTMLElement>('[data-game-file]')?.dataset.gameFile || `/games/${gameUrl.split('/').pop()}_game.html`;
|
||||
|
||||
const editor = document.getElementById('editor');
|
||||
const preview = document.getElementById('preview') as HTMLIFrameElement;
|
||||
const runBtn = document.getElementById('runBtn');
|
||||
const resetBtn = document.getElementById('resetBtn');
|
||||
const fullscreenPreviewBtn = document.getElementById('fullscreenPreviewBtn');
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
|
||||
let originalCode = '';
|
||||
let cm: any;
|
||||
|
||||
try {
|
||||
// Fetch the game HTML
|
||||
const response = await fetch(htmlFile);
|
||||
originalCode = await response.text();
|
||||
|
||||
// Initialize CodeMirror
|
||||
cm = (window as any).CodeMirror(editor, {
|
||||
value: originalCode,
|
||||
mode: 'htmlmixed',
|
||||
theme: 'monokai',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
autofocus: true,
|
||||
viewportMargin: Infinity
|
||||
});
|
||||
|
||||
// Force CodeMirror to fill the container
|
||||
setTimeout(() => {
|
||||
cm.setSize('100%', '100%');
|
||||
cm.refresh();
|
||||
}, 100);
|
||||
|
||||
// Initial preview
|
||||
updatePreview(originalCode);
|
||||
|
||||
// Hide loading overlay
|
||||
loadingOverlay?.classList.add('hidden');
|
||||
} catch (error) {
|
||||
console.error('Error loading game:', error);
|
||||
loadingOverlay!.innerHTML = '<p>Fehler beim Laden des Spiels</p>';
|
||||
}
|
||||
|
||||
// Run button
|
||||
runBtn?.addEventListener('click', () => {
|
||||
const code = cm.getValue();
|
||||
updatePreview(code);
|
||||
showNotification('Code ausgeführt!', 'success');
|
||||
});
|
||||
|
||||
// Reset button
|
||||
resetBtn?.addEventListener('click', () => {
|
||||
if (confirm('Möchtest du wirklich alle Änderungen zurücksetzen?')) {
|
||||
cm.setValue(originalCode);
|
||||
updatePreview(originalCode);
|
||||
showNotification('Code zurückgesetzt!', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
// Fullscreen preview
|
||||
fullscreenPreviewBtn?.addEventListener('click', () => {
|
||||
if (preview.requestFullscreen) {
|
||||
preview.requestFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
cm.refresh();
|
||||
});
|
||||
|
||||
// Auto-save to localStorage
|
||||
cm.on('change', () => {
|
||||
const code = cm.getValue();
|
||||
localStorage.setItem(`playground_${gameUrl}`, code);
|
||||
});
|
||||
|
||||
// Load from localStorage if available
|
||||
const savedCode = localStorage.getItem(`playground_${gameUrl}`);
|
||||
if (savedCode && savedCode !== originalCode) {
|
||||
if (confirm('Es gibt gespeicherte Änderungen. Möchtest du diese laden?')) {
|
||||
cm.setValue(savedCode);
|
||||
updatePreview(savedCode);
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreview(code: string) {
|
||||
const blob = new Blob([code], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
preview.src = url;
|
||||
}
|
||||
}
|
||||
|
||||
function showNotification(message: string, type: 'success' | 'info' | 'error' = 'info') {
|
||||
const notification = document.createElement('div');
|
||||
notification.className = `notification notification-${type}`;
|
||||
notification.textContent = message;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => notification.classList.add('show'), 10);
|
||||
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('show');
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 2000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script define:vars={{ gameFile: game.htmlFile }}>
|
||||
// Pass game file to the script
|
||||
document.body.dataset.gameFile = gameFile;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.playground-page {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.playground-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
height: 100%;
|
||||
gap: 1px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
|
||||
.editor-panel,
|
||||
.preview-panel {
|
||||
background: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-header,
|
||||
.preview-header {
|
||||
background: var(--color-bg-secondary);
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.editor-header h3,
|
||||
.preview-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.editor-btn {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.editor-btn:hover {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.editor-btn.primary {
|
||||
background: var(--color-accent);
|
||||
color: #000;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.editor-btn.primary:hover {
|
||||
background: var(--color-accent-secondary);
|
||||
}
|
||||
|
||||
.editor-btn .icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.code-editor {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* CodeMirror overrides */
|
||||
.code-editor .CodeMirror {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.preview-frame {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.preview-frame iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Loading overlay */
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-bg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.loading-overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Notifications */
|
||||
.notification {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
background: var(--color-bg-secondary);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
z-index: 2000;
|
||||
transition: transform 0.3s ease;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.notification-success {
|
||||
border: 1px solid var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.notification-info {
|
||||
border: 1px solid #3b82f6;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.notification-error {
|
||||
border: 1px solid #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.playground-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-header h3::after {
|
||||
content: ' (Vorschau ausgeblendet)';
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
434
games/mana-games/apps/web/src/pages/impressum.astro
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
---
|
||||
|
||||
<Layout title="Impressum">
|
||||
<div class="impressum-container">
|
||||
<header class="impressum-header">
|
||||
<h1>Impressum</h1>
|
||||
<p class="subtitle">Angaben gemäß § 5 TMG</p>
|
||||
</header>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Verantwortlich für den Inhalt</h2>
|
||||
<div class="contact-card">
|
||||
<div class="contact-icon">👤</div>
|
||||
<div class="contact-info">
|
||||
<p class="name">[Ihr Name]</p>
|
||||
<p>[Ihre Straße und Hausnummer]</p>
|
||||
<p>[PLZ und Ort]</p>
|
||||
<p>Deutschland</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Kontakt</h2>
|
||||
<div class="contact-grid">
|
||||
<div class="contact-item">
|
||||
<span class="icon">📧</span>
|
||||
<div>
|
||||
<h4>E-Mail</h4>
|
||||
<p>[ihre-email@beispiel.de]</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<span class="icon">📱</span>
|
||||
<div>
|
||||
<h4>Telefon</h4>
|
||||
<p>[+49 123 456789]</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Umsatzsteuer-ID</h2>
|
||||
<p>
|
||||
Umsatzsteuer-Identifikationsnummer gemäß §27 a Umsatzsteuergesetz:
|
||||
</p>
|
||||
<div class="highlight-box">
|
||||
<code>DE[IHRE-UST-ID]</code>
|
||||
</div>
|
||||
<p class="note">
|
||||
Falls Sie keine Umsatzsteuer-ID haben, können Sie diesen Abschnitt entfernen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Verantwortlich für den Inhalt nach § 55 Abs. 2 RStV</h2>
|
||||
<div class="responsible-box">
|
||||
<p>[Ihr Name]</p>
|
||||
<p>[Ihre Adresse]</p>
|
||||
<p>[PLZ und Ort]</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>EU-Streitschlichtung</h2>
|
||||
<p>
|
||||
Die Europäische Kommission stellt eine Plattform zur Online-Streitbeilegung (OS) bereit:
|
||||
</p>
|
||||
<div class="link-box">
|
||||
<a href="https://ec.europa.eu/consumers/odr/" target="_blank" rel="noopener noreferrer">
|
||||
https://ec.europa.eu/consumers/odr/
|
||||
</a>
|
||||
</div>
|
||||
<p>
|
||||
Unsere E-Mail-Adresse finden Sie oben im Impressum.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Verbraucherstreitbeilegung / Universalschlichtungsstelle</h2>
|
||||
<p>
|
||||
Wir sind nicht bereit oder verpflichtet, an Streitbeilegungsverfahren vor einer
|
||||
Verbraucherschlichtungsstelle teilzunehmen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Haftungsausschluss (Disclaimer)</h2>
|
||||
|
||||
<h3>Haftung für Inhalte</h3>
|
||||
<p>
|
||||
Als Diensteanbieter sind wir gemäß § 7 Abs.1 TMG für eigene Inhalte auf diesen
|
||||
Seiten nach den allgemeinen Gesetzen verantwortlich. Nach §§ 8 bis 10 TMG sind
|
||||
wir als Diensteanbieter jedoch nicht verpflichtet, übermittelte oder gespeicherte
|
||||
fremde Informationen zu überwachen oder nach Umständen zu forschen, die auf eine
|
||||
rechtswidrige Tätigkeit hinweisen.
|
||||
</p>
|
||||
<p>
|
||||
Verpflichtungen zur Entfernung oder Sperrung der Nutzung von Informationen nach
|
||||
den allgemeinen Gesetzen bleiben hiervon unberührt. Eine diesbezügliche Haftung
|
||||
ist jedoch erst ab dem Zeitpunkt der Kenntnis einer konkreten Rechtsverletzung
|
||||
möglich. Bei Bekanntwerden von entsprechenden Rechtsverletzungen werden wir diese
|
||||
Inhalte umgehend entfernen.
|
||||
</p>
|
||||
|
||||
<h3>Haftung für Links</h3>
|
||||
<p>
|
||||
Unser Angebot enthält Links zu externen Websites Dritter, auf deren Inhalte wir
|
||||
keinen Einfluss haben. Deshalb können wir für diese fremden Inhalte auch keine
|
||||
Gewähr übernehmen. Für die Inhalte der verlinkten Seiten ist stets der jeweilige
|
||||
Anbieter oder Betreiber der Seiten verantwortlich. Die verlinkten Seiten wurden
|
||||
zum Zeitpunkt der Verlinkung auf mögliche Rechtsverstöße überprüft. Rechtswidrige
|
||||
Inhalte waren zum Zeitpunkt der Verlinkung nicht erkennbar.
|
||||
</p>
|
||||
<p>
|
||||
Eine permanente inhaltliche Kontrolle der verlinkten Seiten ist jedoch ohne konkrete
|
||||
Anhaltspunkte einer Rechtsverletzung nicht zumutbar. Bei Bekanntwerden von
|
||||
Rechtsverletzungen werden wir derartige Links umgehend entfernen.
|
||||
</p>
|
||||
|
||||
<h3>Urheberrecht</h3>
|
||||
<p>
|
||||
Die durch die Seitenbetreiber erstellten Inhalte und Werke auf diesen Seiten
|
||||
unterliegen dem deutschen Urheberrecht. Die Vervielfältigung, Bearbeitung,
|
||||
Verbreitung und jede Art der Verwertung außerhalb der Grenzen des Urheberrechtes
|
||||
bedürfen der schriftlichen Zustimmung des jeweiligen Autors bzw. Erstellers.
|
||||
Downloads und Kopien dieser Seite sind nur für den privaten, nicht kommerziellen
|
||||
Gebrauch gestattet.
|
||||
</p>
|
||||
<p>
|
||||
Soweit die Inhalte auf dieser Seite nicht vom Betreiber erstellt wurden, werden
|
||||
die Urheberrechte Dritter beachtet. Insbesondere werden Inhalte Dritter als solche
|
||||
gekennzeichnet. Sollten Sie trotzdem auf eine Urheberrechtsverletzung aufmerksam
|
||||
werden, bitten wir um einen entsprechenden Hinweis. Bei Bekanntwerden von
|
||||
Rechtsverletzungen werden wir derartige Inhalte umgehend entfernen.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Open Source Hinweis</h2>
|
||||
<div class="opensource-box">
|
||||
<div class="opensource-icon">💻</div>
|
||||
<div class="opensource-content">
|
||||
<h4>Diese Website ist Open Source</h4>
|
||||
<p>
|
||||
Der Quellcode dieser Website ist öffentlich verfügbar. Sie finden das Repository auf:
|
||||
</p>
|
||||
<a href="https://github.com/yourusername/mana-games" target="_blank" rel="noopener noreferrer" class="github-link">
|
||||
<span class="icon">📦</span>
|
||||
GitHub Repository
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="footer-actions">
|
||||
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
|
||||
<Button href="/datenschutz" variant="ghost">Datenschutz</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.impressum-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.impressum-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.impressum-header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--color-text), var(--color-text-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
font-size: 3rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.contact-info p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.contact-info .name {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.contact-item .icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-item h4 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.contact-item p {
|
||||
margin-bottom: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.highlight-box {
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.highlight-box code {
|
||||
color: var(--color-accent);
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.responsible-box {
|
||||
background: var(--color-bg-secondary);
|
||||
border-left: 3px solid var(--color-accent);
|
||||
padding: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.responsible-box p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.link-box {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.link-box a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.link-box a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.opensource-box {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
|
||||
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.opensource-icon {
|
||||
font-size: 3rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.opensource-content h4 {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.opensource-content p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.github-link:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.impressum-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.opensource-box {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
778
games/mana-games/apps/web/src/pages/index.astro
Normal file
|
|
@ -0,0 +1,778 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import GameCard from '../components/GameCard.astro';
|
||||
import MyGamesSection from '../components/MyGamesSection.astro';
|
||||
import HorizontalScroller from '../components/HorizontalScroller.astro';
|
||||
import { games } from '../data/games';
|
||||
|
||||
// Filtere offizielle Spiele (ohne community flag)
|
||||
const officialGames = games.filter(game => !game.community);
|
||||
|
||||
// Kategorisiere Spiele nach Genres für verschiedene Scroller
|
||||
const arcadeGames = officialGames.filter(game => game.tags.includes('Arcade'));
|
||||
const puzzleGames = officialGames.filter(game => game.tags.includes('Puzzle'));
|
||||
const actionGames = officialGames.filter(game =>
|
||||
game.tags.includes('Action') || game.tags.includes('Shooter') || game.tags.includes('Jump n Run')
|
||||
);
|
||||
|
||||
// Sortiere nach Beliebtheit/Komplexität
|
||||
const featuredGames = [...officialGames].slice(0, 8);
|
||||
---
|
||||
|
||||
<Layout title="Startseite" fullWidth={true}>
|
||||
<section class="hero">
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
<span class="line line-1">
|
||||
<span id="changingWord">Spiele</span>
|
||||
<span class="line line-2">ohne Grenzen</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="hero-visual">
|
||||
<div class="floating-squares">
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
<div class="square"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="stats-section">
|
||||
<div class="stats-container">
|
||||
<div class="stat">
|
||||
<span class="stat-number">{officialGames.length}</span>
|
||||
<span class="stat-label">Spiele</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">100%</span>
|
||||
<span class="stat-label">Kostenlos</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat">
|
||||
<span class="stat-number">100%</span>
|
||||
<span class="stat-label">Werbefrei</span>
|
||||
</div>
|
||||
<div class="stat-divider"></div>
|
||||
<a href="/stats" class="stat stat-link">
|
||||
<span class="stat-number">📊</span>
|
||||
<span class="stat-label">Meine Stats</span>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Official Games - Netflix Style -->
|
||||
<HorizontalScroller
|
||||
title="Offizielle Mana Games"
|
||||
games={featuredGames}
|
||||
id="featured-scroller"
|
||||
/>
|
||||
|
||||
<!-- My Games Section -->
|
||||
<MyGamesSection maxGames={4} />
|
||||
|
||||
<!-- Genre-basierte Scroller -->
|
||||
{arcadeGames.length > 0 && (
|
||||
<HorizontalScroller
|
||||
title="Arcade Spiele"
|
||||
games={arcadeGames}
|
||||
id="arcade-scroller"
|
||||
/>
|
||||
)}
|
||||
|
||||
{puzzleGames.length > 0 && (
|
||||
<HorizontalScroller
|
||||
title="Puzzle & Denkspiele"
|
||||
games={puzzleGames}
|
||||
id="puzzle-scroller"
|
||||
/>
|
||||
)}
|
||||
|
||||
{actionGames.length > 0 && (
|
||||
<HorizontalScroller
|
||||
title="Action & Adventure"
|
||||
games={actionGames}
|
||||
id="action-scroller"
|
||||
/>
|
||||
)}
|
||||
|
||||
<section class="games-section">
|
||||
<div class="section-header">
|
||||
<h2>Alle Spiele durchsuchen</h2>
|
||||
<div class="filter-tabs">
|
||||
<button class="filter-tab active" data-filter="all">
|
||||
Alle
|
||||
</button>
|
||||
<button class="filter-tab" data-filter="official">
|
||||
Offizielle
|
||||
</button>
|
||||
<button class="filter-tab" data-filter="my-games">
|
||||
Meine Spiele
|
||||
</button>
|
||||
<button class="filter-tab" data-filter="community">
|
||||
Community
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="officialGames" class="games-grid">
|
||||
{officialGames.map((game, index) => (
|
||||
<div class="game-wrapper" style={`--delay: ${0.4 + index * 0.1}s`}>
|
||||
<GameCard
|
||||
title={game.title}
|
||||
description={game.description}
|
||||
slug={game.slug}
|
||||
thumbnail={game.thumbnail}
|
||||
tags={game.tags}
|
||||
complexity={game.complexity}
|
||||
codeStats={game.codeStats}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div id="myGamesGrid" class="games-grid hidden">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<div id="communityGamesGrid" class="games-grid hidden">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<div id="allGamesGrid" class="games-grid hidden">
|
||||
<!-- Will be populated by JavaScript -->
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.hero {
|
||||
position: relative;
|
||||
min-height: 30vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2.5rem, 7vw, 4.5rem);
|
||||
font-weight: 900;
|
||||
line-height: 0.85;
|
||||
margin-bottom: 1rem;
|
||||
letter-spacing: -0.05em;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: inline;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.4s ease forwards;
|
||||
}
|
||||
|
||||
.line-1 {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.line-2 {
|
||||
animation-delay: 0.1s;
|
||||
background: linear-gradient(90deg, var(--color-accent), var(--color-accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
|
||||
#changingWord {
|
||||
display: inline-block;
|
||||
min-width: 3em;
|
||||
text-align: right;
|
||||
position: relative;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.word-fade-out {
|
||||
animation: fadeOut 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.word-fade-in {
|
||||
animation: fadeIn 0.3s ease-in forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Floating visual elements */
|
||||
.hero-visual {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.floating-squares {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.square {
|
||||
position: absolute;
|
||||
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.square:nth-child(1) {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
transform: rotate(15deg);
|
||||
}
|
||||
|
||||
.square:nth-child(2) {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: 50%;
|
||||
right: 10%;
|
||||
transform: rotate(-20deg);
|
||||
}
|
||||
|
||||
.square:nth-child(3) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
bottom: 10%;
|
||||
left: 50%;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Stats Section */
|
||||
.stats-section {
|
||||
margin-top: -1rem;
|
||||
margin-bottom: 2.5rem;
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
animation: fadeInUp 0.4s ease 0.3s forwards;
|
||||
}
|
||||
|
||||
.stats-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 900;
|
||||
color: var(--color-accent);
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
background: linear-gradient(to bottom, transparent, rgba(255,255,255,0.1), transparent);
|
||||
}
|
||||
|
||||
.stat-link {
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat-link:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stat-link .stat-number {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.stat-link:hover .stat-number {
|
||||
transform: scale(1.1);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* Games Section */
|
||||
.games-section {
|
||||
position: relative;
|
||||
margin-top: 4rem;
|
||||
margin-bottom: 6rem;
|
||||
padding-top: 3rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-surface);
|
||||
padding: 0.25rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-accent);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.games-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.game-wrapper {
|
||||
opacity: 0;
|
||||
transform: translateY(15px);
|
||||
animation: fadeInUp 0.3s ease var(--delay) forwards;
|
||||
}
|
||||
|
||||
/* Minimal grid line decoration */
|
||||
.games-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -3rem;
|
||||
left: 0;
|
||||
width: 100px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--color-accent), transparent);
|
||||
}
|
||||
|
||||
/* Generated game cards styling */
|
||||
.my-game-card {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.my-game-card:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 4px 12px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.my-game-card .card-thumbnail {
|
||||
aspect-ratio: 16/9;
|
||||
background: var(--color-bg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.my-game-card .card-thumbnail img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.my-game-card .placeholder-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 3rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.my-game-card .card-content {
|
||||
padding: 1.5rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.my-game-card .card-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0 0 0.5rem 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.my-game-card .card-description {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin: 0 0 1rem 0;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.my-game-card .card-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.my-game-card .complexity-badge {
|
||||
background: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.my-game-card .creation-date {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero {
|
||||
min-height: 25vh;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(2rem, 9vw, 3rem);
|
||||
}
|
||||
|
||||
|
||||
.stats-container {
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-top: -0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.games-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.square {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Wörter-Animation für den Hero-Text
|
||||
const words = ['Spiele', 'Baue', 'Lerne'];
|
||||
let currentWordIndex = 0;
|
||||
const changingWord = document.getElementById('changingWord');
|
||||
|
||||
function changeWord() {
|
||||
// Fade out
|
||||
changingWord.classList.add('word-fade-out');
|
||||
|
||||
setTimeout(() => {
|
||||
// Wechsle zum nächsten Wort
|
||||
currentWordIndex = (currentWordIndex + 1) % words.length;
|
||||
changingWord.textContent = words[currentWordIndex];
|
||||
|
||||
// Fade in
|
||||
changingWord.classList.remove('word-fade-out');
|
||||
changingWord.classList.add('word-fade-in');
|
||||
|
||||
setTimeout(() => {
|
||||
changingWord.classList.remove('word-fade-in');
|
||||
}, 300);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Starte Animation nach 2 Sekunden, dann alle 3 Sekunden
|
||||
setTimeout(() => {
|
||||
changeWord();
|
||||
setInterval(changeWord, 3000);
|
||||
}, 2000);
|
||||
|
||||
// Filter functionality
|
||||
const filterTabs = document.querySelectorAll('.filter-tab');
|
||||
const officialGames = document.getElementById('officialGames');
|
||||
const myGamesGrid = document.getElementById('myGamesGrid');
|
||||
const communityGamesGrid = document.getElementById('communityGamesGrid');
|
||||
const allGamesGrid = document.getElementById('allGamesGrid');
|
||||
|
||||
// GameStorage class (simplified version for reading)
|
||||
class GameStorage {
|
||||
private dbName = 'ManaGamesDB';
|
||||
private storeName = 'generatedGames';
|
||||
private db: IDBDatabase | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||
store.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
store.createIndex('title', 'title', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getAllGames(): Promise<any[]> {
|
||||
if (!this.db) await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db!.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onsuccess = () => resolve(request.result || []);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const gameStorage = new GameStorage();
|
||||
|
||||
// Load and display my games
|
||||
async function loadMyGames() {
|
||||
try {
|
||||
const myGames = await gameStorage.getAllGames();
|
||||
|
||||
if (myGames.length === 0) {
|
||||
myGamesGrid.innerHTML = `
|
||||
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
|
||||
<p style="color: var(--color-text-secondary); font-size: 1.1rem;">
|
||||
Du hast noch keine eigenen Spiele erstellt
|
||||
</p>
|
||||
<a href="/create" style="display: inline-block; margin-top: 1rem; background: var(--color-accent); color: var(--color-bg); padding: 0.75rem 2rem; border-radius: 8px; text-decoration: none; font-weight: 600;">
|
||||
Erstelle dein erstes Spiel
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort by creation date (newest first)
|
||||
myGames.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||
|
||||
// Create game cards
|
||||
const gameCards = myGames.map((game, index) => {
|
||||
const date = new Date(game.createdAt).toLocaleDateString('de-DE');
|
||||
return `
|
||||
<div class="game-wrapper" style="--delay: ${0.4 + index * 0.1}s">
|
||||
<div class="game-card my-game-card" onclick="window.location.href='/play-generated?id=${game.id}'">
|
||||
<div class="card-thumbnail">
|
||||
${game.thumbnail
|
||||
? `<img src="${game.thumbnail}" alt="${game.title}" />`
|
||||
: `<div class="placeholder-thumbnail">🎮</div>`
|
||||
}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<h3 class="card-title">${game.title}</h3>
|
||||
<p class="card-description">${game.description || game.prompt}</p>
|
||||
<div class="card-meta">
|
||||
<span class="complexity-badge">Generiert</span>
|
||||
<span class="creation-date">${date}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
myGamesGrid.innerHTML = gameCards;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading my games:', error);
|
||||
myGamesGrid.innerHTML = '<p style="color: #ef4444;">Fehler beim Laden der Spiele</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load community games
|
||||
async function loadCommunityGames() {
|
||||
// For now, show a placeholder - will be populated when community games are added
|
||||
communityGamesGrid.innerHTML = `
|
||||
<div style="grid-column: 1 / -1; text-align: center; padding: 3rem;">
|
||||
<p style="color: var(--color-text-secondary); font-size: 1.1rem;">
|
||||
Noch keine Community-Spiele verfügbar
|
||||
</p>
|
||||
<a href="/submit" style="display: inline-block; margin-top: 1rem; background: var(--color-accent); color: var(--color-bg); padding: 0.75rem 2rem; border-radius: 8px; text-decoration: none; font-weight: 600;">
|
||||
Reiche dein Spiel ein
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Merge official and my games for "All" view
|
||||
async function loadAllGames() {
|
||||
const officialGamesHTML = officialGames.innerHTML;
|
||||
const myGames = await gameStorage.getAllGames();
|
||||
|
||||
// Clone official games
|
||||
allGamesGrid.innerHTML = officialGamesHTML;
|
||||
|
||||
// Add my games if any
|
||||
if (myGames.length > 0) {
|
||||
const myGamesHTML = myGamesGrid.innerHTML;
|
||||
allGamesGrid.innerHTML = officialGamesHTML + myGamesHTML;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter tab click handlers
|
||||
filterTabs.forEach(tab => {
|
||||
tab.addEventListener('click', async () => {
|
||||
// Update active tab
|
||||
filterTabs.forEach(t => t.classList.remove('active'));
|
||||
tab.classList.add('active');
|
||||
|
||||
const filter = tab.getAttribute('data-filter');
|
||||
|
||||
// Show/hide appropriate grids
|
||||
officialGames.classList.add('hidden');
|
||||
myGamesGrid.classList.add('hidden');
|
||||
communityGamesGrid.classList.add('hidden');
|
||||
allGamesGrid.classList.add('hidden');
|
||||
|
||||
switch(filter) {
|
||||
case 'official':
|
||||
officialGames.classList.remove('hidden');
|
||||
break;
|
||||
case 'my-games':
|
||||
if (myGamesGrid.innerHTML === '') {
|
||||
await loadMyGames();
|
||||
}
|
||||
myGamesGrid.classList.remove('hidden');
|
||||
break;
|
||||
case 'community':
|
||||
if (communityGamesGrid.innerHTML === '') {
|
||||
await loadCommunityGames();
|
||||
}
|
||||
communityGamesGrid.classList.remove('hidden');
|
||||
break;
|
||||
case 'all':
|
||||
default:
|
||||
if (allGamesGrid.innerHTML === '') {
|
||||
await loadAllGames();
|
||||
}
|
||||
allGamesGrid.classList.remove('hidden');
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize with "All" view
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// Trigger click on "All" tab to load combined view
|
||||
const allTab = document.querySelector('[data-filter="all"]');
|
||||
if (allTab) {
|
||||
(allTab as HTMLElement).click();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
601
games/mana-games/apps/web/src/pages/jugendschutz.astro
Normal file
|
|
@ -0,0 +1,601 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
---
|
||||
|
||||
<Layout title="Jugendschutz">
|
||||
<div class="jugendschutz-container">
|
||||
<header class="jugendschutz-header">
|
||||
<div class="header-icon">🛡️</div>
|
||||
<h1>Jugendschutz bei Mana Games</h1>
|
||||
<p class="subtitle">Sicher spielen für alle Altersgruppen</p>
|
||||
</header>
|
||||
|
||||
<section class="intro-section">
|
||||
<div class="intro-card">
|
||||
<p>
|
||||
Bei Mana Games liegt uns die Sicherheit und das Wohlbefinden junger Spieler besonders
|
||||
am Herzen. Unsere Plattform ist so gestaltet, dass Kinder und Jugendliche sicher und
|
||||
altersgerecht spielen können.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Unsere Jugendschutz-Prinzipien</h2>
|
||||
|
||||
<div class="principles-grid">
|
||||
<div class="principle-card">
|
||||
<div class="card-icon">🎮</div>
|
||||
<h3>Altersgerechte Inhalte</h3>
|
||||
<p>
|
||||
Alle unsere Spiele sind familienfreundlich und enthalten keine Gewalt,
|
||||
explizite Inhalte oder verstörende Elemente.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="principle-card">
|
||||
<div class="card-icon">🚫</div>
|
||||
<h3>Keine Werbung</h3>
|
||||
<p>
|
||||
Unsere Plattform ist komplett werbefrei. Kinder werden nicht mit
|
||||
kommerziellen Inhalten oder In-App-Käufen konfrontiert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="principle-card">
|
||||
<div class="card-icon">🔒</div>
|
||||
<h3>Datenschutz</h3>
|
||||
<p>
|
||||
Wir sammeln keine persönlichen Daten von Kindern. Alle Spielstände
|
||||
werden nur lokal im Browser gespeichert.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="principle-card">
|
||||
<div class="card-icon">💬</div>
|
||||
<h3>Kein Chat</h3>
|
||||
<p>
|
||||
Es gibt keine Chat-Funktionen oder soziale Features, die eine
|
||||
Kontaktaufnahme zwischen Nutzern ermöglichen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Altersempfehlungen</h2>
|
||||
|
||||
<div class="age-recommendations">
|
||||
<div class="age-group">
|
||||
<div class="age-badge" style="--badge-color: #4ade80;">0-6 Jahre</div>
|
||||
<h4>Vorschulalter</h4>
|
||||
<p>Einfache Spiele mit großen Buttons und klaren visuellen Elementen:</p>
|
||||
<ul>
|
||||
<li>Memory-Spiele</li>
|
||||
<li>Einfache Puzzle</li>
|
||||
<li>Farb- und Formspiele</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="age-group">
|
||||
<div class="age-badge" style="--badge-color: #22d3ee;">6-12 Jahre</div>
|
||||
<h4>Grundschulalter</h4>
|
||||
<p>Spiele die Geschicklichkeit und logisches Denken fördern:</p>
|
||||
<ul>
|
||||
<li>Jump'n'Run Spiele</li>
|
||||
<li>Einfache Strategiespiele</li>
|
||||
<li>Lernspiele</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="age-group">
|
||||
<div class="age-badge" style="--badge-color: #a78bfa;">12+ Jahre</div>
|
||||
<h4>Jugendliche</h4>
|
||||
<p>Komplexere Spiele mit anspruchsvollen Herausforderungen:</p>
|
||||
<ul>
|
||||
<li>Tower Defense</li>
|
||||
<li>Komplexe Puzzle</li>
|
||||
<li>Strategiespiele</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Hinweise für Eltern</h2>
|
||||
|
||||
<div class="parent-tips">
|
||||
<div class="tip-card">
|
||||
<h3>🕐 Spielzeiten begrenzen</h3>
|
||||
<p>
|
||||
Auch wenn unsere Spiele pädagogisch wertvoll sind, empfehlen wir
|
||||
altersgerechte Bildschirmzeiten einzuhalten.
|
||||
</p>
|
||||
<div class="time-recommendations">
|
||||
<div class="time-item">
|
||||
<strong>3-6 Jahre:</strong> max. 30 Minuten täglich
|
||||
</div>
|
||||
<div class="time-item">
|
||||
<strong>6-9 Jahre:</strong> max. 1 Stunde täglich
|
||||
</div>
|
||||
<div class="time-item">
|
||||
<strong>10+ Jahre:</strong> max. 2 Stunden täglich
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip-card">
|
||||
<h3>👨👩👧 Gemeinsam spielen</h3>
|
||||
<p>
|
||||
Nutzen Sie die Gelegenheit, mit Ihren Kindern gemeinsam zu spielen.
|
||||
Das fördert nicht nur die Bindung, sondern ermöglicht auch Gespräche
|
||||
über das Spielerlebnis.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="tip-card">
|
||||
<h3>🎯 Altersgerechte Auswahl</h3>
|
||||
<p>
|
||||
Achten Sie auf die Komplexitätsstufen unserer Spiele:
|
||||
</p>
|
||||
<ul class="complexity-list">
|
||||
<li><span class="badge minimal">Minimal</span> - Für die Kleinsten</li>
|
||||
<li><span class="badge einfach">Einfach</span> - Ab Grundschulalter</li>
|
||||
<li><span class="badge mittel">Mittel</span> - Für erfahrene Spieler</li>
|
||||
<li><span class="badge komplex">Komplex</span> - Herausfordernd</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>KI-Generator und Jugendschutz</h2>
|
||||
|
||||
<div class="ai-safety">
|
||||
<div class="safety-info">
|
||||
<h3>Sichere KI-Nutzung</h3>
|
||||
<p>
|
||||
Unser KI-Spielegenerator verfügt über eingebaute Sicherheitsmechanismen:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Filterung ungeeigneter Begriffe und Themen</li>
|
||||
<li>Automatische Prüfung generierter Inhalte</li>
|
||||
<li>Keine Generierung von gewalttätigen oder ungeeigneten Spielen</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="warning-box">
|
||||
<h4>⚠️ Empfehlung</h4>
|
||||
<p>
|
||||
Wir empfehlen, dass Kinder unter 12 Jahren den KI-Generator nur
|
||||
unter Aufsicht von Erwachsenen nutzen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Technische Schutzmaßnahmen</h2>
|
||||
|
||||
<div class="tech-measures">
|
||||
<div class="measure">
|
||||
<div class="measure-icon">🌐</div>
|
||||
<div class="measure-content">
|
||||
<h4>Keine externen Links</h4>
|
||||
<p>Unsere Spiele enthalten keine Links zu externen Websites.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="measure">
|
||||
<div class="measure-icon">📵</div>
|
||||
<div class="measure-content">
|
||||
<h4>Offline spielbar</h4>
|
||||
<p>Nach dem ersten Laden können alle Spiele offline gespielt werden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="measure">
|
||||
<div class="measure-icon">🔐</div>
|
||||
<div class="measure-content">
|
||||
<h4>Lokale Datenspeicherung</h4>
|
||||
<p>Alle Daten bleiben auf dem Gerät des Nutzers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Kontakt und Meldungen</h2>
|
||||
|
||||
<div class="contact-info">
|
||||
<p>
|
||||
Haben Sie Bedenken bezüglich eines Spiels oder möchten Sie uns auf
|
||||
problematische Inhalte hinweisen? Wir nehmen jeden Hinweis ernst.
|
||||
</p>
|
||||
|
||||
<div class="contact-card">
|
||||
<h3>Jugendschutzbeauftragter</h3>
|
||||
<p>
|
||||
E-Mail: jugendschutz@[ihre-domain].de<br>
|
||||
Wir antworten innerhalb von 24 Stunden auf alle Anfragen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-section">
|
||||
<h2>Weitere Ressourcen</h2>
|
||||
|
||||
<div class="resources">
|
||||
<h3>Hilfreiche Links für Eltern:</h3>
|
||||
<ul class="resource-list">
|
||||
<li>
|
||||
<a href="https://www.klicksafe.de" target="_blank" rel="noopener noreferrer">
|
||||
klicksafe.de - EU-Initiative für mehr Sicherheit im Netz
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.schau-hin.info" target="_blank" rel="noopener noreferrer">
|
||||
SCHAU HIN! - Medienratgeber für Familien
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://www.jugendschutz.net" target="_blank" rel="noopener noreferrer">
|
||||
jugendschutz.net - Kompetenzzentrum für Jugendschutz im Internet
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="footer-actions">
|
||||
<Button href="/" variant="ghost">Zurück zur Startseite</Button>
|
||||
<Button href="/agb" variant="ghost">Nutzungsbedingungen</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.jugendschutz-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.jugendschutz-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.jugendschutz-header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, var(--color-text), var(--color-accent));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.intro-section {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.intro-card {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), transparent);
|
||||
border: 1px solid rgba(0, 255, 136, 0.2);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.intro-card p {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section h4 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.content-section p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.principles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.principle-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.principle-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.age-recommendations {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.age-group {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.age-badge {
|
||||
display: inline-block;
|
||||
background: var(--badge-color, var(--color-accent));
|
||||
color: #000;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-radius: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.age-group ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.age-group li {
|
||||
padding: 0.5rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
position: relative;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.age-group li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.parent-tips {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.tip-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.time-recommendations {
|
||||
background: var(--color-bg);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.time-item {
|
||||
padding: 0.5rem 0;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.complexity-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.complexity-list li {
|
||||
padding: 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge.minimal {
|
||||
background: #4ade80;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge.einfach {
|
||||
background: #22d3ee;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge.mittel {
|
||||
background: #fbbf24;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge.komplex {
|
||||
background: #f87171;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.ai-safety {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.safety-info {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.safety-info ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.safety-info li {
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.safety-info li::before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.warning-box {
|
||||
background: rgba(255, 193, 7, 0.1);
|
||||
border: 1px solid rgba(255, 193, 7, 0.3);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.tech-measures {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.measure {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.measure-icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.resource-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.resource-list li {
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.resource-list a {
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.resource-list a:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
margin-top: 4rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.jugendschutz-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.content-section h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.principles-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.age-recommendations {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
636
games/mana-games/apps/web/src/pages/mitmachen.astro
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Button from '../components/Button.astro';
|
||||
---
|
||||
|
||||
<Layout title="Mitmachen">
|
||||
<div class="mitmachen-hero">
|
||||
<div class="hero-background">
|
||||
<div class="floating-element element-1"></div>
|
||||
<div class="floating-element element-2"></div>
|
||||
<div class="floating-element element-3"></div>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<h1 class="hero-title">
|
||||
<span class="title-line">Werde Teil der</span>
|
||||
<span class="title-highlight">Community</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
Gemeinsam erschaffen wir die Zukunft des Web-Gaming
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mitmachen-container">
|
||||
<!-- Intro Section -->
|
||||
<section class="intro-section">
|
||||
<div class="intro-content">
|
||||
<p class="intro-text">
|
||||
Mana Games ist mehr als nur eine Spielesammlung – es ist eine wachsende Community
|
||||
von Entwicklern, Kreativen und Gaming-Enthusiasten. Deine Ideen und Beiträge
|
||||
können Teil dieser Vision werden.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Ways to Contribute -->
|
||||
<section class="contribute-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">01</span>
|
||||
<h2>Wie du beitragen kannst</h2>
|
||||
</div>
|
||||
|
||||
<div class="contribute-cards">
|
||||
<div class="contribute-row row-left">
|
||||
<div class="contribute-visual">
|
||||
<div class="icon-box">💡</div>
|
||||
</div>
|
||||
<div class="contribute-content">
|
||||
<h3>Spielideen einreichen</h3>
|
||||
<p>
|
||||
Du hast eine geniale Spielidee? Teile sie mit uns! Wir sind immer auf der Suche
|
||||
nach innovativen Konzepten, die Spaß machen und gleichzeitig technisch interessant sind.
|
||||
</p>
|
||||
<ul class="feature-list">
|
||||
<li>Neue Gameplay-Mechaniken</li>
|
||||
<li>Kreative Themes und Settings</li>
|
||||
<li>Innovative Steuerungskonzepte</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contribute-row row-right">
|
||||
<div class="contribute-content">
|
||||
<h3>Code & Entwicklung</h3>
|
||||
<p>
|
||||
Als Open-Source-Projekt freuen wir uns über Code-Beiträge jeder Art.
|
||||
Ob Bug-Fixes, Performance-Optimierungen oder neue Features – jeder Beitrag zählt.
|
||||
</p>
|
||||
<ul class="feature-list">
|
||||
<li>JavaScript/HTML5 Canvas Expertise</li>
|
||||
<li>Performance-Optimierungen</li>
|
||||
<li>Bug-Fixes und Verbesserungen</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="contribute-visual">
|
||||
<div class="icon-box">🚀</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contribute-row row-left">
|
||||
<div class="contribute-visual">
|
||||
<div class="icon-box">🎨</div>
|
||||
</div>
|
||||
<div class="contribute-content">
|
||||
<h3>Design & Grafik</h3>
|
||||
<p>
|
||||
Hilf uns dabei, Mana Games noch schöner zu machen! Von Spiel-Assets über
|
||||
UI-Verbesserungen bis hin zu komplett neuen visuellen Konzepten.
|
||||
</p>
|
||||
<ul class="feature-list">
|
||||
<li>Pixel Art & Sprites</li>
|
||||
<li>UI/UX Verbesserungen</li>
|
||||
<li>Visuelle Effekte & Animationen</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Benefits Section -->
|
||||
<section class="benefits-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">02</span>
|
||||
<h2>Was dich erwartet</h2>
|
||||
</div>
|
||||
<div class="benefits-grid">
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🏆</div>
|
||||
<h4>Anerkennung</h4>
|
||||
<p>Dein Name in den Credits und der Contributors-Liste</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">📚</div>
|
||||
<h4>Lernerfahrung</h4>
|
||||
<p>Arbeite mit modernen Web-Technologien und lerne von der Community</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🌍</div>
|
||||
<h4>Reichweite</h4>
|
||||
<p>Deine Arbeit wird von Spielern weltweit gesehen und gespielt</p>
|
||||
</div>
|
||||
<div class="benefit-card">
|
||||
<div class="benefit-icon">🤝</div>
|
||||
<h4>Netzwerk</h4>
|
||||
<p>Verbinde dich mit gleichgesinnten Entwicklern und Kreativen</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Guidelines Section -->
|
||||
<section class="guidelines-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">03</span>
|
||||
<h2>Unsere Richtlinien</h2>
|
||||
</div>
|
||||
<div class="guidelines-content">
|
||||
<div class="guideline">
|
||||
<span class="guideline-icon">✅</span>
|
||||
<div>
|
||||
<strong>Qualität über Quantität</strong>
|
||||
<p>Wir legen Wert auf durchdachte, gut implementierte Beiträge</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guideline">
|
||||
<span class="guideline-icon">🎯</span>
|
||||
<div>
|
||||
<strong>Performance im Fokus</strong>
|
||||
<p>Spiele müssen flüssig auf allen Geräten laufen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guideline">
|
||||
<span class="guideline-icon">🌟</span>
|
||||
<div>
|
||||
<strong>Kreativität fördern</strong>
|
||||
<p>Neue Ideen und innovative Ansätze sind immer willkommen</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="guideline">
|
||||
<span class="guideline-icon">👥</span>
|
||||
<div>
|
||||
<strong>Respektvolle Community</strong>
|
||||
<p>Ein freundlicher und konstruktiver Umgang miteinander</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tech Stack -->
|
||||
<section class="tech-section">
|
||||
<div class="section-header">
|
||||
<span class="section-number">04</span>
|
||||
<h2>Unser Tech-Stack</h2>
|
||||
</div>
|
||||
<div class="tech-info">
|
||||
<p class="tech-intro">
|
||||
Arbeite mit modernen Web-Technologien und erweitere deine Fähigkeiten:
|
||||
</p>
|
||||
<div class="tech-grid">
|
||||
<div class="tech-item">
|
||||
<code>HTML5 Canvas</code>
|
||||
<span>Grafik-Engine</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<code>JavaScript ES6+</code>
|
||||
<span>Programmierung</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<code>Astro</code>
|
||||
<span>Framework</span>
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<code>PWA</code>
|
||||
<span>App-Technologie</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section class="cta-section">
|
||||
<div class="cta-content">
|
||||
<h2>Bereit durchzustarten?</h2>
|
||||
<p>
|
||||
Egal ob du Entwickler, Designer oder einfach voller Ideen bist –
|
||||
wir freuen uns auf deinen Beitrag zur Mana Games Community!
|
||||
</p>
|
||||
<div class="cta-buttons">
|
||||
<Button href="https://github.com/yourusername/mana-games" variant="primary" size="large">
|
||||
GitHub Repository
|
||||
</Button>
|
||||
<Button href="/contact" variant="accent" size="large">
|
||||
Kontakt aufnehmen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* Hero Section */
|
||||
.mitmachen-hero {
|
||||
position: relative;
|
||||
min-height: 50vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.hero-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
.floating-element {
|
||||
position: absolute;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.element-1 {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.element-2 {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
bottom: -50px;
|
||||
right: -50px;
|
||||
animation-delay: 5s;
|
||||
}
|
||||
|
||||
.element-3 {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
top: 50%;
|
||||
left: 80%;
|
||||
animation-delay: 10s;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translate(0, 0) rotate(0deg); }
|
||||
33% { transform: translate(30px, -30px) rotate(120deg); }
|
||||
66% { transform: translate(-20px, 20px) rotate(240deg); }
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: clamp(3rem, 8vw, 5rem);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.title-line {
|
||||
display: block;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.6s ease forwards;
|
||||
}
|
||||
|
||||
.title-highlight {
|
||||
display: block;
|
||||
background: linear-gradient(135deg, var(--color-accent), var(--color-accent-secondary));
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.6s ease 0.2s forwards;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
animation: fadeInUp 0.6s ease 0.4s forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.mitmachen-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Sections */
|
||||
section {
|
||||
margin-bottom: 6rem;
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
animation: fadeInUp 0.8s ease forwards;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.section-number {
|
||||
font-size: 3rem;
|
||||
font-weight: 900;
|
||||
color: var(--color-accent);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
/* Intro Section */
|
||||
.intro-section {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Contribute Section - Alternating Layout */
|
||||
.contribute-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
.contribute-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
gap: 3rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.row-right {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
|
||||
.contribute-visual {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), rgba(0, 255, 136, 0.05));
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 4rem;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
.contribute-content h3 {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.contribute-content p {
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.8;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.feature-list li {
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0.5rem 0;
|
||||
padding-left: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.feature-list li::before {
|
||||
content: "→";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Benefits Section */
|
||||
.benefits-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.benefit-card {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.benefit-card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 10px 30px rgba(0, 255, 136, 0.1);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.benefit-card h4 {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.benefit-card p {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Guidelines Section */
|
||||
.guidelines-section {
|
||||
background: var(--color-bg-secondary);
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
margin: 4rem 0;
|
||||
}
|
||||
|
||||
.guidelines-content {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.guideline {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.guideline-icon {
|
||||
font-size: 2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.guideline strong {
|
||||
display: block;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.guideline p {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Tech Section */
|
||||
.tech-info {
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.05), transparent);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.tech-intro {
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tech-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.tech-item {
|
||||
background: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.tech-item:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tech-item code {
|
||||
display: block;
|
||||
color: var(--color-accent);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tech-item span {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* CTA Section */
|
||||
.cta-section {
|
||||
text-align: center;
|
||||
padding: 4rem;
|
||||
background: linear-gradient(135deg, rgba(0, 255, 136, 0.1), transparent);
|
||||
border-radius: 2rem;
|
||||
}
|
||||
|
||||
.cta-content h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cta-content p {
|
||||
font-size: 1.2rem;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 2rem;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.cta-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.hero-title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 4rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.contribute-row {
|
||||
grid-template-columns: 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.row-right {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.contribute-visual {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.guidelines-section {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.cta-section {
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
401
games/mana-games/apps/web/src/pages/play-generated.astro
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
---
|
||||
|
||||
<Layout title="Generiertes Spiel" fullWidth={true} hideFooter={true}>
|
||||
<div class="game-container">
|
||||
<div class="game-header">
|
||||
<button id="backBtn" class="back-btn">
|
||||
← Zurück
|
||||
</button>
|
||||
<h1 id="gameTitle">Lade Spiel...</h1>
|
||||
<div class="game-actions">
|
||||
<button id="editBtn" class="action-btn">
|
||||
📝 Bearbeiten
|
||||
</button>
|
||||
<button id="fullscreenBtn" class="action-btn">
|
||||
⛶ Vollbild
|
||||
</button>
|
||||
<button id="deleteBtn" class="action-btn danger">
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-frame-container">
|
||||
<div id="loadingState" class="loading-state">
|
||||
<div class="spinner"></div>
|
||||
<p>Lade dein Spiel...</p>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
id="gameFrame"
|
||||
class="game-iframe hidden"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
></iframe>
|
||||
|
||||
<div id="errorState" class="error-state hidden">
|
||||
<p>❌ Spiel konnte nicht geladen werden</p>
|
||||
<p class="error-detail">Das gesuchte Spiel wurde nicht gefunden.</p>
|
||||
<a href="/" class="home-link">Zurück zur Startseite</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="game-info">
|
||||
<p id="gameDescription" class="game-description"></p>
|
||||
<p id="gameDate" class="game-date"></p>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<script>
|
||||
// Get game ID from URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const gameId = urlParams.get('id');
|
||||
|
||||
if (!gameId) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Game Storage
|
||||
class GameStorage {
|
||||
constructor() {
|
||||
this.dbName = 'ManaGamesDB';
|
||||
this.storeName = 'generatedGames';
|
||||
this.db = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
const store = db.createObjectStore(this.storeName, { keyPath: 'id' });
|
||||
store.createIndex('createdAt', 'createdAt', { unique: false });
|
||||
store.createIndex('title', 'title', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async getGame(id) {
|
||||
if (!this.db) await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readonly');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteGame(id) {
|
||||
if (!this.db) await this.init();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = this.db.transaction([this.storeName], 'readwrite');
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
const gameStorage = new GameStorage();
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
const errorState = document.getElementById('errorState');
|
||||
const gameFrame = document.getElementById('gameFrame');
|
||||
const gameTitle = document.getElementById('gameTitle');
|
||||
const gameDescription = document.getElementById('gameDescription');
|
||||
const gameDate = document.getElementById('gameDate');
|
||||
const backBtn = document.getElementById('backBtn');
|
||||
const editBtn = document.getElementById('editBtn');
|
||||
const fullscreenBtn = document.getElementById('fullscreenBtn');
|
||||
const deleteBtn = document.getElementById('deleteBtn');
|
||||
|
||||
// Load game
|
||||
async function loadGame() {
|
||||
try {
|
||||
const game = await gameStorage.getGame(gameId);
|
||||
|
||||
if (!game) {
|
||||
showError();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update UI
|
||||
document.title = `${game.title} - ManaGames`;
|
||||
gameTitle.textContent = game.title;
|
||||
gameDescription.textContent = game.description || game.prompt;
|
||||
|
||||
const date = new Date(game.createdAt).toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
gameDate.textContent = `Erstellt am ${date}`;
|
||||
|
||||
// Display game
|
||||
const blob = new Blob([game.html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
gameFrame.src = url;
|
||||
|
||||
gameFrame.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
hideLoading();
|
||||
}, { once: true });
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading game:', error);
|
||||
showError();
|
||||
}
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loadingState.classList.add('hidden');
|
||||
gameFrame.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showError() {
|
||||
loadingState.classList.add('hidden');
|
||||
errorState.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
backBtn.addEventListener('click', () => {
|
||||
window.history.back();
|
||||
});
|
||||
|
||||
editBtn.addEventListener('click', () => {
|
||||
// Store game ID in sessionStorage for the create page to load
|
||||
sessionStorage.setItem('editGameId', gameId);
|
||||
window.location.href = '/create';
|
||||
});
|
||||
|
||||
fullscreenBtn.addEventListener('click', () => {
|
||||
if (gameFrame.requestFullscreen) {
|
||||
gameFrame.requestFullscreen();
|
||||
}
|
||||
});
|
||||
|
||||
deleteBtn.addEventListener('click', async () => {
|
||||
if (confirm('Bist du sicher, dass du dieses Spiel löschen möchtest?')) {
|
||||
try {
|
||||
await gameStorage.deleteGame(gameId);
|
||||
window.location.href = '/';
|
||||
} catch (error) {
|
||||
console.error('Error deleting game:', error);
|
||||
alert('Fehler beim Löschen des Spiels');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load game on page load
|
||||
loadGame();
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.game-container {
|
||||
height: calc(100vh - 60px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.game-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.back-btn:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
#gameTitle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.game-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--color-surface);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #ef4444;
|
||||
border-color: rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.game-frame-container {
|
||||
flex: 1;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.game-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading-state, .error-state {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 3px solid var(--color-border);
|
||||
border-top-color: var(--color-accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-state p, .error-state p {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 1.1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.error-detail {
|
||||
font-size: 0.9rem !important;
|
||||
color: var(--color-text-muted) !important;
|
||||
}
|
||||
|
||||
.home-link {
|
||||
margin-top: 1rem;
|
||||
background: var(--color-accent);
|
||||
color: var(--color-bg);
|
||||
padding: 0.75rem 2rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.home-link:hover {
|
||||
background: var(--color-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.game-info {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.game-description {
|
||||
color: var(--color-text);
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.game-date {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.game-container {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.game-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.game-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#gameTitle {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||