restore(mana-games): bring back AI browser games platform

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-29 09:16:58 +02:00
parent d847eb4115
commit a4184f1bab
108 changed files with 29526 additions and 0 deletions

178
games/mana-games/CLAUDE.md Normal file
View 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

View file

@ -0,0 +1,17 @@
// @ts-check
import {
baseConfig,
typescriptConfig,
nestjsConfig,
prettierConfig,
} from '@manacore/eslint-config';
export default [
{
ignores: ['dist/**', 'node_modules/**'],
},
...baseConfig,
...typescriptConfig,
...nestjsConfig,
...prettierConfig,
];

View file

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View file

@ -0,0 +1,34 @@
{
"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",
"lint": "eslint ."
},
"dependencies": {
"@anthropic-ai/sdk": "^0.65.0",
"openai": "^4.76.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"
}
}

View 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 {}

View file

@ -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;
};
}

View file

@ -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);
}
}

View file

@ -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 {}

View file

@ -0,0 +1,362 @@
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 '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;
}
}

View file

@ -0,0 +1,72 @@
import { IsString, IsArray, IsOptional, 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;
}

View file

@ -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);
}
}

View file

@ -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 {}

View file

@ -0,0 +1,237 @@
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')
);
}
}
}

View file

@ -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',
};
}
}

View file

@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View 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();

View 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
}
}

View 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

View file

@ -0,0 +1,5 @@
// @ts-check
import { defineConfig } from 'astro/config';
// https://astro.build/config
export default defineConfig({});

View file

@ -0,0 +1,23 @@
// @ts-check
import {
baseConfig,
typescriptConfig,
svelteConfig,
prettierConfig,
} from '@manacore/eslint-config';
export default [
{
ignores: [
'dist/**',
'.svelte-kit/**',
'.astro/**',
'node_modules/**',
'**/stats-integration-template.js',
],
},
...baseConfig,
...typescriptConfig,
...svelteConfig,
...prettierConfig,
];

View file

@ -0,0 +1,19 @@
{
"name": "@mana-games/web",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "rm -rf dist && astro build",
"preview": "astro preview",
"astro": "astro",
"lint": "eslint ."
},
"dependencies": {
"astro": "^5.10.1"
},
"devDependencies": {
"sharp": "^0.34.2"
}
}

View 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

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load diff

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View 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

View 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"
}
]
}

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -0,0 +1,161 @@
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 });
});
}
});

View file

@ -0,0 +1,188 @@
---
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>

View file

@ -0,0 +1,193 @@
---
// 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>&copy; 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>

View file

@ -0,0 +1,309 @@
---
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>

View file

@ -0,0 +1,146 @@
---
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>

View file

@ -0,0 +1,352 @@
---
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"></path>
</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"></path>
</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>

View 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>

View file

@ -0,0 +1,537 @@
---
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>

View file

@ -0,0 +1,401 @@
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,
},
},
];

View 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>

View file

@ -0,0 +1,684 @@
---
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>

View file

@ -0,0 +1,472 @@
---
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>

View 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>

View file

@ -0,0 +1,719 @@
---
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>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,445 @@
---
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>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,427 @@
---
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>

View file

@ -0,0 +1,432 @@
---
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>

View file

@ -0,0 +1,765 @@
---
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&nbsp;&nbsp;&nbsp;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>

View 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>

View file

@ -0,0 +1,644 @@
---
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>

Some files were not shown because too many files have changed in this diff Show more