diff --git a/games/mana-games/CLAUDE.md b/games/mana-games/CLAUDE.md new file mode 100644 index 000000000..9620552cc --- /dev/null +++ b/games/mana-games/CLAUDE.md @@ -0,0 +1,178 @@ +# Mana Games - CLAUDE.md + +AI-powered browser games platform mit 22+ Spielen und KI-Spielgenerierung. + +## Projektstruktur + +``` +games/mana-games/ +├── apps/ +│ ├── web/ # Astro PWA (@mana-games/web) +│ │ ├── src/ +│ │ │ ├── pages/ # Astro-Seiten +│ │ │ ├── layouts/ # Layout-Komponenten +│ │ │ ├── components/ +│ │ │ ├── data/ # Spielekatalog (games.ts) +│ │ │ └── services/ # Stats, etc. +│ │ └── public/ +│ │ ├── games/ # 22 HTML-Spiele +│ │ ├── screenshots/ +│ │ └── icons/ # PWA Icons +│ └── backend/ # NestJS API (@mana-games/backend) +│ └── src/ +│ ├── game-generator/ # AI-Spielgenerierung (OpenRouter) +│ ├── game-submission/ # Community-Einreichungen (GitHub API) +│ └── health/ +└── package.json # Root (mana-games) +``` + +## Entwicklung + +```bash +# Alles starten (Web + Backend) +pnpm mana-games:dev + +# Nur Web (Astro) +pnpm dev:mana-games:web + +# Nur Backend (NestJS) +pnpm dev:mana-games:backend + +# Web + Backend zusammen +pnpm dev:mana-games:app +``` + +**Ports:** +- Web: http://localhost:4321 +- Backend: http://localhost:3011 + +## API Endpoints + +| Endpoint | Method | Beschreibung | +|----------|--------|--------------| +| `/api/health` | GET | Health Check | +| `/api/games/generate` | POST | AI-Spielgenerierung | +| `/api/games/submit` | POST | Community-Einreichung | + +### POST /api/games/generate + +```json +{ + "description": "Ein Snake-Spiel im Neon-Stil", + "mode": "create", // oder "iterate" + "model": "gemini-2.0-flash", + "originalPrompt": "...", // nur bei iterate + "currentCode": "..." // nur bei iterate +} +``` + +**Unterstützte Modelle:** + +| Modell | Provider | Beschreibung | +|--------|----------|--------------| +| `gemini-2.0-flash` | Google | Schnell & günstig (Standard) | +| `gemini-2.5-flash` | Google | Schnell & gut | +| `gemini-2.5-pro` | Google | Höchste Qualität | +| `claude-3.5-haiku` | Anthropic | Schnell & präzise | +| `claude-3.5-sonnet` | Anthropic | Beste Code-Qualität | +| `gpt-4o-mini` | Azure OpenAI | Ausgewogen | +| `gpt-4o` | Azure OpenAI | Sehr gut | + +## Environment Variables + +Die Variablen werden zentral in `.env.development` verwaltet: + +```bash +MANA_GAMES_BACKEND_PORT=3011 + +# Google Gemini API +MANA_GAMES_GOOGLE_GENAI_API_KEY=your_key + +# Anthropic Claude API +MANA_GAMES_ANTHROPIC_API_KEY=your_key + +# Azure OpenAI API +MANA_GAMES_AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com +MANA_GAMES_AZURE_OPENAI_API_KEY=your_key +MANA_GAMES_AZURE_OPENAI_DEPLOYMENT=gpt-4o + +# GitHub (für Community-Einreichungen) +MANA_GAMES_GITHUB_TOKEN=your_token +MANA_GAMES_GITHUB_OWNER=tillschneider +MANA_GAMES_GITHUB_REPO=mana-games +``` + +Nach Änderungen: `pnpm setup:env` + +## Spiel hinzufügen + +1. HTML-Datei erstellen in `apps/web/public/games/spiel_name.html` +2. Screenshot in `apps/web/public/screenshots/spiel-name.jpg` +3. Registrieren in `apps/web/src/data/games.ts`: + +```typescript +{ + id: '23', + title: 'Spiel Titel', + description: 'Beschreibung', + slug: 'spiel-name', + htmlFile: '/games/spiel_name.html', + thumbnail: '/screenshots/spiel-name.jpg', + tags: ['Arcade', 'Action'], + difficulty: 'Mittel', + complexity: 'Einfach', + controls: 'Pfeiltasten zum Steuern' +} +``` + +## Spiel-postMessage Integration + +```javascript +// Beim Laden +window.parent.postMessage({ + type: 'GAME_LOADED', + gameId: 'spiel-slug' +}, '*'); + +// Bei Score-Update +window.parent.postMessage({ + type: 'GAME_EVENT', + gameId: 'spiel-slug', + event: 'SCORE_UPDATE', + data: { score: 123 } +}, '*'); + +// Bei Game Over +window.parent.postMessage({ + type: 'GAME_EVENT', + gameId: 'spiel-slug', + event: 'GAME_OVER', + data: { score: 123 } +}, '*'); +``` + +## Design + +**Farbschema:** +- Primary Background: `#0a0a0a` +- Secondary Background: `#1a1a1a` +- Accent: `#00ff88` +- Text: `#ffffff` +- Border: `#2a2a2a` + +## PWA + +- Manifest: `apps/web/public/manifest.json` +- Service Worker: `apps/web/public/sw.js` +- Icons in `apps/web/public/icons/` (72x72 bis 512x512) + +## Spielekatalog + +**22 Spiele** in folgenden Genres: +- Arcade +- Puzzle +- Tower Defense +- Idle/Incremental +- Jump 'n' Run +- Action +- Strategie diff --git a/games/mana-games/apps/backend/nest-cli.json b/games/mana-games/apps/backend/nest-cli.json new file mode 100644 index 000000000..f9aa683b1 --- /dev/null +++ b/games/mana-games/apps/backend/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/games/mana-games/apps/backend/package.json b/games/mana-games/apps/backend/package.json new file mode 100644 index 000000000..0470299c3 --- /dev/null +++ b/games/mana-games/apps/backend/package.json @@ -0,0 +1,33 @@ +{ + "name": "@mana-games/backend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "nest start --watch", + "build": "nest build", + "start": "nest start", + "start:prod": "node dist/main" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.65.0", + "@azure/openai": "^2.0.0", + "@google/genai": "^1.14.0", + "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", + "@nestjs/core": "^10.4.15", + "@nestjs/platform-express": "^10.4.15", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.4.9", + "@nestjs/schematics": "^10.2.3", + "@types/express": "^5.0.0", + "@types/node": "^22.10.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.2" + } +} diff --git a/games/mana-games/apps/backend/src/app.module.ts b/games/mana-games/apps/backend/src/app.module.ts new file mode 100644 index 000000000..caeb8ba86 --- /dev/null +++ b/games/mana-games/apps/backend/src/app.module.ts @@ -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 {} diff --git a/games/mana-games/apps/backend/src/game-generator/dto/generate-game.dto.ts b/games/mana-games/apps/backend/src/game-generator/dto/generate-game.dto.ts new file mode 100644 index 000000000..2462e7e97 --- /dev/null +++ b/games/mana-games/apps/backend/src/game-generator/dto/generate-game.dto.ts @@ -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; + }; +} diff --git a/games/mana-games/apps/backend/src/game-generator/game-generator.controller.ts b/games/mana-games/apps/backend/src/game-generator/game-generator.controller.ts new file mode 100644 index 000000000..d5cc09245 --- /dev/null +++ b/games/mana-games/apps/backend/src/game-generator/game-generator.controller.ts @@ -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 { + return this.gameGeneratorService.generateGame(dto); + } +} diff --git a/games/mana-games/apps/backend/src/game-generator/game-generator.module.ts b/games/mana-games/apps/backend/src/game-generator/game-generator.module.ts new file mode 100644 index 000000000..e3336f37c --- /dev/null +++ b/games/mana-games/apps/backend/src/game-generator/game-generator.module.ts @@ -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 {} diff --git a/games/mana-games/apps/backend/src/game-generator/game-generator.service.ts b/games/mana-games/apps/backend/src/game-generator/game-generator.service.ts new file mode 100644 index 000000000..b194dfb20 --- /dev/null +++ b/games/mana-games/apps/backend/src/game-generator/game-generator.service.ts @@ -0,0 +1,331 @@ +import { Injectable, BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { GenerateGameDto, GenerateGameResponseDto } from './dto/generate-game.dto'; +import { GoogleGenAI } from '@google/genai'; +import Anthropic from '@anthropic-ai/sdk'; +import { AzureOpenAI } from '@azure/openai'; + +type AIProvider = 'google' | 'anthropic' | 'azure'; + +interface ModelConfig { + provider: AIProvider; + modelId: string; + displayName: string; +} + +@Injectable() +export class GameGeneratorService { + private readonly logger = new Logger(GameGeneratorService.name); + + // Model configurations + private readonly modelConfigs: Record = { + // 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('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('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('AZURE_OPENAI_ENDPOINT'); + const azureApiKey = this.configService.get('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 { + 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 { + 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 { + 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 { + if (!this.azureClient) { + throw new InternalServerErrorException('Azure OpenAI client not initialized'); + } + + const deployment = this.configService.get('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: + + + + Spielname + + + + + + + + +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: + + + + Spielname + + + + + + + + +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('')) { + throw new BadRequestException('Invalid game HTML structure'); + } + + // Security sanitization + const sanitized = html + .replace(/]*src=[^>]*>/gi, '') + .replace(/]*href=[^>]*>/gi, '') + .replace(/fetch\s*\(/gi, '// fetch disabled: fetch(') + .replace(/XMLHttpRequest/gi, '// XMLHttpRequest disabled') + .replace(/eval\s*\(/gi, '// eval disabled: eval('); + + return sanitized; + } +} diff --git a/games/mana-games/apps/backend/src/game-submission/dto/submit-game.dto.ts b/games/mana-games/apps/backend/src/game-submission/dto/submit-game.dto.ts new file mode 100644 index 000000000..cfb922e97 --- /dev/null +++ b/games/mana-games/apps/backend/src/game-submission/dto/submit-game.dto.ts @@ -0,0 +1,72 @@ +import { IsString, IsArray, IsOptional, IsObject, ValidateNested, IsIn } from 'class-validator'; +import { Type } from 'class-transformer'; + +class AuthorDto { + @IsString() + name: string; + + @IsOptional() + @IsString() + email?: string; + + @IsOptional() + @IsString() + github?: string; +} + +class FileDto { + @IsString() + name: string; + + @IsString() + content: string; +} + +class FilesDto { + @ValidateNested() + @Type(() => FileDto) + html: FileDto; + + @ValidateNested() + @Type(() => FileDto) + screenshot: FileDto; +} + +export class SubmitGameDto { + @IsString() + title: string; + + @IsString() + description: string; + + @IsString() + controls: string; + + @IsIn(['Einfach', 'Mittel', 'Schwer']) + difficulty: string; + + @IsIn(['Minimal', 'Einfach', 'Mittel', 'Komplex']) + complexity: string; + + @IsArray() + @IsString({ each: true }) + tags: string[]; + + @ValidateNested() + @Type(() => AuthorDto) + author: AuthorDto; + + @ValidateNested() + @Type(() => FilesDto) + files: FilesDto; + + @IsString() + submittedAt: string; +} + +export class SubmitGameResponseDto { + success: boolean; + message: string; + prUrl: string; + prNumber: number; +} diff --git a/games/mana-games/apps/backend/src/game-submission/game-submission.controller.ts b/games/mana-games/apps/backend/src/game-submission/game-submission.controller.ts new file mode 100644 index 000000000..574d91bb7 --- /dev/null +++ b/games/mana-games/apps/backend/src/game-submission/game-submission.controller.ts @@ -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 { + return this.gameSubmissionService.submitGame(dto); + } +} diff --git a/games/mana-games/apps/backend/src/game-submission/game-submission.module.ts b/games/mana-games/apps/backend/src/game-submission/game-submission.module.ts new file mode 100644 index 000000000..ef8df428f --- /dev/null +++ b/games/mana-games/apps/backend/src/game-submission/game-submission.module.ts @@ -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 {} diff --git a/games/mana-games/apps/backend/src/game-submission/game-submission.service.ts b/games/mana-games/apps/backend/src/game-submission/game-submission.service.ts new file mode 100644 index 000000000..4db0beb70 --- /dev/null +++ b/games/mana-games/apps/backend/src/game-submission/game-submission.service.ts @@ -0,0 +1,219 @@ +import { Injectable, BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { SubmitGameDto, SubmitGameResponseDto } from './dto/submit-game.dto'; + +@Injectable() +export class GameSubmissionService { + private readonly logger = new Logger(GameSubmissionService.name); + + constructor(private readonly configService: ConfigService) {} + + async submitGame(dto: SubmitGameDto): Promise { + const githubToken = this.configService.get('GITHUB_TOKEN'); + const githubOwner = this.configService.get('GITHUB_OWNER') || 'tillschneider'; + const githubRepo = this.configService.get('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')); + } + } +} diff --git a/games/mana-games/apps/backend/src/health/health.controller.ts b/games/mana-games/apps/backend/src/health/health.controller.ts new file mode 100644 index 000000000..e32b9644a --- /dev/null +++ b/games/mana-games/apps/backend/src/health/health.controller.ts @@ -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', + }; + } +} diff --git a/games/mana-games/apps/backend/src/health/health.module.ts b/games/mana-games/apps/backend/src/health/health.module.ts new file mode 100644 index 000000000..7476abedd --- /dev/null +++ b/games/mana-games/apps/backend/src/health/health.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { HealthController } from './health.controller'; + +@Module({ + controllers: [HealthController], +}) +export class HealthModule {} diff --git a/games/mana-games/apps/backend/src/main.ts b/games/mana-games/apps/backend/src/main.ts new file mode 100644 index 000000000..3ff9aca66 --- /dev/null +++ b/games/mana-games/apps/backend/src/main.ts @@ -0,0 +1,37 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + + // CORS configuration + app.enableCors({ + origin: [ + 'http://localhost:4321', // Astro dev + 'http://localhost:3000', // Alternative dev + /\.netlify\.app$/, // Legacy Netlify + ], + methods: ['GET', 'POST', 'OPTIONS'], + credentials: false, + }); + + app.setGlobalPrefix('api'); + + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); + + const port = process.env.PORT || 3010; + + // Increase timeout for long-running AI requests (2 minutes) + const server = await app.listen(port); + server.setTimeout(120000); + + console.log(`Mana Games backend running on http://localhost:${port}`); +} +bootstrap(); diff --git a/games/mana-games/apps/backend/tsconfig.json b/games/mana-games/apps/backend/tsconfig.json new file mode 100644 index 000000000..a3fd2c514 --- /dev/null +++ b/games/mana-games/apps/backend/tsconfig.json @@ -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 + } +} diff --git a/games/mana-games/apps/web/.env.example b/games/mana-games/apps/web/.env.example new file mode 100644 index 000000000..b5ff4f29e --- /dev/null +++ b/games/mana-games/apps/web/.env.example @@ -0,0 +1,11 @@ +# OpenRouter API Key +# Get your API key from https://openrouter.ai/keys +OPENROUTER_API_KEY=your_api_key_here + +# GitHub API Token (for community submissions) +# Create a personal access token with 'repo' scope at https://github.com/settings/tokens +GITHUB_TOKEN=your_github_token_here + +# GitHub Repository Settings (optional - defaults to current repo) +GITHUB_OWNER=your_github_username +GITHUB_REPO=mana-games \ No newline at end of file diff --git a/games/mana-games/apps/web/astro.config.mjs b/games/mana-games/apps/web/astro.config.mjs new file mode 100644 index 000000000..e762ba5cf --- /dev/null +++ b/games/mana-games/apps/web/astro.config.mjs @@ -0,0 +1,5 @@ +// @ts-check +import { defineConfig } from 'astro/config'; + +// https://astro.build/config +export default defineConfig({}); diff --git a/games/mana-games/apps/web/package.json b/games/mana-games/apps/web/package.json new file mode 100644 index 000000000..68f88418c --- /dev/null +++ b/games/mana-games/apps/web/package.json @@ -0,0 +1,18 @@ +{ + "name": "@mana-games/web", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview", + "astro": "astro" + }, + "dependencies": { + "astro": "^5.10.1" + }, + "devDependencies": { + "sharp": "^0.34.2" + } +} diff --git a/games/mana-games/apps/web/public/favicon.svg b/games/mana-games/apps/web/public/favicon.svg new file mode 100644 index 000000000..c86b31ede --- /dev/null +++ b/games/mana-games/apps/web/public/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/games/mana-games/apps/web/public/games/asteroid_dash.html b/games/mana-games/apps/web/public/games/asteroid_dash.html new file mode 100644 index 000000000..05d3b7751 --- /dev/null +++ b/games/mana-games/apps/web/public/games/asteroid_dash.html @@ -0,0 +1,666 @@ + + + + Asteroid Dash + + + + + +
+
Score: 0
+
Lives: 3
+
+ +
+ WASD / Pfeiltasten: Bewegung | Leertaste: Boost +
+ +
+

Game Over!

+

Endpunktzahl: 0

+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/balloon_pop.html b/games/mana-games/apps/web/public/games/balloon_pop.html new file mode 100644 index 000000000..633caf5c3 --- /dev/null +++ b/games/mana-games/apps/web/public/games/balloon_pop.html @@ -0,0 +1,677 @@ + + + + Balloon Pop + + + + + +
+
🎈 Punkte: 0
+
Level: 1
+
Combo: 0x
+
+ +
+ +
+ +
+ 🖱️ Klicke auf die Ballons zum Platzen lassen! +
+ +
+

🎉 Spiel beendet!

+

Endpunktzahl: 0

+

Erreichte Level: 1

+

+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/bounce_catch_tutorial.html b/games/mana-games/apps/web/public/games/bounce_catch_tutorial.html new file mode 100644 index 000000000..1e1850009 --- /dev/null +++ b/games/mana-games/apps/web/public/games/bounce_catch_tutorial.html @@ -0,0 +1,491 @@ + + + + + + Bounce & Catch - Tutorial Game + + + +
+ +
+ Punkte: 0 | + Leben: 3 +
+ + + + + +
+

Bounce & Catch Tutorial

+

Ein einfaches Lernspiel, das die Grundlagen der Spieleentwicklung zeigt!

+

Steuerung: Bewege die Maus, um das Paddle zu steuern

+

Ziel: Fange den Ball mit dem Paddle auf, bevor er unten aus dem Bild fällt

+

Je länger du spielst, desto schneller wird der Ball!

+ +
+ + +
+

Game Over!

+
Endpunktzahl: 0
+ +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/card_stack_rush.html b/games/mana-games/apps/web/public/games/card_stack_rush.html new file mode 100644 index 000000000..57c106dc6 --- /dev/null +++ b/games/mana-games/apps/web/public/games/card_stack_rush.html @@ -0,0 +1,710 @@ + + + + + + Card Stack Rush + + + +
+

Card Stack Rush

+ +
+
+
+ Punkte: + 0 +
+
+ Karten: + 0 +
+
+ Zeit: +
+
+
+
+
+ + +
+
+ +
+
Sortiere nach Farbe!
+ +
+
+ +
+
+
+ +
+
Combo x2!
+
+ +
+
+

Zeit abgelaufen!

+
+

Endpunktzahl: 0

+

Platzierte Karten: 0

+

Höchste Combo: 0

+
+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/click_race.html b/games/mana-games/apps/web/public/games/click_race.html new file mode 100644 index 000000000..ba9834b72 --- /dev/null +++ b/games/mana-games/apps/web/public/games/click_race.html @@ -0,0 +1,180 @@ + + + + Click Race + + + +
+

CLICK RACE

+

30 Klicks so schnell wie möglich!

+
+

Klicke zum Starten!

+ +
+ + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/color_memory.html b/games/mana-games/apps/web/public/games/color_memory.html new file mode 100644 index 000000000..08494988b --- /dev/null +++ b/games/mana-games/apps/web/public/games/color_memory.html @@ -0,0 +1,144 @@ + + + + Color Memory + + + +

COLOR MEMORY

+

Merke dir die Reihenfolge!

+
+
+
+
+
+
+

Level: 1

+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/fish_catcher.html b/games/mana-games/apps/web/public/games/fish_catcher.html new file mode 100644 index 000000000..41999e3f6 --- /dev/null +++ b/games/mana-games/apps/web/public/games/fish_catcher.html @@ -0,0 +1,697 @@ + + + + Fish Catcher + + + + + +
+
🐟 Fische: 0
+
❤️ Leben: 3
+
+ +
+ ⏰ Zeit: 60s +
+ +
+ A/D oder ← → : Boot bewegen | Maus: Alternative Steuerung +
+ +
+

🎣 Angeltag beendet!

+

Gefangene Fische: 0

+

+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/flappy_mana.html b/games/mana-games/apps/web/public/games/flappy_mana.html new file mode 100644 index 000000000..070d7f781 --- /dev/null +++ b/games/mana-games/apps/web/public/games/flappy_mana.html @@ -0,0 +1,489 @@ + + + + + + Flappy Mana + + + +
+
SCORE: 0
+ + +
+

FLAPPY MANA

+

Klicke oder drücke SPACE zum Fliegen

+

Weiche den Röhren aus!

+ +
+ +
+

GAME OVER

+
SCORE: 0
+
BEST: 0
+ +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/game-stats-example.html b/games/mana-games/apps/web/public/games/game-stats-example.html new file mode 100644 index 000000000..a0750cecd --- /dev/null +++ b/games/mana-games/apps/web/public/games/game-stats-example.html @@ -0,0 +1,202 @@ + + + + + + Game Stats Integration Example + + + +
+

Game Stats Integration Guide

+ +
+

1. Spiel laden

+

Sende diese Nachricht wenn dein Spiel startet:

+
window.parent.postMessage({
+    type: 'GAME_LOADED',
+    gameId: 'dein-spiel-slug'
+}, '*');
+
+ +
+

2. Score aktualisieren

+

Sende diese Nachricht wenn sich der Score ändert:

+
window.parent.postMessage({
+    type: 'GAME_EVENT',
+    gameId: 'dein-spiel-slug',
+    event: 'SCORE_UPDATE',
+    data: { score: 1250 }
+}, '*');
+
+ +
+

3. Game Over

+

Sende diese Nachricht wenn das Spiel endet:

+
window.parent.postMessage({
+    type: 'GAME_EVENT',
+    gameId: 'dein-spiel-slug',
+    event: 'GAME_OVER',
+    data: { score: 1250 }
+}, '*');
+
+ +
+

4. Achievement freischalten

+

Sende diese Nachricht für Achievements:

+
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'
+        }
+    }
+}, '*');
+
+ +
+

5. Spiel beenden

+

Optional: Sende diese Nachricht beim Verlassen:

+
window.parent.postMessage({
+    type: 'GAME_ENDED',
+    gameId: 'dein-spiel-slug'
+}, '*');
+
+ +
+

Test-Buttons

+

Teste die Integration mit diesen Buttons:

+
+ + + + + +
+
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/gravity_painter.html b/games/mana-games/apps/web/public/games/gravity_painter.html new file mode 100644 index 000000000..f17a7b98a --- /dev/null +++ b/games/mana-games/apps/web/public/games/gravity_painter.html @@ -0,0 +1,483 @@ + + + + + + Gravity Painter + + + +
+ +
+
Score: 0
+
Level: 1
+
Particles: 10
+
+
+ Klicke um Gravitationspunkte zu setzen • Leertaste für Partikel • Treffe die grünen Ziele! +
+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/mana_defense.html b/games/mana-games/apps/web/public/games/mana_defense.html new file mode 100644 index 000000000..41c06b479 --- /dev/null +++ b/games/mana-games/apps/web/public/games/mana_defense.html @@ -0,0 +1,1124 @@ + + + + + + Mana Defense + + + +
+ + +
+
💎 Mana: 100
+
❤️ Leben: 20
+
🌊 Welle: 1 / 20
+
🏆 Punkte: 0
+
+ +
+
+
⚡ Blitz
+
💎 50
+
+
+
❄️ Frost
+
💎 75
+
+
+
🔥 Feuer
+
💎 100
+
+
+
💰 Verkaufen
+
50% zurück
+
+
+ +
+

🏰 Mana Defense 🏰

+

Verteidige deinen Mana-Kristall!

+

Anleitung:

+

1. Wähle einen Turm aus dem Menü

+

2. Platziere ihn auf dem Spielfeld

+

3. Upgrade Türme durch erneutes Anklicken

+

⚡ Blitz: Schnell, Einzelziel

+

❄️ Frost: Verlangsamt Gegner

+

🔥 Feuer: Flächenschaden

+ +
+ +
+

Game Over!

+

Erreichte Welle: 0

+

Punkte: 0

+ +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/mana_factory.html b/games/mana-games/apps/web/public/games/mana_factory.html new file mode 100644 index 000000000..de64a3fb3 --- /dev/null +++ b/games/mana-games/apps/web/public/games/mana_factory.html @@ -0,0 +1,966 @@ + + + + + + Mana Factory + + + +
+ +
+
+

🏭 Mana Factory 🏭

+ +
+ 💎 0 Mana +
+ +
+ 0 Mana/Sek +
+ + + +
+
Generatoren
+
Upgrades
+
Aufstieg
+
+ +
+
+
+ 💧 Mana-Brunnen + 0 +
+
+ Produziert: 1 Mana/s + Kosten: 10 +
+
+ +
+
+ ⛏️ Kristall-Mine + 0 +
+
+ Produziert: 10 Mana/s + Kosten: 100 +
+
+ +
+
+ ⚡ Mana-Reaktor + 0 +
+
+ Produziert: 100 Mana/s + Kosten: 1000 +
+
+ +
+
+ 🌀 Dimensions-Portal + 0 +
+
+ Produziert: 1000 Mana/s + Kosten: 10000 +
+
+ +
+
+ 🌟 Mana-Nexus + 0 +
+
+ Produziert: 10000 Mana/s + Kosten: 100000 +
+
+
+ +
+

Verbesserungen

+
+
+ +
+

Aufstieg

+

+ Setze deinen Fortschritt zurück und erhalte Prestige-Punkte für permanente Boni! +

+

+ Prestige-Punkte: 0 +

+

+ Nächster Aufstieg: 0 Punkte +

+

+ Multiplikator: x1 +

+ +
+
+ +
+

Statistiken

+
+
+ Gesamtes Mana: + 0 +
+
+ Mana durch Klicks: + 0 +
+
+ Mana durch Generatoren: + 0 +
+
+ Klicks gesamt: + 0 +
+
+ Spielzeit: + 0:00 +
+
+ Aufstiege: + 0 +
+
+ +

Erfolge

+
+
+
🎯 Erster Klick
+
Ernte deinen ersten Kristall
+
+
+
🏗️ Industrialisierung
+
Kaufe deinen ersten Generator
+
+
+
💯 Hundert!
+
Erreiche 100 Mana
+
+
+
📈 Tausender
+
Erreiche 1,000 Mana
+
+
+
⭐ Aufgestiegen
+
Führe deinen ersten Aufstieg durch
+
+
+
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/mana_runner.html b/games/mana-games/apps/web/public/games/mana_runner.html new file mode 100644 index 000000000..b9a176565 --- /dev/null +++ b/games/mana-games/apps/web/public/games/mana_runner.html @@ -0,0 +1,569 @@ + + + + + + Mana Runner + + + + + +
+

🏃‍♂️ Mana Runner 🏃‍♂️

+

Sammle Mana-Kristalle und weiche Hindernissen aus!

+

Steuerung:

+

Leertaste = Springen

+

Doppelsprung verfügbar nach 10 Kristallen!

+ +
+ +
+

Game Over!

+

Punkte: 0

+

Kristalle: 0

+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/memory_card_match.html b/games/mana-games/apps/web/public/games/memory_card_match.html new file mode 100644 index 000000000..556f2e4a3 --- /dev/null +++ b/games/mana-games/apps/web/public/games/memory_card_match.html @@ -0,0 +1,508 @@ + + + + + + Memory Card Match + + + +
+

Memory Card Match

+ +
+
+
+ Zeit: + 0:00 +
+
+ Züge: + 0 +
+
+ Paare: + 0/8 +
+
+ +
+ + + +
+ + +
+
+ +
+
+
+ +
+
+

🎉 Gewonnen! 🎉

+
+

Zeit:

+

Züge:

+

Effizienz: %

+
+ +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/neon_maze_runner.html b/games/mana-games/apps/web/public/games/neon_maze_runner.html new file mode 100644 index 000000000..92b4f5442 --- /dev/null +++ b/games/mana-games/apps/web/public/games/neon_maze_runner.html @@ -0,0 +1,886 @@ + + + + + + Neon Maze Runner + + + +
+
+
PUNKTE: 0
+
LEVEL: 1
+
ZEIT: 60s
+
+ + + +
+ +
+

NEON MAZE RUNNER

+
+

Steuerung: WASD oder Pfeiltasten

+

Ziel: Sammle alle Diamanten und finde den Ausgang!

+

Tipp: Achte auf Power-ups und die Zeit!

+
+
+
+
💎
+
Diamanten
+100 Punkte
+
+
+
+
Speed Boost
2x Geschwindigkeit
+
+
+
🕐
+
Zeitbonus
+15 Sekunden
+
+
+ +
+ +
+

GAME OVER

+
+

Erreichte Punkte: 0

+

Erreichte Level: 1

+
+ +
+ +
+

LEVEL GESCHAFFT!

+
+

Level Punkte: 0

+

Zeit Bonus: 0

+
+ +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/puzzle_blocks.html b/games/mana-games/apps/web/public/games/puzzle_blocks.html new file mode 100644 index 000000000..8509e3a17 --- /dev/null +++ b/games/mana-games/apps/web/public/games/puzzle_blocks.html @@ -0,0 +1,636 @@ + + + + + + Puzzle Blocks - Mana Games + + + +
+
+ +
+

PUZZLE BLOCKS

+

Klassisches Tetris-Gameplay

+ +
+
+

GAME OVER

+

Deine Punkte: 0

+ +
+
+ +
+
+

Punkte

+
0
+
+ +
+

Level

+
Level 1
+
Linien: 0
+
+ +
+

Nächster Block

+
+ +
+
+ +
+

Steuerung

+

Bewegen

+

Schneller fallen

+

Drehen

+

Space Sofort fallen

+

P Pause

+
+
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/reaction_test.html b/games/mana-games/apps/web/public/games/reaction_test.html new file mode 100644 index 000000000..934d88f4e --- /dev/null +++ b/games/mana-games/apps/web/public/games/reaction_test.html @@ -0,0 +1,138 @@ + + + + Reaction Test + + + +
+

REACTION TEST

+

Klicke wenn der Bildschirm GRÜN wird!

+

+

+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/rhythm_defender.html b/games/mana-games/apps/web/public/games/rhythm_defender.html new file mode 100644 index 000000000..dfeacb841 --- /dev/null +++ b/games/mana-games/apps/web/public/games/rhythm_defender.html @@ -0,0 +1,795 @@ + + + + + + Rhythm Defender + + + +
+
+
SCORE: 0
+
COMBO: 0x
+
+
LEBEN
+
+
+
+
+
+ + + +
BEAT
+
+ +
+

RHYTHM DEFENDER

+
+

Verteidige dich im Rhythmus der Musik!

+

Drücke die richtigen Tasten im Takt:

+
+
A
+
S
+
D
+
F
+
+

Treffe die Noten wenn sie die Ziellinie erreichen!

+

PERFECT = 100 Punkte + Combo

+

GOOD = 50 Punkte

+
+ +
+ +
+

GAME OVER

+

Finaler Score: 0

+

Maximale Combo: 0

+ +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/snake_game.html b/games/mana-games/apps/web/public/games/snake_game.html new file mode 100644 index 000000000..262c2702f --- /dev/null +++ b/games/mana-games/apps/web/public/games/snake_game.html @@ -0,0 +1,662 @@ + + + + + + Snake Spiel + + + +
+
SCORE: 0
+ +
+
GAME OVER
+
SCORE: 0
+ +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/space_defender_game.html b/games/mana-games/apps/web/public/games/space_defender_game.html new file mode 100644 index 000000000..b1eca6faa --- /dev/null +++ b/games/mana-games/apps/web/public/games/space_defender_game.html @@ -0,0 +1,508 @@ + + + + + + Space Defender + + + +
+ +
+
Score: 0
+
Schwierigkeit: 1.0
+
Zeit: 0s
+
+
+

GAME OVER

+

Finaler Score: 0

+ +
+
+
+

🎮 Steuerung: A/D oder ←/→ zum Bewegen • LEERTASTE zum Schießen

+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/turbo_racer.html b/games/mana-games/apps/web/public/games/turbo_racer.html new file mode 100644 index 000000000..6327e2279 --- /dev/null +++ b/games/mana-games/apps/web/public/games/turbo_racer.html @@ -0,0 +1,791 @@ + + + + + + Turbo Racer + + + +
+ + +
+
+ 0 km/h +
+
+ Runde: 0 +
+
+ Zeit: 0:00 +
+
+ Beste Runde: --:-- +
+
+ +
+
+
+ +
+

TURBO RACER

+

Drift durch die Kurven und stelle Bestzeiten auf!

+

🏁 Endlos-Runden • ⚡ Nitro-Boost • 🏆 Drift-Punkte

+ +
+ +
+

ZEIT-HERAUSFORDERUNG BEENDET!

+
0 Runden
+

Beste Runde: --:--

+ +
+ +
+ ↑↓ oder WS: Gas/Bremse | ←→ oder AD: Lenken | Leertaste: Boost +
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/games/word_scramble.html b/games/mana-games/apps/web/public/games/word_scramble.html new file mode 100644 index 000000000..7c9702efd --- /dev/null +++ b/games/mana-games/apps/web/public/games/word_scramble.html @@ -0,0 +1,1274 @@ + + + + + + Word Scramble + + + +
+ COMBO x0 +
+ +
+ +
+ +
+
+

WORD SCRAMBLE

+
+ +
+
+
Punkte: 0
+
+
+
Level: 1
+
+
+
Zeit: 60s
+
+
+ +
+
+ Kategorie: Tiere +
+ + +
+ +
+
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/icons/icon-120x120.png b/games/mana-games/apps/web/public/icons/icon-120x120.png new file mode 100644 index 000000000..172bbcaf2 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-120x120.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-128x128.png b/games/mana-games/apps/web/public/icons/icon-128x128.png new file mode 100644 index 000000000..7d28b8534 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-128x128.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-144x144.png b/games/mana-games/apps/web/public/icons/icon-144x144.png new file mode 100644 index 000000000..63165eb24 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-144x144.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-152x152.png b/games/mana-games/apps/web/public/icons/icon-152x152.png new file mode 100644 index 000000000..4b81c5755 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-152x152.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-167x167.png b/games/mana-games/apps/web/public/icons/icon-167x167.png new file mode 100644 index 000000000..9063b8c72 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-167x167.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-180x180.png b/games/mana-games/apps/web/public/icons/icon-180x180.png new file mode 100644 index 000000000..81e7610cb Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-180x180.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-192x192.png b/games/mana-games/apps/web/public/icons/icon-192x192.png new file mode 100644 index 000000000..355007c45 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-192x192.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-384x384.png b/games/mana-games/apps/web/public/icons/icon-384x384.png new file mode 100644 index 000000000..aac3099d2 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-384x384.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-512x512.png b/games/mana-games/apps/web/public/icons/icon-512x512.png new file mode 100644 index 000000000..7195360c9 Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-512x512.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-72x72.png b/games/mana-games/apps/web/public/icons/icon-72x72.png new file mode 100644 index 000000000..bc2d228cb Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-72x72.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-96x96.png b/games/mana-games/apps/web/public/icons/icon-96x96.png new file mode 100644 index 000000000..4437e7bed Binary files /dev/null and b/games/mana-games/apps/web/public/icons/icon-96x96.png differ diff --git a/games/mana-games/apps/web/public/icons/icon-base.svg b/games/mana-games/apps/web/public/icons/icon-base.svg new file mode 100644 index 000000000..e4ff4c7d1 --- /dev/null +++ b/games/mana-games/apps/web/public/icons/icon-base.svg @@ -0,0 +1,10 @@ + + + + + + + + + MG + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/manifest.json b/games/mana-games/apps/web/public/manifest.json new file mode 100644 index 000000000..1714d7da9 --- /dev/null +++ b/games/mana-games/apps/web/public/manifest.json @@ -0,0 +1,89 @@ +{ + "name": "Mana Games - Spiele ohne Grenzen", + "short_name": "Mana Games", + "description": "Eine Sammlung kostenloser, werbefreier Web-Spiele zum Spielen, Bauen und Lernen", + "start_url": "/", + "display": "standalone", + "orientation": "portrait", + "theme_color": "#1a1a1a", + "background_color": "#0a0a0a", + "categories": ["games", "education", "entertainment"], + "lang": "de", + "dir": "ltr", + "icons": [ + { + "src": "/icons/icon-72x72.png", + "sizes": "72x72", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-96x96.png", + "sizes": "96x96", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-128x128.png", + "sizes": "128x128", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-144x144.png", + "sizes": "144x144", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-152x152.png", + "sizes": "152x152", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-384x384.png", + "sizes": "384x384", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "screenshots": [ + { + "src": "/screenshots/desktop-home.png", + "sizes": "1280x720", + "type": "image/png", + "label": "Mana Games Startseite" + }, + { + "src": "/screenshots/mobile-home.png", + "sizes": "750x1334", + "type": "image/png", + "label": "Mobile Ansicht" + } + ], + "shortcuts": [ + { + "name": "Snake Game", + "url": "/games/snake", + "description": "Klassisches Snake-Spiel spielen" + }, + { + "name": "Meine Statistiken", + "url": "/stats", + "description": "Spielstatistiken anzeigen" + } + ] +} \ No newline at end of file diff --git a/games/mana-games/apps/web/public/offline.html b/games/mana-games/apps/web/public/offline.html new file mode 100644 index 000000000..c1b60aa92 --- /dev/null +++ b/games/mana-games/apps/web/public/offline.html @@ -0,0 +1,184 @@ + + + + + + Offline - Mana Games + + + +
+
📡
+

Du bist offline

+

+ Keine Internetverbindung gefunden. Aber keine Sorge! + Einige Spiele, die du bereits gespielt hast, sind möglicherweise + noch im Cache verfügbar. +

+ + + +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/public/screenshots/asteroid-dash.jpg b/games/mana-games/apps/web/public/screenshots/asteroid-dash.jpg new file mode 100644 index 000000000..43c29ba2e Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/asteroid-dash.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/balloon-pop.jpg b/games/mana-games/apps/web/public/screenshots/balloon-pop.jpg new file mode 100644 index 000000000..2f1d48f1c Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/balloon-pop.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/bounce-catch.jpg b/games/mana-games/apps/web/public/screenshots/bounce-catch.jpg new file mode 100644 index 000000000..38c1e73be Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/bounce-catch.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/click-race.jpg b/games/mana-games/apps/web/public/screenshots/click-race.jpg new file mode 100644 index 000000000..def635cd4 Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/click-race.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/color-memory.jpg b/games/mana-games/apps/web/public/screenshots/color-memory.jpg new file mode 100644 index 000000000..3ffbf02dd Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/color-memory.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/fish-catcher.jpg b/games/mana-games/apps/web/public/screenshots/fish-catcher.jpg new file mode 100644 index 000000000..c1c52220f Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/fish-catcher.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/gravity-painter.jpg b/games/mana-games/apps/web/public/screenshots/gravity-painter.jpg new file mode 100644 index 000000000..37c0091a1 Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/gravity-painter.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/neon-maze-runner.jpg b/games/mana-games/apps/web/public/screenshots/neon-maze-runner.jpg new file mode 100644 index 000000000..32ed8611a Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/neon-maze-runner.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/reaction-test.jpg b/games/mana-games/apps/web/public/screenshots/reaction-test.jpg new file mode 100644 index 000000000..d88d6864e Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/reaction-test.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/rhythm-defender.jpg b/games/mana-games/apps/web/public/screenshots/rhythm-defender.jpg new file mode 100644 index 000000000..303ec3c2a Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/rhythm-defender.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/snake.jpg b/games/mana-games/apps/web/public/screenshots/snake.jpg new file mode 100644 index 000000000..3cdd2b3f5 Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/snake.jpg differ diff --git a/games/mana-games/apps/web/public/screenshots/space-defenders.jpg b/games/mana-games/apps/web/public/screenshots/space-defenders.jpg new file mode 100644 index 000000000..0a2bd532a Binary files /dev/null and b/games/mana-games/apps/web/public/screenshots/space-defenders.jpg differ diff --git a/games/mana-games/apps/web/public/splash/splash-1125x2436.png b/games/mana-games/apps/web/public/splash/splash-1125x2436.png new file mode 100644 index 000000000..fbab093d6 Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-1125x2436.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-1242x2688.png b/games/mana-games/apps/web/public/splash/splash-1242x2688.png new file mode 100644 index 000000000..e37234898 Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-1242x2688.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-1536x2048.png b/games/mana-games/apps/web/public/splash/splash-1536x2048.png new file mode 100644 index 000000000..b7358ae1f Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-1536x2048.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-1668x2224.png b/games/mana-games/apps/web/public/splash/splash-1668x2224.png new file mode 100644 index 000000000..7de3ee1d7 Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-1668x2224.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-2048x2732.png b/games/mana-games/apps/web/public/splash/splash-2048x2732.png new file mode 100644 index 000000000..911db7a74 Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-2048x2732.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-640x1136.png b/games/mana-games/apps/web/public/splash/splash-640x1136.png new file mode 100644 index 000000000..90997853b Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-640x1136.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-750x1334.png b/games/mana-games/apps/web/public/splash/splash-750x1334.png new file mode 100644 index 000000000..68c9c51fa Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-750x1334.png differ diff --git a/games/mana-games/apps/web/public/splash/splash-828x1792.png b/games/mana-games/apps/web/public/splash/splash-828x1792.png new file mode 100644 index 000000000..4022d5a27 Binary files /dev/null and b/games/mana-games/apps/web/public/splash/splash-828x1792.png differ diff --git a/games/mana-games/apps/web/public/sw.js b/games/mana-games/apps/web/public/sw.js new file mode 100644 index 000000000..e57c129fc --- /dev/null +++ b/games/mana-games/apps/web/public/sw.js @@ -0,0 +1,170 @@ +const CACHE_NAME = 'mana-games-v1'; +const OFFLINE_URL = '/offline.html'; + +// Assets, die immer gecacht werden sollen +const STATIC_CACHE_URLS = [ + '/', + '/offline.html', + '/favicon.svg', + '/manifest.json' +]; + +// Cache-Strategien für verschiedene Ressourcen +const CACHE_STRATEGIES = { + // Netzwerk zuerst, dann Cache (für HTML) + networkFirst: [ + /\/$/, + /\.html$/, + /\.astro$/ + ], + // Cache zuerst, dann Netzwerk (für Assets) + cacheFirst: [ + /\.css$/, + /\.js$/, + /\.woff2?$/, + /\.ttf$/, + /\.otf$/, + /\.svg$/, + /\.png$/, + /\.jpg$/, + /\.jpeg$/, + /\.webp$/, + /\.ico$/ + ], + // Nur Netzwerk (für API-Calls) + networkOnly: [ + /\/api\//, + /\.json$/ + ] +}; + +// Service Worker Installation +self.addEventListener('install', event => { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => { + console.log('Service Worker: Caching static assets'); + return cache.addAll(STATIC_CACHE_URLS); + }) + .then(() => self.skipWaiting()) + ); +}); + +// Service Worker Aktivierung +self.addEventListener('activate', event => { + event.waitUntil( + caches.keys() + .then(cacheNames => { + return Promise.all( + cacheNames + .filter(cacheName => cacheName !== CACHE_NAME) + .map(cacheName => caches.delete(cacheName)) + ); + }) + .then(() => self.clients.claim()) + ); +}); + +// Fetch-Event Handler +self.addEventListener('fetch', event => { + const { request } = event; + const url = new URL(request.url); + + // Ignoriere Chrome Extension Requests + if (url.protocol === 'chrome-extension:') { + return; + } + + // Bestimme die Cache-Strategie + const strategy = getStrategy(url.pathname); + + if (strategy === 'networkFirst') { + event.respondWith(networkFirst(request)); + } else if (strategy === 'cacheFirst') { + event.respondWith(cacheFirst(request)); + } else if (strategy === 'networkOnly') { + event.respondWith(networkOnly(request)); + } else { + // Standard: Network First + event.respondWith(networkFirst(request)); + } +}); + +// Cache-Strategien Implementierung +async function networkFirst(request) { + try { + const networkResponse = await fetch(request); + if (networkResponse.ok) { + const cache = await caches.open(CACHE_NAME); + cache.put(request, networkResponse.clone()); + } + return networkResponse; + } catch (error) { + const cachedResponse = await caches.match(request); + if (cachedResponse) { + return cachedResponse; + } + + // Wenn es eine Navigation ist und wir offline sind, zeige die Offline-Seite + if (request.mode === 'navigate') { + const offlineResponse = await caches.match(OFFLINE_URL); + if (offlineResponse) { + return offlineResponse; + } + } + + throw error; + } +} + +async function cacheFirst(request) { + const cachedResponse = await caches.match(request); + if (cachedResponse) { + return cachedResponse; + } + + try { + const networkResponse = await fetch(request); + if (networkResponse.ok) { + const cache = await caches.open(CACHE_NAME); + cache.put(request, networkResponse.clone()); + } + return networkResponse; + } catch (error) { + console.error('Fetch failed:', error); + throw error; + } +} + +async function networkOnly(request) { + return fetch(request); +} + +// Hilfsfunktion zur Bestimmung der Cache-Strategie +function getStrategy(pathname) { + for (const [strategy, patterns] of Object.entries(CACHE_STRATEGIES)) { + if (patterns.some(pattern => pattern.test(pathname))) { + return strategy; + } + } + return 'networkFirst'; +} + +// Message Handler für Cache-Updates +self.addEventListener('message', event => { + if (event.data && event.data.type === 'SKIP_WAITING') { + self.skipWaiting(); + } + + if (event.data && event.data.type === 'CACHE_GAME') { + const gameUrl = event.data.url; + caches.open(CACHE_NAME) + .then(cache => cache.add(gameUrl)) + .then(() => { + event.ports[0].postMessage({ cached: true }); + }) + .catch(error => { + event.ports[0].postMessage({ cached: false, error: error.message }); + }); + } +}); \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/Button.astro b/games/mana-games/apps/web/src/components/Button.astro new file mode 100644 index 000000000..2d47cc6f6 --- /dev/null +++ b/games/mana-games/apps/web/src/components/Button.astro @@ -0,0 +1,191 @@ +--- +export interface Props { + variant?: 'primary' | 'secondary' | 'accent' | 'ghost' | 'danger'; + size?: 'small' | 'medium' | 'large' | 'icon'; + href?: string; + onclick?: string; + id?: string; + class?: string; + title?: string; + disabled?: boolean; + type?: 'button' | 'submit' | 'reset'; +} + +const { + variant = 'secondary', + size = 'medium', + href, + onclick, + id, + class: className = '', + title, + disabled = false, + type = 'button' +} = Astro.props; + +const isLink = Boolean(href); +const Component = isLink ? 'a' : 'button'; + +const classes = [ + 'btn', + `btn-${variant}`, + `btn-${size}`, + className +].filter(Boolean).join(' '); + +const props = { + class: classes, + ...(id && { id }), + ...(title && { title }), + ...(isLink ? { href } : { type, disabled }), + ...(onclick && { onclick }) +}; +--- + + + + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/Footer.astro b/games/mana-games/apps/web/src/components/Footer.astro new file mode 100644 index 000000000..071799aa7 --- /dev/null +++ b/games/mana-games/apps/web/src/components/Footer.astro @@ -0,0 +1,189 @@ +--- +// Footer component with compact site navigation +--- + + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/GameCard.astro b/games/mana-games/apps/web/src/components/GameCard.astro new file mode 100644 index 000000000..5e9de1db0 --- /dev/null +++ b/games/mana-games/apps/web/src/components/GameCard.astro @@ -0,0 +1,305 @@ +--- +import GameStats from './GameStats.astro'; +import Button from './Button.astro'; + +export interface Props { + title: string; + description: string; + slug: string; + thumbnail?: string; + tags?: string[]; + complexity?: 'Minimal' | 'Einfach' | 'Mittel' | 'Komplex'; + codeStats?: { + total: number; + code: number; + comments: number; + }; +} + +const { title, description, slug, thumbnail, tags = [], complexity, codeStats } = Astro.props; +--- + + + + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/GameStats.astro b/games/mana-games/apps/web/src/components/GameStats.astro new file mode 100644 index 000000000..e1f3b8d9b --- /dev/null +++ b/games/mana-games/apps/web/src/components/GameStats.astro @@ -0,0 +1,144 @@ +--- +import { statsService } from '../services/statsService'; + +export interface Props { + gameId: string; + showDetails?: boolean; +} + +const { gameId, showDetails = false } = Astro.props; +const stats = statsService.getStats(gameId); +--- + +{stats && ( +
+
+ {stats.highScore > 0 && ( +
+ 🏆 + {stats.highScore.toLocaleString('de-DE')} +
+ )} + + {stats.gamesPlayed > 0 && ( +
+ 🎮 + {stats.gamesPlayed}x +
+ )} + + {stats.totalPlayTime > 0 && ( +
+ ⏱️ + {statsService.formatPlayTime(stats.totalPlayTime)} +
+ )} +
+ + {showDetails && stats.lastPlayed && ( +
+ Zuletzt gespielt: {statsService.getRelativeTime(stats.lastPlayed)} +
+ )} + + {showDetails && stats.achievements && stats.achievements.length > 0 && ( +
+

Achievements

+
+ {stats.achievements.map(achievement => ( +
+ 🏅 + {achievement.name} +
+ ))} +
+
+ )} +
+)} + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/HorizontalScroller.astro b/games/mana-games/apps/web/src/components/HorizontalScroller.astro new file mode 100644 index 000000000..d61f7af12 --- /dev/null +++ b/games/mana-games/apps/web/src/components/HorizontalScroller.astro @@ -0,0 +1,314 @@ +--- +import GameCard from './GameCard.astro'; + +export interface Props { + title: string; + games: any[]; + id?: string; +} + +const { title, games, id = 'scroller' } = Astro.props; +--- + +
+
+

{title}

+
+ + +
+
+ +
+
+
+ +
+
+ {games.map((game) => ( +
+ +
+ ))} +
+
+
+
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/InstallPrompt.astro b/games/mana-games/apps/web/src/components/InstallPrompt.astro new file mode 100644 index 000000000..03757de4c --- /dev/null +++ b/games/mana-games/apps/web/src/components/InstallPrompt.astro @@ -0,0 +1,183 @@ +--- +// Keine Props benötigt +--- + + + + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/components/MyGamesSection.astro b/games/mana-games/apps/web/src/components/MyGamesSection.astro new file mode 100644 index 000000000..7c9e59563 --- /dev/null +++ b/games/mana-games/apps/web/src/components/MyGamesSection.astro @@ -0,0 +1,529 @@ +--- +export interface Props { + maxGames?: number; +} + +const { maxGames = 8 } = Astro.props; +--- + +
+
+

Meine generierten Spiele

+
+ + +
+
+ +
+
+
+

Lade deine Spiele...

+
+
+ + +
+ + + + \ No newline at end of file diff --git a/games/mana-games/apps/web/src/data/games.ts b/games/mana-games/apps/web/src/data/games.ts new file mode 100644 index 000000000..2aac276f3 --- /dev/null +++ b/games/mana-games/apps/web/src/data/games.ts @@ -0,0 +1,381 @@ +export interface Game { + id: string; + title: string; + description: string; + slug: string; + htmlFile: string; + thumbnail?: string; + tags: string[]; + difficulty: 'Einfach' | 'Mittel' | 'Schwer'; + complexity: 'Minimal' | 'Einfach' | 'Mittel' | 'Komplex'; + controls: string; + codeStats?: { + total: number; + code: number; + comments: number; + }; + // Community game fields + community?: boolean; + author?: string; + submittedAt?: string; +} + +export const games: Game[] = [ + { + id: '1', + title: 'Snake', + description: 'Der Klassiker! Steuere die Schlange und sammle Nahrung, aber vermeide die roten Felder!', + slug: 'snake', + htmlFile: '/games/snake_game.html', + thumbnail: '/screenshots/snake.jpg', + tags: ['Arcade', 'Klassiker', 'Retro'], + difficulty: 'Einfach', + complexity: 'Komplex', + controls: 'Pfeiltasten oder WASD zum Steuern', + codeStats: { + total: 604, + code: 338, + comments: 192 + } + }, + { + id: '2', + title: 'Space Defender', + description: 'Verteidige dein Raumschiff gegen Wellen von Aliens. Die Schwierigkeit steigt mit der Zeit!', + slug: 'space-defender', + htmlFile: '/games/space_defender_game.html', + thumbnail: '/screenshots/space-defenders.jpg', + tags: ['Shooter', 'Arcade', 'Action'], + difficulty: 'Mittel', + complexity: 'Mittel', + controls: 'A/D oder Pfeiltasten zum Bewegen, Leertaste zum Schießen', + codeStats: { + total: 436, + code: 348, + comments: 32 + } + }, + { + id: '3', + title: 'Gravity Painter', + description: 'Ein kreatives Physik-Puzzle! Setze Gravitationspunkte und lenke Partikel zu den Zielen.', + slug: 'gravity-painter', + htmlFile: '/games/gravity_painter.html', + thumbnail: '/screenshots/gravity-painter.jpg', + tags: ['Puzzle', 'Physik', 'Kreativ'], + difficulty: 'Schwer', + complexity: 'Mittel', + controls: 'Klicke für Gravitationspunkte, Leertaste für Partikel', + codeStats: { + total: 426, + code: 348, + comments: 21 + } + }, + { + id: '4', + title: 'Bounce & Catch Tutorial', + description: 'Ein einfaches Lernspiel, das die Grundlagen der Spieleentwicklung zeigt. Perfekt für Anfänger!', + slug: 'bounce-catch-tutorial', + htmlFile: '/games/bounce_catch_tutorial.html', + thumbnail: '/screenshots/bounce-catch.jpg', + tags: ['Tutorial', 'Lernspiel', 'Arcade'], + difficulty: 'Einfach', + complexity: 'Einfach', + controls: 'Mausbewegung zum Steuern des Paddles', + codeStats: { + total: 437, + code: 289, + comments: 87 + } + }, + { + id: '5', + title: 'Neon Maze Runner', + description: 'Navigiere durch prozedural generierte Labyrinthe! Sammle Diamanten, nutze Power-ups und finde den Ausgang.', + slug: 'neon-maze-runner', + htmlFile: '/games/neon_maze_runner.html', + thumbnail: '/screenshots/neon-maze-runner.jpg', + tags: ['Puzzle', 'Labyrinth', 'Arcade'], + difficulty: 'Mittel', + complexity: 'Komplex', + controls: 'WASD oder Pfeiltasten zum Bewegen', + codeStats: { + total: 832, + code: 644, + comments: 69 + } + }, + { + id: '6', + title: 'Rhythm Defender', + description: 'Verteidige dich im Takt der Musik! Drücke die richtigen Tasten im perfekten Timing für maximale Combos.', + slug: 'rhythm-defender', + htmlFile: '/games/rhythm_defender.html', + thumbnail: '/screenshots/rhythm-defender.jpg', + tags: ['Rhythmus', 'Musik', 'Arcade'], + difficulty: 'Mittel', + complexity: 'Komplex', + controls: 'A, S, D, F Tasten im Rhythmus drücken', + codeStats: { + total: 741, + code: 584, + comments: 56 + } + }, + { + id: '7', + title: 'Click Race', + description: 'Das schnellste Spiel! Klicke 30 mal so schnell du kannst. Wie schnell bist du?', + slug: 'click-race', + htmlFile: '/games/click_race.html', + thumbnail: '/screenshots/click-race.jpg', + tags: ['Geschwindigkeit', 'Minimal', 'Arcade'], + difficulty: 'Einfach', + complexity: 'Minimal', + controls: 'Klicke auf das rote Quadrat', + codeStats: { + total: 111, + code: 88, + comments: 23 + } + }, + { + id: '8', + title: 'Color Memory', + description: 'Merke dir die Farbreihenfolge! Ein klassisches Gedächtnisspiel das immer schwerer wird.', + slug: 'color-memory', + htmlFile: '/games/color_memory.html', + thumbnail: '/screenshots/color-memory.jpg', + tags: ['Gedächtnis', 'Minimal', 'Puzzle'], + difficulty: 'Einfach', + complexity: 'Minimal', + controls: 'Klicke die Farben in der richtigen Reihenfolge', + codeStats: { + total: 86, + code: 86, + comments: 0 + } + }, + { + id: '9', + title: 'Reaction Test', + description: 'Wie schnell sind deine Reflexe? Klicke so schnell wie möglich wenn der Bildschirm grün wird!', + slug: 'reaction-test', + htmlFile: '/games/reaction_test.html', + thumbnail: '/screenshots/reaction-test.jpg', + tags: ['Reaktion', 'Minimal', 'Test'], + difficulty: 'Einfach', + complexity: 'Minimal', + controls: 'Klicke wenn der Bildschirm grün wird', + codeStats: { + total: 78, + code: 78, + comments: 0 + } + }, + { + id: '10', + title: 'Asteroid Dash', + description: 'Fliege durch gefährliche Asteroidenfelder! Sammle Energie-Kristalle, nutze Power-ups und weiche den rotierenden Asteroiden aus.', + slug: 'asteroid-dash', + htmlFile: '/games/asteroid_dash.html', + thumbnail: '/screenshots/asteroid-dash.jpg', + tags: ['Action', 'Arcade', 'Weltraum'], + difficulty: 'Mittel', + complexity: 'Mittel', + controls: 'WASD oder Pfeiltasten zum Fliegen, Leertaste für Boost', + codeStats: { + total: 485, + code: 428, + comments: 57 + } + }, + { + id: '11', + title: 'Fish Catcher', + description: 'Fange Fische mit deinem Boot! Verschiedene Fischarten bringen unterschiedliche Punkte. Sammle Power-ups für größere Netze und Boni.', + slug: 'fish-catcher', + htmlFile: '/games/fish_catcher.html', + thumbnail: '/screenshots/fish-catcher.jpg', + tags: ['Arcade', 'Familie', 'Entspannend'], + difficulty: 'Einfach', + complexity: 'Einfach', + controls: 'A/D oder Pfeiltasten zum Bewegen, Maus für sanfte Steuerung', + codeStats: { + total: 362, + code: 321, + comments: 41 + } + }, + { + id: '12', + title: 'Balloon Pop', + description: 'Platze bunte Ballons bevor sie entkommen! Verschiedene Ballonarten, Power-ups und Combo-System für maximalen Spaß.', + slug: 'balloon-pop', + htmlFile: '/games/balloon_pop.html', + thumbnail: '/screenshots/balloon-pop.jpg', + tags: ['Geschicklichkeit', 'Familie', 'Bunt'], + difficulty: 'Einfach', + complexity: 'Einfach', + controls: 'Maus zum Klicken auf Ballons', + codeStats: { + total: 398, + code: 351, + comments: 47 + } + }, + { + id: '13', + title: 'Word Scramble', + description: 'Entschlüssele durcheinandergewürfelte Wörter! Mit 5 Kategorien, Combo-System und steigender Schwierigkeit.', + slug: 'word-scramble', + htmlFile: '/games/word_scramble.html', + thumbnail: '/screenshots/word-scramble.jpg', + tags: ['Puzzle', 'Wortspiel', 'Bildung'], + difficulty: 'Mittel', + complexity: 'Mittel', + controls: 'Tastatur zum Eingeben, Maus zum Klicken auf Buchstaben', + codeStats: { + total: 850, + code: 720, + comments: 130 + } + }, + { + id: '14', + title: 'Memory Card Match', + description: 'Das klassische Memory-Spiel! Finde alle Kartenpaare mit Emojis. Drei Schwierigkeitsstufen für jeden Spieler.', + slug: 'memory-card-match', + htmlFile: '/games/memory_card_match.html', + thumbnail: '/screenshots/memory-card-match.jpg', + tags: ['Gedächtnis', 'Kartenspiel', 'Familie'], + difficulty: 'Einfach', + complexity: 'Einfach', + controls: 'Maus zum Aufdecken der Karten', + codeStats: { + total: 415, + code: 350, + comments: 0 + } + }, + { + id: '15', + title: 'Turbo Racer', + description: 'Drift durch die Kurven und stelle Bestzeiten auf! Mit realistischer Drift-Physik und Nitro-Boost.', + slug: 'turbo-racer', + htmlFile: '/games/turbo_racer.html', + thumbnail: '/screenshots/turbo-racer.jpg', + tags: ['Rennen', 'Action', 'Arcade'], + difficulty: 'Mittel', + complexity: 'Mittel', + controls: 'WASD oder Pfeiltasten zum Fahren, Leertaste für Boost', + codeStats: { + total: 680, + code: 620, + comments: 60 + } + }, + { + id: '16', + title: 'Card Stack Rush', + description: 'Sortiere Karten blitzschnell auf die richtigen Stapel! Mit wechselnden Regeln, Combo-System und Zeitdruck.', + slug: 'card-stack-rush', + htmlFile: '/games/card_stack_rush.html', + thumbnail: '/screenshots/card-stack-rush.jpg', + tags: ['Kartenspiel', 'Geschwindigkeit', 'Arcade'], + difficulty: 'Mittel', + complexity: 'Einfach', + controls: 'Drag & Drop oder Klicken zum Platzieren', + codeStats: { + total: 520, + code: 480, + comments: 0 + } + }, + { + id: '17', + title: 'Flappy Mana', + description: 'Fliege durch Röhren und sammle Punkte! Ein Flappy Bird Klon mit Partikeleffekten und Highscore-System.', + slug: 'flappy-mana', + htmlFile: '/games/flappy_mana.html', + thumbnail: '/screenshots/flappy-mana.jpg', + tags: ['Arcade', 'Geschicklichkeit', 'Endless'], + difficulty: 'Mittel', + complexity: 'Einfach', + controls: 'Klick oder Leertaste zum Fliegen', + codeStats: { + total: 450, + code: 430, + comments: 20 + } + }, + { + id: '18', + title: 'Mana Runner', + description: 'Laufe und springe durch magische Welten! Sammle Mana-Kristalle, weiche Hindernissen aus und schalte den Doppelsprung frei.', + slug: 'mana-runner', + htmlFile: '/games/mana_runner.html', + thumbnail: '/screenshots/mana-runner.jpg', + tags: ['Jump n Run', 'Arcade', 'Endless'], + difficulty: 'Mittel', + complexity: 'Mittel', + controls: 'Leertaste zum Springen, Doppelsprung nach 10 Kristallen', + codeStats: { + total: 600, + code: 580, + comments: 20 + } + }, + { + id: '19', + title: 'Mana Defense', + description: 'Verteidige deinen Mana-Kristall! Baue Türme, plane deine Strategie und überlebe 20 Wellen von Gegnern.', + slug: 'mana-defense', + htmlFile: '/games/mana_defense.html', + thumbnail: '/screenshots/mana-defense.jpg', + tags: ['Tower Defense', 'Strategie', 'Aufbau'], + difficulty: 'Schwer', + complexity: 'Komplex', + controls: 'Maus zum Platzieren, 1-3 für Turmauswahl, S zum Verkaufen', + codeStats: { + total: 900, + code: 850, + comments: 50 + } + }, + { + id: '20', + title: 'Mana Factory', + description: 'Baue die größte Mana-Produktionsanlage! Ein Idle-Game mit Upgrades, Prestige-System und exponentiellem Wachstum.', + slug: 'mana-factory', + htmlFile: '/games/mana_factory.html', + thumbnail: '/screenshots/mana-factory.jpg', + tags: ['Idle', 'Incremental', 'Aufbau'], + difficulty: 'Einfach', + complexity: 'Mittel', + controls: 'Maus zum Klicken und Kaufen', + codeStats: { + total: 800, + code: 750, + comments: 50 + } + }, + { + id: '21', + title: 'Puzzle Blocks', + description: 'Klassisches Tetris-Gameplay! Stapele fallende Blöcke, vervollständige Reihen und erreiche den höchsten Score.', + slug: 'puzzle-blocks', + htmlFile: '/games/puzzle_blocks.html', + thumbnail: '/screenshots/puzzle-blocks.jpg', + tags: ['Puzzle', 'Klassiker', 'Arcade'], + difficulty: 'Mittel', + complexity: 'Einfach', + controls: '← → zum Bewegen, ↑ zum Drehen, ↓ schneller fallen, Space für Harddrop', + codeStats: { + total: 450, + code: 420, + comments: 30 + } + } +]; \ No newline at end of file diff --git a/games/mana-games/apps/web/src/layouts/Layout.astro b/games/mana-games/apps/web/src/layouts/Layout.astro new file mode 100644 index 000000000..efcc33870 --- /dev/null +++ b/games/mana-games/apps/web/src/layouts/Layout.astro @@ -0,0 +1,713 @@ +--- +import Button from '../components/Button.astro'; +import InstallPrompt from '../components/InstallPrompt.astro'; +import Footer from '../components/Footer.astro'; + +export interface Props { + title: string; + description?: string; + isGamePage?: boolean; + gameTitle?: string; + gameSlug?: string; + isPlayground?: boolean; + fullWidth?: boolean; + hideFooter?: boolean; +} + +const { title, description = "Mana Games - Eine Sammlung von Web-basierten Spielen", isGamePage = false, gameTitle, gameSlug, isPlayground = false, fullWidth = false, hideFooter = false } = Astro.props; +--- + + + + + + + + + + {title} | Mana Games + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ {!hideFooter &&