rename(mana-games): rebrand to Arcade

Rename games/mana-games/ to games/arcade/, update all package names
(@mana-games/* → @arcade/*), appIds, display names, docker-compose
service, root scripts, and documentation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-29 18:31:37 +02:00
parent 2874e202ea
commit 9e82e40e16
105 changed files with 86 additions and 80 deletions

159
games/arcade/CLAUDE.md Normal file
View file

@ -0,0 +1,159 @@
# Arcade - CLAUDE.md
AI-powered browser games platform mit 22+ Spielen und KI-Spielgenerierung.
## Projektstruktur
```
games/arcade/
├── apps/
│ ├── web/ # SvelteKit Web-App (@arcade/web)
│ │ ├── src/
│ │ │ ├── routes/ # SvelteKit-Routen
│ │ │ │ ├── (app)/ # App-Routen mit PillNavigation
│ │ │ │ │ ├── play/[slug] # Spiel im iframe
│ │ │ │ │ ├── create/ # AI Game Generator
│ │ │ │ │ ├── community/ # Community-Spiele
│ │ │ │ │ ├── stats/ # Spieler-Statistiken
│ │ │ │ │ └── play-generated/ # Generierte Spiele
│ │ │ │ └── (auth)/ # Login/Register
│ │ │ └── lib/
│ │ │ ├── components/ # Svelte 5 Komponenten
│ │ │ ├── data/ # Local-first Store, Game-Katalog
│ │ │ ├── stores/ # Theme, Auth, Navigation
│ │ │ ├── services/ # Game-Kommunikation (postMessage)
│ │ │ └── i18n/ # DE + EN Übersetzungen
│ │ └── static/
│ │ ├── games/ # 22 HTML-Spiele
│ │ └── screenshots/ # Game-Thumbnails
│ ├── web-astro/ # Alte Astro-App (Referenz, zum Löschen)
│ └── backend/ # NestJS API (@arcade/backend)
│ └── src/
│ ├── game-generator/ # AI-Spielgenerierung (Gemini, Claude, GPT-4)
│ ├── game-submission/ # Community-Einreichungen (GitHub API)
│ └── health/
└── package.json # Root (arcade)
```
## Tech Stack
| Aspekt | Technologie |
|--------|-------------|
| Frontend | SvelteKit 2 + Svelte 5 (Runes) |
| Styling | Tailwind CSS 4 + @manacore/shared-tailwind |
| Auth | @manacore/shared-auth (SSO) |
| PWA | @vite-pwa/sveltekit + @manacore/shared-pwa |
| State | @manacore/local-store (Dexie.js + sync) |
| i18n | svelte-i18n (DE + EN) |
| UI | @manacore/shared-ui (PillNav, AuthGate, etc.) |
| Theming | @manacore/shared-theme (multi-theme) |
| Backend | NestJS (AI-Generierung, Community) |
## Entwicklung
```bash
# Alles starten (Web + Backend)
pnpm arcade:dev
# Nur Web (SvelteKit)
pnpm dev:arcade:web
# Nur Backend (NestJS)
pnpm dev:arcade:backend
# Web + Backend zusammen
pnpm dev:arcade:app
```
**Ports:**
- Web: http://localhost:5210
- Backend: http://localhost:3011
## Local-First Daten
Stats und generierte Spiele werden in IndexedDB gespeichert (Dexie.js) mit optionalem Sync:
**Collections:**
- `gameStats` — Highscores, Spielzeit, Spiele pro Game
- `generatedGames` — Mit KI erstellte Spiele (HTML, Prompt, Modell)
- `favorites` — Favorisierte Spiele
**Dateien:**
- `src/lib/data/local-store.ts` — Dexie-Store Definition
- `src/lib/data/queries.ts` — Reactive Queries (useLiveQuery)
- `src/lib/data/games.ts` — Statischer Spielekatalog (21 Spiele)
## 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",
"model": "gemini-2.0-flash",
"originalPrompt": "...",
"currentCode": "..."
}
```
**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
```bash
MANA_GAMES_BACKEND_PORT=3011
MANA_GAMES_GOOGLE_GENAI_API_KEY=your_key
MANA_GAMES_ANTHROPIC_API_KEY=your_key
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
MANA_GAMES_GITHUB_TOKEN=your_token
MANA_GAMES_GITHUB_OWNER=tillschneider
MANA_GAMES_GITHUB_REPO=arcade
```
## Spiel hinzufügen
1. HTML-Datei in `apps/web/static/games/spiel_name.html`
2. Screenshot in `apps/web/static/screenshots/spiel-name.jpg`
3. Registrieren in `apps/web/src/lib/data/games.ts`
## 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 }
}, '*');
```
## Spielekatalog
**21 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": "@arcade/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:5210', // SvelteKit dev
'http://localhost:4321', // Legacy Astro dev
'http://localhost:3000', // Alternative dev
],
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(`Arcade 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,29 @@
# syntax=docker/dockerfile:1
FROM sveltekit-base:local AS builder
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
COPY games/arcade/apps/web ./games/arcade/apps/web
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --no-frozen-lockfile --ignore-scripts
WORKDIR /app/games/arcade/apps/web
RUN pnpm exec svelte-kit sync
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
FROM node:20-alpine AS production
WORKDIR /app/games/arcade/apps/web
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
COPY --from=builder /app/games/arcade/apps/web/node_modules ./node_modules
COPY --from=builder /app/games/arcade/apps/web/build ./build
COPY --from=builder /app/games/arcade/apps/web/package.json ./
EXPOSE 5210
ENV NODE_ENV=production PORT=5210 HOST=0.0.0.0
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:5210/health || exit 1
CMD ["node", "build"]

View file

@ -0,0 +1,58 @@
{
"name": "@arcade/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "eslint .",
"format": "prettier --write .",
"type-check": "svelte-kit sync && svelte-check --threshold error"
},
"devDependencies": {
"@manacore/shared-pwa": "workspace:*",
"@manacore/shared-vite-config": "workspace:*",
"@sveltejs/adapter-node": "^5.0.0",
"@sveltejs/kit": "^2.47.1",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/vite": "^4.1.7",
"@types/node": "^20.0.0",
"@vite-pwa/sveltekit": "^1.1.0",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "^5.41.0",
"svelte-check": "^4.3.3",
"tailwindcss": "^4.1.7",
"tslib": "^2.4.1",
"typescript": "^5.9.3",
"vite": "^6.0.0"
},
"dependencies": {
"@manacore/local-store": "workspace:*",
"@manacore/shared-app-onboarding": "workspace:*",
"@manacore/shared-auth": "workspace:*",
"@manacore/shared-auth-stores": "workspace:*",
"@manacore/shared-auth-ui": "workspace:*",
"@manacore/shared-branding": "workspace:*",
"@manacore/shared-error-tracking": "workspace:*",
"@manacore/feedback": "workspace:*",
"@manacore/shared-i18n": "workspace:*",
"@manacore/help": "workspace:*",
"@manacore/shared-icons": "workspace:*",
"@manacore/shared-profile-ui": "workspace:*",
"@manacore/shared-stores": "workspace:*",
"@manacore/shared-tags": "workspace:*",
"@manacore/subscriptions": "workspace:*",
"@manacore/shared-tailwind": "workspace:*",
"@manacore/shared-theme": "workspace:*",
"@manacore/shared-theme-ui": "workspace:*",
"@manacore/shared-ui": "workspace:*",
"@manacore/shared-utils": "workspace:*",
"svelte-i18n": "^4.0.1"
},
"type": "module"
}

View file

@ -0,0 +1,10 @@
@import "tailwindcss";
@import "@manacore/shared-tailwind/themes.css";
/* Scan shared packages for Tailwind classes */
@source "../../../../../../packages/shared-ui/src";
@source "../../../../../../packages/shared-auth-ui/src";
@source "../../../../../../packages/shared-branding/src";
@source "../../../../../../packages/shared-theme-ui/src";
@source "../../../../../../packages/shared-theme-ui/src/components";
@source "../../../../../../packages/shared-theme-ui/src/pages";

View file

@ -0,0 +1,12 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,12 @@
import { initErrorTracking, handleSvelteError } from '@manacore/shared-error-tracking/browser';
import type { HandleClientError } from '@sveltejs/kit';
initErrorTracking({
serviceName: 'arcade-web',
dsn: (window as any).__PUBLIC_GLITCHTIP_DSN__,
environment: import.meta.env.MODE,
});
export const handleError: HandleClientError = ({ error }) => {
handleSvelteError(error);
};

View file

@ -0,0 +1,28 @@
import type { Handle } from '@sveltejs/kit';
import { injectUmamiAnalytics } from '@manacore/shared-utils/analytics-server';
import { setSecurityHeaders } from '@manacore/shared-utils/security-headers';
const PUBLIC_MANA_CORE_AUTH_URL_CLIENT =
process.env.PUBLIC_MANA_CORE_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_CORE_AUTH_URL || '';
const PUBLIC_BACKEND_URL_CLIENT =
process.env.PUBLIC_BACKEND_URL_CLIENT || process.env.PUBLIC_BACKEND_URL || '';
const PUBLIC_GLITCHTIP_DSN = process.env.PUBLIC_GLITCHTIP_DSN || '';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
transformPageChunk: ({ html }) => {
const envScript = `<script>
window.__PUBLIC_MANA_CORE_AUTH_URL__ = ${JSON.stringify(PUBLIC_MANA_CORE_AUTH_URL_CLIENT)};
window.__PUBLIC_BACKEND_URL__ = ${JSON.stringify(PUBLIC_BACKEND_URL_CLIENT)};
window.__PUBLIC_GLITCHTIP_DSN__ = ${JSON.stringify(PUBLIC_GLITCHTIP_DSN)};
</script>`;
return injectUmamiAnalytics(html.replace('<head>', `<head>${envScript}`));
},
});
setSecurityHeaders(response, {
connectSrc: [PUBLIC_MANA_CORE_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT],
});
return response;
};

View file

@ -0,0 +1,54 @@
<script lang="ts">
import type { Game } from '$lib/data/games';
let { game, href }: { game: Game; href: string } = $props();
const difficultyColors: Record<string, string> = {
Einfach: 'bg-green-500/20 text-green-400',
Mittel: 'bg-yellow-500/20 text-yellow-400',
Schwer: 'bg-red-500/20 text-red-400',
};
</script>
<a
{href}
class="group block rounded-xl border border-border bg-card p-0 overflow-hidden transition-all hover:border-primary/50 hover:shadow-lg hover:shadow-primary/5 hover:-translate-y-0.5"
>
{#if game.thumbnail}
<div class="aspect-video w-full overflow-hidden bg-muted">
<img
src={game.thumbnail}
alt={game.title}
class="h-full w-full object-cover transition-transform group-hover:scale-105"
loading="lazy"
/>
</div>
{:else}
<div class="aspect-video w-full bg-muted flex items-center justify-center">
<span class="text-4xl opacity-40">🎮</span>
</div>
{/if}
<div class="p-4">
<div class="flex items-start justify-between gap-2 mb-2">
<h3 class="font-semibold text-foreground group-hover:text-primary transition-colors">
{game.title}
</h3>
<span class="shrink-0 text-xs px-2 py-0.5 rounded-full {difficultyColors[game.difficulty]}">
{game.difficulty}
</span>
</div>
<p class="text-sm text-muted-foreground line-clamp-2 mb-3">
{game.description}
</p>
<div class="flex flex-wrap gap-1">
{#each game.tags.slice(0, 3) as tag}
<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
{tag}
</span>
{/each}
</div>
</div>
</a>

View file

@ -0,0 +1,72 @@
<script lang="ts">
import { SkeletonBox } from '@manacore/shared-ui';
</script>
<div class="app-loading-skeleton" role="status" aria-label="App wird geladen...">
<div class="header-skeleton">
<SkeletonBox width="120px" height="32px" borderRadius="8px" />
<div class="header-nav">
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
<SkeletonBox width="80px" height="32px" borderRadius="16px" />
</div>
<SkeletonBox width="36px" height="36px" borderRadius="50%" />
</div>
<div class="content-skeleton">
<div class="games-placeholder">
{#each Array(6) as _}
<SkeletonBox width="100%" height="200px" borderRadius="12px" />
{/each}
</div>
</div>
</div>
<style>
.app-loading-skeleton {
min-height: 100vh;
background: hsl(var(--background));
}
.header-skeleton {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
border-bottom: 1px solid hsl(var(--border));
}
.header-nav {
display: flex;
gap: 0.5rem;
}
.content-skeleton {
max-width: 80rem;
margin: 0 auto;
padding: 2rem;
}
.games-placeholder {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
@media (max-width: 768px) {
.header-nav {
display: none;
}
.header-skeleton {
padding: 1rem;
}
.content-skeleton {
padding: 1rem;
}
.games-placeholder {
grid-template-columns: 1fr;
}
}
</style>

View file

@ -0,0 +1 @@
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';

View file

@ -0,0 +1,325 @@
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?: 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.',
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',
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.',
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',
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.',
slug: 'memory-card-match',
htmlFile: '/games/memory_card_match.html',
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',
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 und Combo-System.',
slug: 'card-stack-rush',
htmlFile: '/games/card_stack_rush.html',
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.',
slug: 'flappy-mana',
htmlFile: '/games/flappy_mana.html',
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 und weiche Hindernissen aus.',
slug: 'mana-runner',
htmlFile: '/games/mana_runner.html',
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.',
slug: 'mana-defense',
htmlFile: '/games/mana_defense.html',
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 und Prestige-System.',
slug: 'mana-factory',
htmlFile: '/games/mana_factory.html',
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',
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 },
},
];
export function getGameBySlug(slug: string): Game | undefined {
return games.find((g) => g.slug === slug);
}
export function getGamesByTag(tag: string): Game[] {
return games.filter((g) => g.tags.includes(tag));
}
export function getAllTags(): string[] {
const tagSet = new Set<string>();
for (const game of games) {
for (const tag of game.tags) {
tagSet.add(tag);
}
}
return [...tagSet].sort();
}

View file

@ -0,0 +1 @@
// No guest seed data needed — games are static HTML files, stats build up from play

View file

@ -0,0 +1,55 @@
import { createLocalStore, type BaseRecord } from '@manacore/local-store';
// ─── Types ──────────────────────────────────────────────────
export interface LocalGameStats extends BaseRecord {
gameId: string;
highScore: number;
lastScore: number;
gamesPlayed: number;
totalPlayTime: number;
lastPlayed: string;
}
export interface LocalGeneratedGame extends BaseRecord {
title: string;
description: string;
htmlCode: string;
prompt: string;
model: string;
iterationCount: number;
}
export interface LocalFavorite extends BaseRecord {
gameId: string;
}
// ─── Store ──────────────────────────────────────────────────
const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
export const gamesStore = createLocalStore({
appId: 'arcade',
collections: [
{
name: 'gameStats',
indexes: ['gameId', 'highScore'],
},
{
name: 'generatedGames',
indexes: ['title'],
},
{
name: 'favorites',
indexes: ['gameId'],
},
],
sync: {
serverUrl: SYNC_SERVER_URL,
},
});
// Typed collection accessors
export const gameStatsCollection = gamesStore.collection<LocalGameStats>('gameStats');
export const generatedGameCollection = gamesStore.collection<LocalGeneratedGame>('generatedGames');
export const favoriteCollection = gamesStore.collection<LocalFavorite>('favorites');

View file

@ -0,0 +1,24 @@
import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
import {
gameStatsCollection,
generatedGameCollection,
favoriteCollection,
type LocalGameStats,
type LocalGeneratedGame,
type LocalFavorite,
} from './local-store';
export function useAllGameStats() {
return useLiveQueryWithDefault(async () => gameStatsCollection.getAll(), [] as LocalGameStats[]);
}
export function useAllGeneratedGames() {
return useLiveQueryWithDefault(async () => {
const games = await generatedGameCollection.getAll();
return games.reverse();
}, [] as LocalGeneratedGame[]);
}
export function useAllFavorites() {
return useLiveQueryWithDefault(async () => favoriteCollection.getAll(), [] as LocalFavorite[]);
}

View file

@ -0,0 +1,38 @@
import { browser } from '$app/environment';
import { init, register, locale, waitLocale } from 'svelte-i18n';
export const supportedLocales = ['de', 'en'] as const;
export type SupportedLocale = (typeof supportedLocales)[number];
const defaultLocale = 'de';
register('de', () => import('./locales/de.json'));
register('en', () => import('./locales/en.json'));
function getInitialLocale(): SupportedLocale {
if (browser) {
const stored = localStorage.getItem('arcade_locale');
if (stored && supportedLocales.includes(stored as SupportedLocale)) {
return stored as SupportedLocale;
}
const browserLang = navigator.language.split('-')[0];
if (supportedLocales.includes(browserLang as SupportedLocale)) {
return browserLang as SupportedLocale;
}
}
return defaultLocale;
}
init({
fallbackLocale: defaultLocale,
initialLocale: getInitialLocale(),
});
export function setLocale(newLocale: SupportedLocale) {
locale.set(newLocale);
if (browser) {
localStorage.setItem('arcade_locale', newLocale);
}
}
export { waitLocale };

View file

@ -0,0 +1,66 @@
{
"app": {
"name": "Arcade",
"loading": "Wird geladen..."
},
"nav": {
"home": "Spiele",
"create": "Erstellen",
"community": "Community",
"stats": "Statistiken",
"settings": "Einstellungen"
},
"home": {
"title": "Browser-Spiele",
"subtitle": "22+ Spiele direkt im Browser spielen",
"search": "Spiel suchen...",
"noResults": "Keine Spiele gefunden",
"allGames": "Alle Spiele",
"favorites": "Favoriten",
"recentlyPlayed": "Zuletzt gespielt"
},
"game": {
"play": "Spielen",
"difficulty": "Schwierigkeit",
"controls": "Steuerung",
"tags": "Tags",
"stats": "Statistiken",
"highScore": "Highscore",
"gamesPlayed": "Spiele gespielt",
"totalPlayTime": "Gesamtspielzeit",
"lastPlayed": "Zuletzt gespielt",
"back": "Zurück",
"fullscreen": "Vollbild",
"editor": "Code ansehen"
},
"create": {
"title": "Spiel erstellen",
"subtitle": "Beschreibe dein Spiel und lass es von KI generieren",
"prompt": "Was für ein Spiel soll erstellt werden?",
"promptPlaceholder": "Ein Neon-Snake-Spiel mit Partikeleffekten...",
"generate": "Generieren",
"generating": "Generiere Spiel...",
"model": "KI-Modell",
"iterate": "Verbessern",
"save": "Speichern",
"preview": "Vorschau"
},
"stats": {
"title": "Deine Statistiken",
"totalGames": "Gespielte Spiele",
"totalTime": "Gesamtspielzeit",
"favoriteGame": "Lieblingsspiel",
"noStats": "Noch keine Statistiken. Spiele ein paar Spiele!"
},
"difficulty": {
"Einfach": "Einfach",
"Mittel": "Mittel",
"Schwer": "Schwer"
},
"time": {
"justNow": "Gerade eben",
"minutesAgo": "Vor {minutes} Minuten",
"hoursAgo": "Vor {hours} Stunden",
"daysAgo": "Vor {days} Tagen"
}
}

View file

@ -0,0 +1,66 @@
{
"app": {
"name": "Arcade",
"loading": "Loading..."
},
"nav": {
"home": "Games",
"create": "Create",
"community": "Community",
"stats": "Statistics",
"settings": "Settings"
},
"home": {
"title": "Browser Games",
"subtitle": "22+ games to play right in your browser",
"search": "Search games...",
"noResults": "No games found",
"allGames": "All Games",
"favorites": "Favorites",
"recentlyPlayed": "Recently Played"
},
"game": {
"play": "Play",
"difficulty": "Difficulty",
"controls": "Controls",
"tags": "Tags",
"stats": "Statistics",
"highScore": "High Score",
"gamesPlayed": "Games Played",
"totalPlayTime": "Total Play Time",
"lastPlayed": "Last Played",
"back": "Back",
"fullscreen": "Fullscreen",
"editor": "View Code"
},
"create": {
"title": "Create Game",
"subtitle": "Describe your game and let AI generate it",
"prompt": "What kind of game should be created?",
"promptPlaceholder": "A neon snake game with particle effects...",
"generate": "Generate",
"generating": "Generating game...",
"model": "AI Model",
"iterate": "Improve",
"save": "Save",
"preview": "Preview"
},
"stats": {
"title": "Your Statistics",
"totalGames": "Games Played",
"totalTime": "Total Play Time",
"favoriteGame": "Favorite Game",
"noStats": "No statistics yet. Play some games!"
},
"difficulty": {
"Einfach": "Easy",
"Mittel": "Medium",
"Schwer": "Hard"
},
"time": {
"justNow": "Just now",
"minutesAgo": "{minutes} minutes ago",
"hoursAgo": "{hours} hours ago",
"daysAgo": "{days} days ago"
}
}

View file

@ -0,0 +1,8 @@
import { createFeedbackService } from '@manacore/feedback';
import { authStore } from '$lib/stores/auth.svelte';
export const feedbackService = createFeedbackService({
apiUrl: import.meta.env.DEV ? 'http://localhost:3001' : 'https://auth.mana.how',
appId: 'arcade',
getAuthToken: async () => authStore.getAccessToken(),
});

View file

@ -0,0 +1,114 @@
import { gameStatsCollection, type LocalGameStats } from '$lib/data/local-store';
export interface GameMessage {
type: 'GAME_EVENT' | 'GAME_LOADED' | 'GAME_ENDED';
gameId: string;
event?: string;
data?: Record<string, unknown>;
}
export function initGameCommunication(gameSlug: string) {
let gameStartTime: number | null = null;
async function getOrCreateStats(gameId: string): Promise<LocalGameStats | null> {
const all = await gameStatsCollection.getAll();
return all.find((s) => s.gameId === gameId) || null;
}
async function updateGameStats(gameId: string, update: Partial<LocalGameStats>) {
const existing = await getOrCreateStats(gameId);
if (existing) {
await gameStatsCollection.update(existing.id, {
...update,
lastPlayed: new Date().toISOString(),
});
} else {
await gameStatsCollection.insert({
gameId,
highScore: 0,
lastScore: 0,
gamesPlayed: 0,
totalPlayTime: 0,
lastPlayed: new Date().toISOString(),
...update,
});
}
}
function handleMessage(event: MessageEvent) {
if (event.origin !== window.location.origin) return;
const message = event.data as GameMessage;
if (!message.type || message.gameId !== gameSlug) return;
switch (message.type) {
case 'GAME_LOADED':
gameStartTime = Date.now();
getOrCreateStats(gameSlug).then((stats) => {
updateGameStats(gameSlug, {
gamesPlayed: (stats?.gamesPlayed || 0) + 1,
});
});
break;
case 'GAME_EVENT':
handleGameEvent(gameSlug, message.event!, message.data);
break;
case 'GAME_ENDED':
if (gameStartTime) {
const playTime = Math.floor((Date.now() - gameStartTime) / 1000);
getOrCreateStats(gameSlug).then((stats) => {
updateGameStats(gameSlug, {
totalPlayTime: (stats?.totalPlayTime || 0) + playTime,
});
});
gameStartTime = null;
}
break;
}
}
function handleBeforeUnload() {
if (gameStartTime) {
const playTime = Math.floor((Date.now() - gameStartTime) / 1000);
getOrCreateStats(gameSlug).then((stats) => {
updateGameStats(gameSlug, {
totalPlayTime: (stats?.totalPlayTime || 0) + playTime,
});
});
}
}
window.addEventListener('message', handleMessage);
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('message', handleMessage);
window.removeEventListener('beforeunload', handleBeforeUnload);
};
async function handleGameEvent(
gameId: string,
event: string,
data: Record<string, unknown> | undefined
) {
if (!data) return;
switch (event) {
case 'SCORE_UPDATE':
case 'GAME_OVER': {
const score = data.score as number;
if (score) {
const stats = await getOrCreateStats(gameId);
await updateGameStats(gameId, {
lastScore: score,
highScore: Math.max(score, stats?.highScore || 0),
});
}
break;
}
}
}
}

View file

@ -0,0 +1,41 @@
import { createAppOnboardingStore, type AppOnboardingStep } from '@manacore/shared-app-onboarding';
import { userSettings } from './user-settings.svelte';
const onboardingSteps: AppOnboardingStep[] = [
{
id: 'features',
type: 'info',
question: 'Willkommen bei Arcade!',
description: 'Das erwartet dich:',
emoji: '🎮',
gradient: { from: 'green-500', to: 'green-700' },
bullets: [
'22+ Browser-Spiele direkt spielbar',
'KI-Spielgenerator: Erstelle eigene Games',
'Statistiken: Highscores & Spielzeit',
'Community: Reiche eigene Spiele ein',
],
},
{
id: 'welcome',
type: 'info',
question: "Los geht's!",
description: 'Tipps:',
emoji: '🕹️',
gradient: { from: 'primary', to: 'primary/70' },
bullets: [
'Cmd/Ctrl+K für Schnellsuche',
'Spiele laufen komplett im Browser',
'Stats werden lokal gespeichert',
'Anmelden synchronisiert deine Daten',
],
},
];
export const gamesOnboarding = createAppOnboardingStore({
appId: 'arcade',
steps: onboardingSteps,
userSettings,
onComplete: async () => {},
onSkip: async () => {},
});

View file

@ -0,0 +1,5 @@
import { createManaAuthStore } from '@manacore/shared-auth-stores';
export const authStore = createManaAuthStore({
devBackendPort: 3011,
});

View file

@ -0,0 +1,5 @@
import { createSimpleNavigationStores } from '@manacore/shared-stores';
export const { isNavCollapsed } = createSimpleNavigationStores({
storageKey: 'arcade',
});

View file

@ -0,0 +1,6 @@
import { createThemeStore } from '@manacore/shared-theme';
export const theme = createThemeStore({
appId: 'arcade',
defaultVariant: 'lume',
});

View file

@ -0,0 +1,18 @@
import { browser } from '$app/environment';
import { createUserSettingsStore } from '@manacore/shared-theme';
import { authStore } from './auth.svelte';
function getAuthUrl(): string {
if (browser && typeof window !== 'undefined') {
const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
.__PUBLIC_MANA_CORE_AUTH_URL__;
if (injectedUrl) return injectedUrl;
}
return import.meta.env.DEV ? 'http://localhost:3001' : '';
}
export const userSettings = createUserSettingsStore({
appId: 'arcade',
authUrl: getAuthUrl,
getAccessToken: () => authStore.getAccessToken(),
});

View file

@ -0,0 +1,298 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { locale } from 'svelte-i18n';
import { PillNavigation, CommandBar, SyncIndicator } from '@manacore/shared-ui';
import type {
PillNavItem,
PillDropdownItem,
CommandBarItem,
QuickAction,
} from '@manacore/shared-ui';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { userSettings } from '$lib/stores/user-settings.svelte';
import { games, getGameBySlug } from '$lib/data/games';
import {
THEME_DEFINITIONS,
DEFAULT_THEME_VARIANTS,
EXTENDED_THEME_VARIANTS,
} from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
import { filterHiddenNavItems } from '@manacore/shared-theme';
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n';
import { getPillAppItems } from '@manacore/shared-branding';
import { setLocale, supportedLocales } from '$lib/i18n';
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@manacore/shared-auth-ui';
import { shouldShowGuestWelcome } from '@manacore/shared-auth-ui';
import { gamesOnboarding } from '$lib/stores/app-onboarding.svelte';
import { MiniOnboardingModal } from '@manacore/shared-app-onboarding';
import { gamesStore } from '$lib/data/local-store';
import {
tagLocalStore,
tagMutations,
useAllTags as useAllSharedTags,
} from '@manacore/shared-stores';
const allTags = useAllSharedTags();
let showGuestWelcome = $state(false);
function initGuestWelcome() {
if (!authStore.isAuthenticated && shouldShowGuestWelcome('arcade')) {
showGuestWelcome = true;
}
}
const appItems = getPillAppItems('arcade');
let { children } = $props();
let commandBarOpen = $state(false);
const commandBarQuickActions: QuickAction[] = [
{ id: 'home', label: 'Alle Spiele', icon: 'gamepad-2', href: '/', shortcut: '1' },
{ id: 'create', label: 'Spiel erstellen', icon: 'sparkles', href: '/create', shortcut: '2' },
{ id: 'community', label: 'Community', icon: 'users', href: '/community', shortcut: '3' },
{ id: 'stats', label: 'Statistiken', icon: 'bar-chart-3', href: '/stats', shortcut: '4' },
{ id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' },
];
async function handleCommandBarSearch(query: string): Promise<CommandBarItem[]> {
if (!query.trim()) return [];
const queryLower = query.toLowerCase();
return games
.filter(
(g) =>
g.title.toLowerCase().includes(queryLower) ||
g.tags.some((t) => t.toLowerCase().includes(queryLower))
)
.slice(0, 10)
.map((g) => ({
id: `game-${g.slug}`,
title: g.title,
subtitle: g.tags.join(', '),
}));
}
function handleCommandBarSelect(item: CommandBarItem) {
const slug = item.id.replace('game-', '');
goto(`/play/${slug}`);
}
let isCollapsed = $state(false);
let isDark = $derived(theme.isDark);
let pinnedThemes = $derived<ThemeVariant[]>(
(userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant =>
EXTENDED_THEME_VARIANTS.includes(t as ThemeVariant)
)
);
let visibleThemes = $derived<ThemeVariant[]>([...DEFAULT_THEME_VARIANTS, ...pinnedThemes]);
let themeVariantItems = $derived<PillDropdownItem[]>([
...visibleThemes.map((variant) => ({
id: variant,
label: THEME_DEFINITIONS[variant]?.label || variant,
icon: THEME_DEFINITIONS[variant]?.icon || '🎨',
onClick: () => theme.setVariant(variant),
active: (theme.variant || 'lume') === variant,
})),
{
id: 'all-themes',
label: 'Alle Themes',
icon: 'palette',
onClick: () => goto('/themes'),
active: false,
},
]);
let currentThemeVariantLabel = $derived(
THEME_DEFINITIONS[theme.variant]?.label || THEME_DEFINITIONS.lume?.label || 'Lume'
);
let currentLocale = $derived($locale || 'de');
function handleLocaleChange(newLocale: string) {
setLocale(newLocale as any);
}
let languageItems = $derived(
getLanguageDropdownItems(supportedLocales, currentLocale, handleLocaleChange)
);
let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale));
let userEmail = $derived(authStore.isAuthenticated ? authStore.user?.email || 'Menü' : '');
const baseNavItems: PillNavItem[] = [
{ href: '/', label: 'Spiele', icon: 'gamepad-2' },
{ href: '/create', label: 'Erstellen', icon: 'sparkles' },
{ href: '/community', label: 'Community', icon: 'users' },
{ href: '/stats', label: 'Statistiken', icon: 'bar-chart-3' },
{ href: '/settings', label: 'Einstellungen', icon: 'settings' },
];
const navItems = $derived(
filterHiddenNavItems('arcade', baseNavItems, userSettings.nav?.hiddenNavItems || {})
);
function handleKeydown(event: KeyboardEvent) {
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
event.preventDefault();
commandBarOpen = true;
}
}
function handleCollapsedChange(collapsed: boolean) {
isCollapsed = collapsed;
collapsedStore.set(collapsed);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('arcade-nav-collapsed', String(collapsed));
}
}
function handleToggleTheme() {
theme.toggleMode();
}
function handleThemeModeChange(mode: 'light' | 'dark' | 'system') {
theme.setMode(mode);
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
async function handleAuthReady() {
await Promise.all([gamesStore.initialize(), tagLocalStore.initialize()]);
if (authStore.isAuthenticated) {
const getToken = () => authStore.getValidToken();
gamesStore.startSync(getToken);
tagMutations.startSync(getToken);
}
const savedCollapsed = localStorage.getItem('arcade-nav-collapsed');
if (savedCollapsed === 'true') {
isCollapsed = true;
collapsedStore.set(true);
}
initGuestWelcome();
if (authStore.isAuthenticated) {
await userSettings.load();
}
}
</script>
<svelte:window onkeydown={handleKeydown} />
<AuthGate {authStore} {goto} allowGuest={true} onReady={handleAuthReady}>
<div class="layout-container">
<PillNavigation
items={navItems}
currentPath={$page.url.pathname}
appName="Arcade"
homeRoute="/"
onToggleTheme={handleToggleTheme}
{isDark}
{isCollapsed}
onCollapsedChange={handleCollapsedChange}
showThemeToggle={true}
showThemeVariants={true}
{themeVariantItems}
{currentThemeVariantLabel}
themeMode={theme.mode}
onThemeModeChange={handleThemeModeChange}
showLanguageSwitcher={true}
{languageItems}
{currentLanguageLabel}
showLogout={authStore.isAuthenticated}
onLogout={handleLogout}
loginHref="/login"
primaryColor="#00ff88"
showAppSwitcher={true}
{appItems}
{userEmail}
settingsHref="/settings"
manaHref="/mana"
profileHref="/profile"
themesHref="/themes"
helpHref="/help"
allAppsHref="/apps"
/>
<main class="main-content bg-background">
<div class="content-wrapper">
{@render children()}
</div>
</main>
<CommandBar
bind:open={commandBarOpen}
onClose={() => (commandBarOpen = false)}
onSearch={handleCommandBarSearch}
onSelect={handleCommandBarSelect}
quickActions={commandBarQuickActions}
placeholder="Spiel suchen..."
emptyText="Keine Ergebnisse"
searchingText="Suche..."
/>
</div>
{#if gamesOnboarding.shouldShow}
<MiniOnboardingModal store={gamesOnboarding} appName="Arcade" appEmoji="🎮" />
{/if}
<GuestWelcomeModal
appId="arcade"
visible={showGuestWelcome}
onClose={() => (showGuestWelcome = false)}
onLogin={() => goto('/login')}
onRegister={() => goto('/register')}
locale={($locale || 'de') === 'de' ? 'de' : 'en'}
/>
{#if authStore.isAuthenticated}
<SessionExpiredBanner locale={$locale || 'de'} loginHref="/login" />
{/if}
<SyncIndicator />
</AuthGate>
<style>
.layout-container {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-content {
position: relative;
z-index: 0;
padding-bottom: 100px;
}
.content-wrapper {
max-width: 100%;
margin-left: auto;
margin-right: auto;
padding: 1rem;
position: relative;
z-index: 0;
}
@media (min-width: 640px) {
.content-wrapper {
padding: 1.5rem;
}
}
@media (min-width: 1024px) {
.content-wrapper {
padding: 2rem;
}
}
</style>

View file

@ -0,0 +1,83 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { games, getAllTags } from '$lib/data/games';
import GameCard from '$lib/components/GameCard.svelte';
let searchQuery = $state('');
let selectedTag = $state<string | null>(null);
const allTags = getAllTags();
let filteredGames = $derived(() => {
let result = games;
if (searchQuery.trim()) {
const q = searchQuery.toLowerCase();
result = result.filter(
(g) =>
g.title.toLowerCase().includes(q) ||
g.description.toLowerCase().includes(q) ||
g.tags.some((t) => t.toLowerCase().includes(q))
);
}
if (selectedTag) {
result = result.filter((g) => g.tags.includes(selectedTag!));
}
return result;
});
</script>
<svelte:head>
<title>{$_('app.name')} - {$_('home.title')}</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">{$_('home.title')}</h1>
<p class="text-muted-foreground mt-1">{$_('home.subtitle')}</p>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<input
type="text"
bind:value={searchQuery}
placeholder={$_('home.search')}
class="flex-1 rounded-lg border border-border bg-background px-4 py-2 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div class="flex flex-wrap gap-2">
<button
class="text-xs px-3 py-1.5 rounded-full transition-colors {selectedTag === null
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => (selectedTag = null)}
>
{$_('home.allGames')}
</button>
{#each allTags as tag}
<button
class="text-xs px-3 py-1.5 rounded-full transition-colors {selectedTag === tag
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => (selectedTag = selectedTag === tag ? null : tag)}
>
{tag}
</button>
{/each}
</div>
{#if filteredGames().length === 0}
<div class="text-center py-12">
<p class="text-muted-foreground">{$_('home.noResults')}</p>
</div>
{:else}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{#each filteredGames() as game (game.id)}
<GameCard {game} href="/play/{game.slug}" />
{/each}
</div>
{/if}
</div>

View file

@ -0,0 +1,27 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
</script>
<svelte:head>
<title>{$_('nav.community')} - Arcade</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">{$_('nav.community')}</h1>
<p class="text-muted-foreground mt-1">
Von der Community erstellte Spiele. Reiche dein eigenes Spiel ein!
</p>
</div>
<div class="text-center py-12 rounded-xl border border-dashed border-border">
<p class="text-4xl mb-4">🎮</p>
<p class="text-muted-foreground">Noch keine Community-Spiele vorhanden.</p>
<a
href="/create"
class="inline-block mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
Erstelle das erste Spiel!
</a>
</div>
</div>

View file

@ -0,0 +1,211 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { generatedGameCollection } from '$lib/data/local-store';
const BACKEND_URL = import.meta.env.DEV
? 'http://localhost:3011'
: import.meta.env.PUBLIC_MANA_GAMES_BACKEND_URL || '';
const models = [
{ id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash', provider: 'Google', speed: 'Schnell' },
{ id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash', provider: 'Google', speed: 'Schnell' },
{ id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro', provider: 'Google', speed: 'Langsam' },
{ id: 'claude-3.5-haiku', label: 'Claude 3.5 Haiku', provider: 'Anthropic', speed: 'Schnell' },
{ id: 'claude-3.5-sonnet', label: 'Claude Sonnet', provider: 'Anthropic', speed: 'Mittel' },
{ id: 'gpt-4o-mini', label: 'GPT-4o Mini', provider: 'Azure', speed: 'Schnell' },
{ id: 'gpt-4o', label: 'GPT-4o', provider: 'Azure', speed: 'Mittel' },
];
let prompt = $state('');
let selectedModel = $state('gemini-2.0-flash');
let isGenerating = $state(false);
let generatedHtml = $state('');
let error = $state('');
let iterationCount = $state(0);
let originalPrompt = $state('');
async function generateGame() {
if (!prompt.trim() || isGenerating) return;
isGenerating = true;
error = '';
try {
const body: Record<string, unknown> = {
description: prompt,
model: selectedModel,
mode: iterationCount > 0 ? 'iterate' : 'create',
};
if (iterationCount > 0 && generatedHtml) {
body.originalPrompt = originalPrompt;
body.currentCode = generatedHtml;
body.iterationCount = iterationCount;
}
const response = await fetch(`${BACKEND_URL}/api/games/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Fehler: ${response.status} ${response.statusText}`);
}
const data = await response.json();
if (data.success && data.html) {
generatedHtml = data.html;
if (iterationCount === 0) {
originalPrompt = prompt;
}
iterationCount++;
} else {
error = data.error || 'Unbekannter Fehler bei der Generierung.';
}
} catch (err) {
error = err instanceof Error ? err.message : 'Verbindungsfehler zum Backend.';
} finally {
isGenerating = false;
}
}
async function saveGame() {
if (!generatedHtml || !prompt) return;
await generatedGameCollection.insert({
title: originalPrompt || prompt,
description: prompt,
htmlCode: generatedHtml,
prompt: originalPrompt || prompt,
model: selectedModel,
iterationCount,
});
// Reset
prompt = '';
generatedHtml = '';
iterationCount = 0;
originalPrompt = '';
}
function resetGame() {
generatedHtml = '';
iterationCount = 0;
originalPrompt = '';
prompt = '';
error = '';
}
</script>
<svelte:head>
<title>{$_('create.title')} - Arcade</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">{$_('create.title')}</h1>
<p class="text-muted-foreground mt-1">{$_('create.subtitle')}</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Input Panel -->
<div class="space-y-4">
<div>
<label for="prompt" class="block text-sm font-medium text-foreground mb-2">
{$_('create.prompt')}
</label>
<textarea
id="prompt"
bind:value={prompt}
placeholder={$_('create.promptPlaceholder')}
rows="4"
class="w-full rounded-lg border border-border bg-background px-4 py-3 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
></textarea>
</div>
<div>
<label for="model" class="block text-sm font-medium text-foreground mb-2">
{$_('create.model')}
</label>
<select
id="model"
bind:value={selectedModel}
class="w-full rounded-lg border border-border bg-background px-4 py-2.5 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
>
{#each models as model}
<option value={model.id}>
{model.label} ({model.provider} - {model.speed})
</option>
{/each}
</select>
</div>
{#if error}
<div class="rounded-lg border border-red-500/30 bg-red-500/10 p-3 text-sm text-red-400">
{error}
</div>
{/if}
<div class="flex gap-2">
<button
onclick={generateGame}
disabled={!prompt.trim() || isGenerating}
class="flex-1 px-4 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{#if isGenerating}
<span class="inline-flex items-center gap-2">
<span class="animate-spin"></span>
{$_('create.generating')}
</span>
{:else if iterationCount > 0}
{$_('create.iterate')}
{:else}
{$_('create.generate')}
{/if}
</button>
{#if generatedHtml}
<button
onclick={saveGame}
class="px-4 py-2.5 rounded-lg border border-border bg-card text-foreground hover:bg-muted transition-colors"
>
{$_('create.save')}
</button>
<button
onclick={resetGame}
class="px-4 py-2.5 rounded-lg border border-border bg-card text-muted-foreground hover:bg-muted transition-colors"
>
Neu
</button>
{/if}
</div>
{#if iterationCount > 0}
<p class="text-xs text-muted-foreground">
Iteration {iterationCount} &middot; Beschreibe Änderungen im Prompt-Feld
</p>
{/if}
</div>
<!-- Preview Panel -->
<div class="rounded-xl border border-border bg-black overflow-hidden">
{#if generatedHtml}
<iframe
srcdoc={generatedHtml}
title="Generiertes Spiel"
class="w-full aspect-[16/10] border-0"
sandbox="allow-scripts"
></iframe>
{:else}
<div class="w-full aspect-[16/10] flex items-center justify-center">
<div class="text-center">
<p class="text-4xl mb-3 opacity-40">🎮</p>
<p class="text-muted-foreground text-sm">{$_('create.preview')}</p>
</div>
</div>
{/if}
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { FeedbackPage } from '@manacore/feedback';
import { feedbackService } from '$lib/services/feedback';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Feedback - Arcade</title>
</svelte:head>
<FeedbackPage {feedbackService} appName="Arcade" currentUserId={authStore.user?.id} />

View file

@ -0,0 +1,47 @@
<svelte:head>
<title>Hilfe - Arcade</title>
</svelte:head>
<div class="max-w-2xl mx-auto space-y-6">
<h1 class="text-2xl font-bold text-foreground">Hilfe</h1>
<section class="space-y-4">
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">Wie spiele ich?</h2>
<p class="text-sm text-muted-foreground">
Wähle ein Spiel auf der Startseite aus und klicke darauf. Das Spiel läuft direkt im Browser.
Die Steuerung wird auf der Spielseite angezeigt.
</p>
</div>
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">KI-Spielgenerator</h2>
<p class="text-sm text-muted-foreground">
Unter "Erstellen" kannst du eigene Spiele beschreiben und von verschiedenen KI-Modellen
generieren lassen. Generierte Spiele werden lokal in deinem Browser gespeichert.
</p>
</div>
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">Statistiken</h2>
<p class="text-sm text-muted-foreground">
Deine Highscores, Spielzeiten und Fortschritte werden automatisch gespeichert. Melde dich
an, um sie geräteübergreifend zu synchronisieren.
</p>
</div>
<div class="rounded-xl border border-border bg-card p-4">
<h2 class="font-semibold text-foreground mb-2">Tastaturkürzel</h2>
<div class="grid grid-cols-2 gap-2 mt-2">
{#each [['Cmd/Ctrl+K', 'Schnellsuche'], ['Esc', 'Suche schließen']] as [key, desc]}
<div class="flex items-center gap-2">
<kbd class="px-2 py-0.5 rounded bg-muted text-xs font-mono text-muted-foreground"
>{key}</kbd
>
<span class="text-sm text-foreground">{desc}</span>
</div>
{/each}
</div>
</div>
</section>
</div>

View file

@ -0,0 +1,9 @@
<svelte:head>
<title>Mana - Arcade</title>
</svelte:head>
<div class="max-w-2xl mx-auto text-center py-12">
<p class="text-4xl mb-4">💎</p>
<h1 class="text-2xl font-bold text-foreground">Mana</h1>
<p class="text-muted-foreground mt-2">Demnächst verfügbar.</p>
</div>

View file

@ -0,0 +1,84 @@
<script lang="ts">
import { useAllGeneratedGames } from '$lib/data/queries';
import { generatedGameCollection } from '$lib/data/local-store';
const generatedGames = useAllGeneratedGames();
let selectedGameId = $state<string | null>(null);
let selectedGame = $derived(generatedGames.value.find((g) => g.id === selectedGameId));
async function deleteGame(id: string) {
await generatedGameCollection.remove(id);
if (selectedGameId === id) {
selectedGameId = null;
}
}
</script>
<svelte:head>
<title>Generierte Spiele - Arcade</title>
</svelte:head>
<div class="space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Generierte Spiele</h1>
<p class="text-muted-foreground mt-1">Deine mit KI erstellten Spiele</p>
</div>
{#if generatedGames.value.length === 0}
<div class="text-center py-12 rounded-xl border border-dashed border-border">
<p class="text-4xl mb-4"></p>
<p class="text-muted-foreground">Noch keine generierten Spiele.</p>
<a
href="/create"
class="inline-block mt-4 px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
Erstelle dein erstes Spiel!
</a>
</div>
{:else}
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="space-y-2 lg:col-span-1">
{#each generatedGames.value as game (game.id)}
<button
onclick={() => (selectedGameId = game.id)}
class="w-full text-left rounded-lg border p-3 transition-colors {selectedGameId ===
game.id
? 'border-primary bg-primary/5'
: 'border-border bg-card hover:bg-muted/50'}"
>
<p class="font-medium text-foreground text-sm truncate">{game.title}</p>
<p class="text-xs text-muted-foreground mt-1">
{game.model} &middot; {game.iterationCount} Iterationen
</p>
</button>
{/each}
</div>
<div class="lg:col-span-2 rounded-xl border border-border bg-black overflow-hidden">
{#if selectedGame}
<div class="flex items-center justify-between px-3 py-2 bg-card border-b border-border">
<span class="text-sm text-foreground truncate">{selectedGame.title}</span>
<button
onclick={() => deleteGame(selectedGame!.id)}
class="text-xs text-red-400 hover:text-red-300 transition-colors"
>
Löschen
</button>
</div>
<iframe
srcdoc={selectedGame.htmlCode}
title={selectedGame.title}
class="w-full aspect-[16/10] border-0"
sandbox="allow-scripts"
></iframe>
{:else}
<div class="w-full aspect-[16/10] flex items-center justify-center">
<p class="text-muted-foreground text-sm">Wähle ein Spiel aus der Liste</p>
</div>
{/if}
</div>
</div>
{/if}
</div>

View file

@ -0,0 +1,132 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount, onDestroy } from 'svelte';
import { _ } from 'svelte-i18n';
import { getGameBySlug } from '$lib/data/games';
import { initGameCommunication } from '$lib/services/game-communication';
import { gameStatsCollection, type LocalGameStats } from '$lib/data/local-store';
const slug = $derived($page.params.slug);
const game = $derived(getGameBySlug(slug));
let stats = $state<LocalGameStats | null>(null);
let isFullscreen = $state(false);
let iframeEl: HTMLIFrameElement;
let cleanup: (() => void) | undefined;
onMount(async () => {
if (!slug) return;
cleanup = initGameCommunication(slug);
const all = await gameStatsCollection.getAll();
stats = all.find((s) => s.gameId === slug) || null;
});
onDestroy(() => {
cleanup?.();
});
function toggleFullscreen() {
if (!iframeEl) return;
if (!document.fullscreenElement) {
iframeEl.requestFullscreen();
isFullscreen = true;
} else {
document.exitFullscreen();
isFullscreen = false;
}
}
function formatPlayTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
</script>
<svelte:head>
<title>{game?.title || 'Spiel'} - Arcade</title>
</svelte:head>
{#if game}
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<a href="/" class="text-muted-foreground hover:text-foreground transition-colors">
&larr; {$_('game.back')}
</a>
<h1 class="text-xl font-bold text-foreground">{game.title}</h1>
</div>
<div class="flex gap-2">
<button
onclick={toggleFullscreen}
class="px-3 py-1.5 text-sm rounded-lg border border-border bg-card text-foreground hover:bg-muted transition-colors"
>
{$_('game.fullscreen')}
</button>
</div>
</div>
<div class="rounded-xl overflow-hidden border border-border bg-black">
<iframe
bind:this={iframeEl}
src={game.htmlFile}
title={game.title}
class="w-full aspect-[16/10] border-0"
sandbox="allow-scripts allow-same-origin"
></iframe>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="rounded-xl border border-border bg-card p-4 space-y-3">
<h2 class="font-semibold text-foreground">{game.title}</h2>
<p class="text-sm text-muted-foreground">{game.description}</p>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.difficulty')}</span>
<span class="text-foreground">{game.difficulty}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.controls')}</span>
<span class="text-foreground text-right max-w-[60%]">{game.controls}</span>
</div>
</div>
<div class="flex flex-wrap gap-1 pt-2">
{#each game.tags as tag}
<span class="text-xs px-2 py-0.5 rounded-full bg-muted text-muted-foreground">
{tag}
</span>
{/each}
</div>
</div>
{#if stats}
<div class="rounded-xl border border-border bg-card p-4 space-y-3">
<h2 class="font-semibold text-foreground">{$_('game.stats')}</h2>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.highScore')}</span>
<span class="text-foreground font-mono">{stats.highScore.toLocaleString()}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.gamesPlayed')}</span>
<span class="text-foreground">{stats.gamesPlayed}</span>
</div>
<div class="flex justify-between">
<span class="text-muted-foreground">{$_('game.totalPlayTime')}</span>
<span class="text-foreground">{formatPlayTime(stats.totalPlayTime)}</span>
</div>
</div>
</div>
{/if}
</div>
</div>
{:else}
<div class="text-center py-12">
<p class="text-muted-foreground">Spiel nicht gefunden.</p>
<a href="/" class="text-primary hover:underline mt-2 inline-block">Zurück zur Übersicht</a>
</div>
{/if}

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { ProfilePage } from '@manacore/shared-profile-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
</script>
<svelte:head>
<title>Profil - Arcade</title>
</svelte:head>
{#if authStore.isAuthenticated}
<ProfilePage {authStore} {goto} />
{:else}
<div class="max-w-2xl mx-auto text-center py-12">
<p class="text-muted-foreground">Bitte melde dich an.</p>
<a href="/login" class="text-primary hover:underline mt-2 inline-block">Anmelden</a>
</div>
{/if}

View file

@ -0,0 +1,165 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { locale } from 'svelte-i18n';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { setLocale, supportedLocales } from '$lib/i18n';
import { goto } from '$app/navigation';
import { gameStatsCollection } from '$lib/data/local-store';
async function clearStats() {
const all = await gameStatsCollection.getAll();
for (const stat of all) {
await gameStatsCollection.remove(stat.id);
}
}
async function handleLogout() {
await authStore.signOut();
goto('/login');
}
</script>
<svelte:head>
<title>{$_('nav.settings')} - Arcade</title>
</svelte:head>
<div class="settings-page">
<header class="mb-8">
<h1 class="text-2xl font-bold text-foreground">{$_('nav.settings')}</h1>
<p class="text-muted-foreground text-sm mt-1">Passe Arcade an deine Bedürfnisse an</p>
</header>
<!-- Theme -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Darstellung</h2>
<div class="setting-row">
<div>
<div class="setting-label">Farbmodus</div>
<div class="setting-desc">Hell, Dunkel oder System</div>
</div>
<div class="flex gap-1">
{#each ['light', 'dark', 'system'] as mode}
<button
class="px-3 py-1.5 text-sm rounded-lg transition-colors {theme.mode === mode
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => theme.setMode(mode as 'light' | 'dark' | 'system')}
>
{mode === 'light' ? 'Hell' : mode === 'dark' ? 'Dunkel' : 'System'}
</button>
{/each}
</div>
</div>
</section>
<!-- Language -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Sprache</h2>
<div class="setting-row">
<div>
<div class="setting-label">App-Sprache</div>
<div class="setting-desc">Sprache der Benutzeroberfläche</div>
</div>
<select
value={$locale}
onchange={(e) => setLocale((e.target as HTMLSelectElement).value as any)}
class="h-9 px-3 rounded-lg bg-background border border-border text-foreground text-sm"
>
{#each supportedLocales as loc}
<option value={loc}>{loc === 'de' ? 'Deutsch' : 'English'}</option>
{/each}
</select>
</div>
</section>
<!-- Account -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Konto</h2>
{#if authStore.isAuthenticated}
<div class="setting-row">
<div>
<div class="setting-label">Eingeloggt als</div>
<div class="setting-desc">{authStore.user?.email}</div>
</div>
<button
class="px-4 py-2 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors text-sm"
onclick={handleLogout}
>
Abmelden
</button>
</div>
{:else}
<div class="setting-row">
<div>
<div class="setting-label">Gast-Modus</div>
<div class="setting-desc">Melde dich an, um Stats zu synchronisieren</div>
</div>
<a
href="/login"
class="px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors text-sm"
>
Anmelden
</a>
</div>
{/if}
</section>
<!-- Data -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Daten</h2>
<div class="setting-row">
<div>
<div class="setting-label">Spielstatistiken löschen</div>
<div class="setting-desc">Alle Highscores und Spielzeiten zurücksetzen</div>
</div>
<button
class="px-4 py-2 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors text-sm"
onclick={clearStats}
>
Löschen
</button>
</div>
</section>
</div>
<style>
.settings-page {
max-width: 600px;
margin: 0 auto;
}
.settings-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid hsl(var(--border));
}
.settings-section:last-child {
border-bottom: none;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 0;
}
.setting-label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.setting-desc {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-top: 2px;
}
</style>

View file

@ -0,0 +1,95 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { useAllGameStats } from '$lib/data/queries';
import { games } from '$lib/data/games';
const allStats = useAllGameStats();
let totalGamesPlayed = $derived(allStats.value.reduce((sum, s) => sum + s.gamesPlayed, 0));
let totalPlayTime = $derived(allStats.value.reduce((sum, s) => sum + s.totalPlayTime, 0));
let favoriteGame = $derived(() => {
if (allStats.value.length === 0) return null;
const top = allStats.value.reduce((fav, s) => (s.gamesPlayed > fav.gamesPlayed ? s : fav));
return games.find((g) => g.slug === top.gameId || g.id === top.gameId);
});
let sortedStats = $derived([...allStats.value].sort((a, b) => b.highScore - a.highScore));
function formatPlayTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function getGameTitle(gameId: string): string {
return games.find((g) => g.slug === gameId || g.id === gameId)?.title || gameId;
}
</script>
<svelte:head>
<title>{$_('stats.title')} - Arcade</title>
</svelte:head>
<div class="space-y-6">
<h1 class="text-2xl font-bold text-foreground">{$_('stats.title')}</h1>
{#if allStats.value.length === 0}
<div class="text-center py-12">
<p class="text-4xl mb-4">📊</p>
<p class="text-muted-foreground">{$_('stats.noStats')}</p>
</div>
{:else}
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="rounded-xl border border-border bg-card p-4 text-center">
<p class="text-3xl font-bold text-primary">{totalGamesPlayed}</p>
<p class="text-sm text-muted-foreground mt-1">{$_('stats.totalGames')}</p>
</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<p class="text-3xl font-bold text-primary">{formatPlayTime(totalPlayTime)}</p>
<p class="text-sm text-muted-foreground mt-1">{$_('stats.totalTime')}</p>
</div>
<div class="rounded-xl border border-border bg-card p-4 text-center">
<p class="text-3xl font-bold text-primary">{favoriteGame()?.title || '-'}</p>
<p class="text-sm text-muted-foreground mt-1">{$_('stats.favoriteGame')}</p>
</div>
</div>
<div class="rounded-xl border border-border bg-card overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-border text-left">
<th class="px-4 py-3 text-muted-foreground font-medium">Spiel</th>
<th class="px-4 py-3 text-muted-foreground font-medium text-right"
>{$_('game.highScore')}</th
>
<th class="px-4 py-3 text-muted-foreground font-medium text-right hidden sm:table-cell"
>{$_('game.gamesPlayed')}</th
>
<th class="px-4 py-3 text-muted-foreground font-medium text-right hidden md:table-cell"
>{$_('game.totalPlayTime')}</th
>
</tr>
</thead>
<tbody>
{#each sortedStats as stat (stat.id)}
<tr class="border-b border-border/50 hover:bg-muted/30 transition-colors">
<td class="px-4 py-3 text-foreground">{getGameTitle(stat.gameId)}</td>
<td class="px-4 py-3 text-foreground font-mono text-right"
>{stat.highScore.toLocaleString()}</td
>
<td class="px-4 py-3 text-muted-foreground text-right hidden sm:table-cell"
>{stat.gamesPlayed}</td
>
<td class="px-4 py-3 text-muted-foreground text-right hidden md:table-cell"
>{formatPlayTime(stat.totalPlayTime)}</td
>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>

View file

@ -0,0 +1,202 @@
<script lang="ts">
import { _ } from 'svelte-i18n';
import { authStore } from '$lib/stores/auth.svelte';
const BACKEND_URL = import.meta.env.DEV
? 'http://localhost:3011'
: import.meta.env.PUBLIC_MANA_GAMES_BACKEND_URL || '';
let title = $state('');
let description = $state('');
let controls = $state('');
let difficulty = $state<'Einfach' | 'Mittel' | 'Schwer'>('Mittel');
let tags = $state('');
let htmlCode = $state('');
let authorName = $state('');
let isSubmitting = $state(false);
let submitResult = $state<{ success: boolean; message: string } | null>(null);
async function handleSubmit() {
if (!title.trim() || !htmlCode.trim() || !authorName.trim()) return;
isSubmitting = true;
submitResult = null;
try {
const response = await fetch(`${BACKEND_URL}/api/games/submit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title,
description,
controls,
difficulty,
complexity: 'Mittel',
tags: tags
.split(',')
.map((t) => t.trim())
.filter(Boolean),
author: { name: authorName },
files: { html: htmlCode },
submittedAt: new Date().toISOString(),
}),
});
const data = await response.json();
submitResult = {
success: data.success,
message: data.success
? `Eingereicht! PR #${data.prNumber} erstellt.`
: data.error || 'Fehler beim Einreichen.',
};
if (data.success) {
title = '';
description = '';
controls = '';
tags = '';
htmlCode = '';
}
} catch {
submitResult = { success: false, message: 'Verbindungsfehler zum Backend.' };
} finally {
isSubmitting = false;
}
}
</script>
<svelte:head>
<title>Spiel einreichen - Arcade</title>
</svelte:head>
<div class="max-w-2xl mx-auto space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Spiel einreichen</h1>
<p class="text-muted-foreground mt-1">Reiche dein eigenes HTML5-Spiel bei der Community ein.</p>
</div>
{#if !authStore.isAuthenticated}
<div class="rounded-xl border border-border bg-card p-6 text-center">
<p class="text-muted-foreground mb-4">Bitte melde dich an, um ein Spiel einzureichen.</p>
<a
href="/login"
class="inline-block px-4 py-2 rounded-lg bg-primary text-primary-foreground hover:bg-primary/90 transition-colors"
>
Anmelden
</a>
</div>
{:else}
<form
onsubmit={(e) => {
e.preventDefault();
handleSubmit();
}}
class="space-y-4"
>
<div>
<label for="title" class="block text-sm font-medium text-foreground mb-1">Titel *</label>
<input
id="title"
type="text"
bind:value={title}
required
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="author" class="block text-sm font-medium text-foreground mb-1">Autor *</label>
<input
id="author"
type="text"
bind:value={authorName}
required
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="desc" class="block text-sm font-medium text-foreground mb-1">Beschreibung</label
>
<textarea
id="desc"
bind:value={description}
rows="3"
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
></textarea>
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="controls" class="block text-sm font-medium text-foreground mb-1"
>Steuerung</label
>
<input
id="controls"
type="text"
bind:value={controls}
placeholder="Pfeiltasten, Maus..."
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="difficulty" class="block text-sm font-medium text-foreground mb-1"
>Schwierigkeit</label
>
<select
id="difficulty"
bind:value={difficulty}
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
>
<option value="Einfach">Einfach</option>
<option value="Mittel">Mittel</option>
<option value="Schwer">Schwer</option>
</select>
</div>
</div>
<div>
<label for="tags" class="block text-sm font-medium text-foreground mb-1"
>Tags (kommagetrennt)</label
>
<input
id="tags"
type="text"
bind:value={tags}
placeholder="Arcade, Action, Puzzle"
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground focus:outline-none focus:ring-2 focus:ring-primary/50"
/>
</div>
<div>
<label for="html" class="block text-sm font-medium text-foreground mb-1">HTML-Code *</label>
<textarea
id="html"
bind:value={htmlCode}
rows="12"
required
placeholder="<!DOCTYPE html>..."
class="w-full rounded-lg border border-border bg-background px-4 py-2 text-foreground font-mono text-sm focus:outline-none focus:ring-2 focus:ring-primary/50 resize-none"
></textarea>
</div>
{#if submitResult}
<div
class="rounded-lg border p-3 text-sm {submitResult.success
? 'border-green-500/30 bg-green-500/10 text-green-400'
: 'border-red-500/30 bg-red-500/10 text-red-400'}"
>
{submitResult.message}
</div>
{/if}
<button
type="submit"
disabled={!title.trim() || !htmlCode.trim() || !authorName.trim() || isSubmitting}
class="w-full px-4 py-2.5 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? 'Wird eingereicht...' : 'Spiel einreichen'}
</button>
</form>
{/if}
</div>

View file

@ -0,0 +1,9 @@
<svelte:head>
<title>Tags - Arcade</title>
</svelte:head>
<div class="max-w-2xl mx-auto text-center py-12">
<p class="text-4xl mb-4">🏷️</p>
<h1 class="text-2xl font-bold text-foreground">Tags</h1>
<p class="text-muted-foreground mt-2">Demnächst verfügbar.</p>
</div>

View file

@ -0,0 +1,36 @@
<script lang="ts">
import { theme } from '$lib/stores/theme.svelte';
import { THEME_DEFINITIONS, EXTENDED_THEME_VARIANTS } from '@manacore/shared-theme';
import type { ThemeVariant } from '@manacore/shared-theme';
const allThemes = EXTENDED_THEME_VARIANTS;
</script>
<svelte:head>
<title>Themes - Arcade</title>
</svelte:head>
<div class="max-w-4xl mx-auto space-y-6">
<div>
<h1 class="text-2xl font-bold text-foreground">Themes</h1>
<p class="text-muted-foreground mt-1">Wähle ein Theme für Arcade</p>
</div>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{#each allThemes as variant}
{@const def = THEME_DEFINITIONS[variant]}
{#if def}
<button
onclick={() => theme.setVariant(variant)}
class="rounded-xl border p-4 text-left transition-all hover:-translate-y-0.5 {theme.variant ===
variant
? 'border-primary bg-primary/5 ring-2 ring-primary/30'
: 'border-border bg-card hover:border-primary/30'}"
>
<div class="text-2xl mb-2">{def.icon || '🎨'}</div>
<div class="font-medium text-foreground text-sm">{def.label}</div>
</button>
{/if}
{/each}
</div>
</div>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ForgotPasswordPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Arcade - Passwort vergessen</title>
</svelte:head>
<ForgotPasswordPage {authStore} {goto} appName="Arcade" loginHref="/login" />

View file

@ -0,0 +1,18 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { LoginPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Arcade - Login</title>
</svelte:head>
<LoginPage
{authStore}
{goto}
appName="Arcade"
registerHref="/register"
forgotPasswordHref="/forgot-password"
primaryColor="#00ff88"
/>

View file

@ -0,0 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { RegisterPage } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
</script>
<svelte:head>
<title>Arcade - Registrieren</title>
</svelte:head>
<RegisterPage {authStore} {goto} appName="Arcade" loginHref="/login" primaryColor="#00ff88" />

View file

@ -0,0 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
</script>
<svelte:head>
<title>Arcade - Passwort zurücksetzen</title>
</svelte:head>
<div class="min-h-screen flex items-center justify-center">
<div class="max-w-md w-full p-6 text-center">
<h1 class="text-xl font-bold text-foreground mb-4">Passwort zurücksetzen</h1>
<p class="text-muted-foreground mb-6">Funktion wird eingerichtet.</p>
<a href="/login" class="text-primary hover:underline">Zurück zum Login</a>
</div>
</div>

View file

@ -0,0 +1,39 @@
<script lang="ts">
import '../app.css';
import { onMount } from 'svelte';
import { isLoading as isLocaleLoading } from 'svelte-i18n';
import { theme } from '$lib/stores/theme.svelte';
import { authStore } from '$lib/stores/auth.svelte';
import { waitLocale } from '$lib/i18n';
import { ToastContainer, setupGlobalErrorHandler } from '@manacore/shared-ui';
import { AppLoadingSkeleton } from '$lib/components/skeletons';
let { children } = $props();
let loading = $state(true);
onMount(() => {
const cleanupErrorHandler = setupGlobalErrorHandler();
const init = async () => {
await waitLocale();
theme.initialize();
await authStore.initialize();
loading = false;
};
init();
return cleanupErrorHandler;
});
</script>
<ToastContainer />
{#if $isLocaleLoading || loading}
<AppLoadingSkeleton />
{:else}
<div class="min-h-screen bg-background text-foreground">
{@render children()}
</div>
{/if}

View file

@ -0,0 +1,2 @@
// Disable SSR — all data is local-first (IndexedDB + mana-sync)
export const ssr = false;

View file

@ -0,0 +1,14 @@
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
return new Response(
JSON.stringify({
status: 'ok',
service: 'arcade-web',
timestamp: new Date().toISOString(),
}),
{
headers: { 'Content-Type': 'application/json' },
}
);
};

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

View file

@ -0,0 +1,21 @@
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
out: 'build',
}),
prerender: {
handleHttpError: ({ path, message }) => {
if (path === '/favicon.png') return;
throw new Error(message);
},
},
},
};
export default config;

View file

@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

View file

@ -0,0 +1,33 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
import { createPWAConfig } from '@manacore/shared-pwa';
import { MANACORE_SHARED_PACKAGES, getBuildDefines } from '@manacore/shared-vite-config';
export default defineConfig({
plugins: [
sveltekit(),
SvelteKitPWA(
createPWAConfig({
name: 'Arcade - Browser-Spiele',
shortName: 'Arcade',
description: 'AI-powered Browser-Games Plattform',
themeColor: '#00ff88',
preset: 'minimal',
})
),
],
server: {
port: 5210,
strictPort: true,
},
ssr: {
noExternal: [...MANACORE_SHARED_PACKAGES],
},
optimizeDeps: {
exclude: [...MANACORE_SHARED_PACKAGES],
},
define: {
...getBuildDefines(),
},
});

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