chore: extract arcade into standalone repo
Arcade lives as its own pnpm workspace at ~/Documents/Code/arcade now, with no @mana/* coupling. This drops every reference and the games/ directory from the monorepo. Removes: - games/ directory (89 files: web + server + 22 HTML games + screenshots) - @arcade/web, @arcade/server pnpm workspace entries (games/* globs) - arcade scripts in root package.json (4 scripts) - arcade.mana.how from mana-auth trusted origins + CORS_ORIGINS - arcade entries in mana-apps registry, app-icons, URL overrides - arcade.mana.how from cloudflared tunnel + prometheus blackbox probes - arcade-web service block in docker-compose.macmini.yml - generate-env.mjs entries for arcade server + web - BRANDING_ONLY 'arcade' entry in registry consistency spec - dead arcade translation keys in GuestWelcomeModal (DE+EN) - arcade mention in CLAUDE.md, authentication guideline, MODULE_REGISTRY Verified: - services/mana-auth/src/auth/sso-config.spec.ts: 8/8 pass - pnpm install regenerates lockfile cleanly (-536 lines) - no remaining 'arcade' refs outside historical snapshot docs Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
@ -112,8 +112,6 @@ app.get('/api/v1/data', (c) => {
|
|||
});
|
||||
```
|
||||
|
||||
> Note: Arcade's server (`@arcade/server`) does not require auth — game generation and community submission are public endpoints.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```env
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ apps/
|
|||
├── memoro/apps/ # Only remaining mobile app (Expo SDK 55)
|
||||
├── {product}/ # Per-product landing pages, packages
|
||||
│ # Standalone (own container, not unified): manavoxel
|
||||
games/ # arcade, voxelava, whopixels, worldream
|
||||
services/ # Backend services (Hono/Bun, Go, Python) — see list below
|
||||
packages/ # Shared workspace packages (@mana/*)
|
||||
docs/ # Long-form docs (deployment, hardware, postmortems, etc.)
|
||||
|
|
@ -165,7 +164,7 @@ Enforced by `pnpm run validate:turbo` (`scripts/validate-no-recursive-turbo.mjs`
|
|||
| `@mana/shared-theme` | Theme config |
|
||||
| `@mana/shared-i18n` | i18n |
|
||||
| `@mana/shared-privacy` | Unified visibility/privacy system: `VisibilityLevel` enum + zod schema + `<VisibilityPicker>` + predicates (`canEmbedOnWebsite`, …). Plan: [`docs/plans/visibility-system.md`](docs/plans/visibility-system.md). Rollout per-module, not yet adopted anywhere. |
|
||||
| `@mana/local-store` | Local-first store primitives — used by unified Mana, manavoxel, arcade, and shared-uload/-stores/-links |
|
||||
| `@mana/local-store` | Local-first store primitives — used by unified Mana, manavoxel, and shared-uload/-stores/-links |
|
||||
| `@mana/local-llm` | Browser-local LLM inference (transformers.js + Gemma 4 E2B, WebGPU). Powers `/llm-test` and the playground module. See [`packages/local-llm/CLAUDE.md`](packages/local-llm/CLAUDE.md) for the CSP requirements and the transformers.js v4 gotchas. |
|
||||
| `@mana/local-stt` | Browser-local speech-to-text (transformers.js + Whisper, WebGPU). Powers the QuickInputBar mic button. Same architecture as local-llm. See [`packages/local-stt/CLAUDE.md`](packages/local-stt/CLAUDE.md). |
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,6 @@ const BRANDING_ONLY = new Set([
|
|||
// Meta entry for the unified Mana app itself — it can't be a "module"
|
||||
// of its own workbench.
|
||||
'mana',
|
||||
// Standalone web app on its own subdomain (arcade.mana.how).
|
||||
'arcade',
|
||||
// Marketing placeholders, status: 'planning' / 'development'. No
|
||||
// workbench module exists yet — they only show up in the AppsPage
|
||||
// gallery as "Coming Soon" hints.
|
||||
|
|
|
|||
|
|
@ -156,8 +156,6 @@ ingress:
|
|||
# ============================================
|
||||
- hostname: playground.mana.how
|
||||
service: http://localhost:5050
|
||||
- hostname: arcade.mana.how
|
||||
service: http://localhost:5210
|
||||
- hostname: manavoxel.mana.how
|
||||
service: http://localhost:5028
|
||||
- hostname: whopxl.mana.how
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ services:
|
|||
# Enforced by services/mana-auth/src/auth/sso-config.spec.ts.
|
||||
# All productivity modules now live under mana.how (path-based) —
|
||||
# no per-module subdomain entries required here.
|
||||
CORS_ORIGINS: https://mana.how,https://auth.mana.how,https://arcade.mana.how,https://whopxl.mana.how
|
||||
CORS_ORIGINS: https://mana.how,https://auth.mana.how,https://whopxl.mana.how
|
||||
ports:
|
||||
- "3001:3001"
|
||||
healthcheck:
|
||||
|
|
@ -1028,33 +1028,7 @@ services:
|
|||
# uload-web, memoro-web
|
||||
|
||||
# picture-backend: REMOVED — replaced by Hono server (apps/picture/apps/server)
|
||||
|
||||
arcade-web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: games/arcade/apps/web/Dockerfile
|
||||
image: arcade-web:local
|
||||
container_name: mana-app-arcade-web
|
||||
restart: always
|
||||
mem_limit: 128m
|
||||
depends_on:
|
||||
mana-auth:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: 5210
|
||||
PUBLIC_MANA_AUTH_URL: http://mana-auth:3001
|
||||
PUBLIC_MANA_AUTH_URL_CLIENT: https://auth.mana.how
|
||||
PUBLIC_SYNC_SERVER_URL: http://mana-sync:3010
|
||||
PUBLIC_SYNC_SERVER_URL_CLIENT: https://sync.mana.how
|
||||
ports:
|
||||
- "5210:5210"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:5210/health"]
|
||||
interval: 180s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
# arcade-web: REMOVED — extracted to standalone repo at ~/Documents/Code/arcade
|
||||
|
||||
manavoxel-web:
|
||||
build:
|
||||
|
|
@ -1756,7 +1730,7 @@ services:
|
|||
# as the rest of the platform.
|
||||
DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/mana_platform
|
||||
# CORS — only the unified mana.how origin needs access today.
|
||||
# The arcade + manavoxel game frontends don't call apps/api.
|
||||
# The manavoxel game frontend doesn't call apps/api.
|
||||
CORS_ORIGINS: https://mana.how
|
||||
# Structured-logger format
|
||||
LOGGER_FORMAT: json
|
||||
|
|
|
|||
|
|
@ -277,7 +277,6 @@ scrape_configs:
|
|||
- https://mana.how/playground
|
||||
# Standalone games (separate containers)
|
||||
- https://whopxl.mana.how
|
||||
- https://arcade.mana.how
|
||||
relabel_configs:
|
||||
- source_labels: [__address__]
|
||||
target_label: __param_target
|
||||
|
|
|
|||
|
|
@ -135,12 +135,11 @@ Alle 76 Module der Mana-App (`apps/mana/apps/web/src/lib/modules/`).
|
|||
| `feedback` | Feedback | Bug-Reports und Feedback |
|
||||
| `playground` | Playground | Dev/Test-Playground |
|
||||
|
||||
## Sonstige (5)
|
||||
## Sonstige (4)
|
||||
|
||||
| Modul | Name | Beschreibung |
|
||||
|---|---|---|
|
||||
| `spiral` | Spiral | Spiral-Timeline-Visualisierung |
|
||||
| `arcade` | Arcade | AI-generierte Browser-Games |
|
||||
| `uload` | uLoad | URL-Shortener mit Click-Tracking |
|
||||
| `complexity` | Complexity | System-Komplexitäts-Metriken |
|
||||
| `core` | Core | Core-Infrastruktur und shared Exports |
|
||||
|
|
|
|||
|
|
@ -1,159 +0,0 @@
|
|||
# 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)
|
||||
│ └── server/ # Hono/Bun Compute Server (@arcade/server)
|
||||
│ └── src/
|
||||
│ ├── routes/
|
||||
│ │ └── games.ts # AI-Spielgenerierung + Community-Einreichungen
|
||||
│ └── index.ts
|
||||
└── package.json # Root (arcade)
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Aspekt | Technologie |
|
||||
|--------|-------------|
|
||||
| Frontend | SvelteKit 2 + Svelte 5 (Runes) |
|
||||
| Styling | Tailwind CSS 4 + @mana/shared-tailwind |
|
||||
| Auth | @mana/shared-auth (SSO) |
|
||||
| PWA | @vite-pwa/sveltekit + @mana/shared-pwa |
|
||||
| State | @mana/local-store (Dexie.js + sync) |
|
||||
| i18n | svelte-i18n (DE + EN) |
|
||||
| UI | @mana/shared-ui (PillNav, AuthGate, etc.) |
|
||||
| Theming | @mana/shared-theme (multi-theme) |
|
||||
| Server | Hono + Bun (AI-Generierung, Community) |
|
||||
|
||||
## Entwicklung
|
||||
|
||||
```bash
|
||||
# Alles starten (Web + Backend)
|
||||
pnpm arcade:dev
|
||||
|
||||
# Nur Web (SvelteKit)
|
||||
pnpm dev:arcade:web
|
||||
|
||||
# Nur Server (Hono/Bun)
|
||||
pnpm dev:arcade:server
|
||||
|
||||
# Web + Backend zusammen
|
||||
pnpm dev:arcade:app
|
||||
```
|
||||
|
||||
**Ports:**
|
||||
- Web: http://localhost:5210
|
||||
- Server: 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
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"name": "@arcade/server",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun run --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --outdir dist --target bun",
|
||||
"start": "bun run src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@google/genai": "^1.14.0",
|
||||
"@mana/shared-hono": "workspace:*",
|
||||
"hono": "^4.7.5",
|
||||
"openai": "^4.76.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { errorHandler, notFoundHandler } from '@mana/shared-hono';
|
||||
import { gamesRoutes } from './routes/games';
|
||||
|
||||
const PORT = parseInt(process.env.PORT || '3011', 10);
|
||||
const CORS_ORIGINS = (process.env.CORS_ORIGINS || 'http://localhost:5173').split(',');
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
app.onError(errorHandler);
|
||||
app.notFound(notFoundHandler);
|
||||
|
||||
app.use('*', cors({ origin: CORS_ORIGINS, credentials: false }));
|
||||
|
||||
app.get('/health', (c) =>
|
||||
c.json({ status: 'ok', timestamp: new Date().toISOString(), service: 'arcade-server' })
|
||||
);
|
||||
|
||||
app.route('/api/games', gamesRoutes);
|
||||
|
||||
console.log(`Arcade server running on http://localhost:${PORT}`);
|
||||
|
||||
export default { port: PORT, fetch: app.fetch };
|
||||
|
|
@ -1,432 +0,0 @@
|
|||
import { Hono } from 'hono';
|
||||
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;
|
||||
}
|
||||
|
||||
const MODEL_CONFIGS: Record<string, ModelConfig> = {
|
||||
'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',
|
||||
},
|
||||
'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',
|
||||
},
|
||||
'gpt-4o': { provider: 'azure', modelId: 'gpt-4o', displayName: 'GPT-4o' },
|
||||
'gpt-4o-mini': { provider: 'azure', modelId: 'gpt-4o-mini', displayName: 'GPT-4o Mini' },
|
||||
};
|
||||
|
||||
function initClients() {
|
||||
const googleKey = process.env.GOOGLE_GENAI_API_KEY;
|
||||
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
||||
const azureEndpoint = process.env.AZURE_OPENAI_ENDPOINT;
|
||||
const azureKey = process.env.AZURE_OPENAI_API_KEY;
|
||||
|
||||
return {
|
||||
google:
|
||||
googleKey && !googleKey.includes('your_') ? new GoogleGenAI({ apiKey: googleKey }) : null,
|
||||
anthropic:
|
||||
anthropicKey && !anthropicKey.includes('your_')
|
||||
? new Anthropic({ apiKey: anthropicKey })
|
||||
: null,
|
||||
azure:
|
||||
azureEndpoint && azureKey && !azureKey.includes('your_')
|
||||
? new AzureOpenAI({
|
||||
endpoint: azureEndpoint,
|
||||
apiKey: azureKey,
|
||||
apiVersion: '2024-08-01-preview',
|
||||
})
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
const clients = initClients();
|
||||
|
||||
function 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.`;
|
||||
}
|
||||
|
||||
function validateAndSanitizeGame(html: string): string {
|
||||
if (!html || typeof html !== 'string') {
|
||||
throw new Error('Invalid HTML content');
|
||||
}
|
||||
if (!html.includes('<!DOCTYPE html>')) {
|
||||
throw new Error('Invalid game HTML structure');
|
||||
}
|
||||
return 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(');
|
||||
}
|
||||
|
||||
async function generateWithProvider(config: ModelConfig, prompt: string): Promise<string> {
|
||||
if (config.provider === 'google') {
|
||||
if (!clients.google) throw new Error('Google Gemini not configured');
|
||||
const response = await clients.google.models.generateContent({
|
||||
model: config.modelId,
|
||||
contents: prompt,
|
||||
config: { temperature: 0.7, maxOutputTokens: 8192 },
|
||||
});
|
||||
const content = response.text;
|
||||
if (!content) throw new Error('No content from Google Gemini');
|
||||
return content;
|
||||
}
|
||||
|
||||
if (config.provider === 'anthropic') {
|
||||
if (!clients.anthropic) throw new Error('Anthropic not configured');
|
||||
const response = await clients.anthropic.messages.create({
|
||||
model: config.modelId,
|
||||
max_tokens: 8192,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
});
|
||||
const content = response.content[0];
|
||||
if (!content || content.type !== 'text') throw new Error('No content from Anthropic');
|
||||
return content.text;
|
||||
}
|
||||
|
||||
if (config.provider === 'azure') {
|
||||
if (!clients.azure) throw new Error('Azure OpenAI not configured');
|
||||
const deployment = process.env.AZURE_OPENAI_DEPLOYMENT || config.modelId;
|
||||
const response = await clients.azure.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 Error('No content from Azure OpenAI');
|
||||
return content;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown provider: ${config.provider}`);
|
||||
}
|
||||
|
||||
export const gamesRoutes = new Hono();
|
||||
|
||||
gamesRoutes.post('/generate', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body) return c.json({ error: 'Invalid JSON body' }, 400);
|
||||
|
||||
const {
|
||||
description,
|
||||
mode = 'create',
|
||||
originalPrompt,
|
||||
currentCode,
|
||||
model = 'gemini-2.0-flash',
|
||||
} = body;
|
||||
|
||||
if (!description || typeof description !== 'string' || description.trim().length < 10) {
|
||||
return c.json({ error: 'Bitte gib eine Spielbeschreibung mit mindestens 10 Zeichen ein' }, 400);
|
||||
}
|
||||
|
||||
const config = MODEL_CONFIGS[model] ?? MODEL_CONFIGS['gemini-2.0-flash'];
|
||||
|
||||
const isAvailable =
|
||||
(config.provider === 'google' && clients.google !== null) ||
|
||||
(config.provider === 'anthropic' && clients.anthropic !== null) ||
|
||||
(config.provider === 'azure' && clients.azure !== null);
|
||||
|
||||
if (!isAvailable) {
|
||||
return c.json({ error: `AI provider ${config.provider} is not configured` }, 500);
|
||||
}
|
||||
|
||||
const prompt = createGamePrompt(description.trim(), mode, originalPrompt, currentCode);
|
||||
|
||||
try {
|
||||
let raw = await generateWithProvider(config, prompt);
|
||||
|
||||
const htmlMatch = raw.match(/```html\n([\s\S]*?)\n```/);
|
||||
if (htmlMatch) raw = htmlMatch[1];
|
||||
|
||||
const html = validateAndSanitizeGame(raw);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
html,
|
||||
metadata: { description: description.trim(), generatedAt: new Date().toISOString() },
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return c.json({ error: `Failed to generate game: ${message}` }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
gamesRoutes.post('/submit', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body) return c.json({ error: 'Invalid JSON body' }, 400);
|
||||
|
||||
const { title, description, controls, difficulty, complexity, tags, author, files, submittedAt } =
|
||||
body;
|
||||
|
||||
if (!title || !description || !files?.html?.content || !files?.screenshot?.content) {
|
||||
return c.json({ error: 'Missing required fields' }, 400);
|
||||
}
|
||||
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
const githubOwner = process.env.GITHUB_OWNER || 'tillschneider';
|
||||
const githubRepo = process.env.GITHUB_REPO || 'mana-games';
|
||||
|
||||
if (!githubToken || githubToken.includes('your_')) {
|
||||
return c.json({ error: 'Server configuration error - GitHub token missing' }, 500);
|
||||
}
|
||||
|
||||
const gameSlug = 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 {
|
||||
const repoResponse = await fetch(`https://api.github.com/repos/${githubOwner}/${githubRepo}`, {
|
||||
headers,
|
||||
});
|
||||
if (!repoResponse.ok) {
|
||||
return c.json({ error: `Failed to fetch repository info: ${repoResponse.status}` }, 500);
|
||||
}
|
||||
const repoData = (await repoResponse.json()) as { default_branch: string };
|
||||
const defaultBranch = repoData.default_branch;
|
||||
|
||||
const refResponse = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/git/refs/heads/${defaultBranch}`,
|
||||
{ headers }
|
||||
);
|
||||
if (!refResponse.ok) return c.json({ error: 'Failed to fetch branch info' }, 500);
|
||||
const refData = (await refResponse.json()) as { object: { sha: string } };
|
||||
const baseSha = refData.object.sha;
|
||||
|
||||
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) return c.json({ error: 'Failed to create branch' }, 500);
|
||||
|
||||
const gameData = {
|
||||
id: String(timestamp),
|
||||
title,
|
||||
description,
|
||||
slug: gameSlug,
|
||||
htmlFile: `/games/${gameSlug}.html`,
|
||||
thumbnail: `/screenshots/${gameSlug}.jpg`,
|
||||
tags,
|
||||
difficulty,
|
||||
complexity,
|
||||
controls,
|
||||
community: true,
|
||||
author: author.name,
|
||||
submittedAt,
|
||||
};
|
||||
|
||||
const communityGamesPath = 'src/data/community-games.json';
|
||||
let communityGames: unknown[] = [];
|
||||
|
||||
const existingFileResponse = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${communityGamesPath}?ref=${defaultBranch}`,
|
||||
{ headers }
|
||||
);
|
||||
if (existingFileResponse.ok) {
|
||||
const existingFile = (await existingFileResponse.json()) as { content: string };
|
||||
const content = Buffer.from(existingFile.content, 'base64').toString('utf-8');
|
||||
communityGames = JSON.parse(content);
|
||||
}
|
||||
communityGames.push(gameData);
|
||||
|
||||
const filesToCreate = [
|
||||
{
|
||||
path: `public/games/${gameSlug}.html`,
|
||||
content: Buffer.from(files.html.content).toString('base64'),
|
||||
},
|
||||
{
|
||||
path: `public/screenshots/${gameSlug}.jpg`,
|
||||
content: files.screenshot.content.split(',')[1],
|
||||
},
|
||||
{
|
||||
path: communityGamesPath,
|
||||
content: Buffer.from(JSON.stringify(communityGames, null, 2)).toString('base64'),
|
||||
},
|
||||
];
|
||||
|
||||
for (const file of filesToCreate) {
|
||||
const res = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/contents/${file.path}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
message: `Add community game: ${title}`,
|
||||
content: file.content,
|
||||
branch: branchName,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!res.ok) return c.json({ error: `Failed to create file ${file.path}` }, 500);
|
||||
}
|
||||
|
||||
const prBody = `## Neues Community-Spiel: ${title}
|
||||
|
||||
### Spiel-Details
|
||||
- **Autor:** ${author.name}${author.github ? ` (@${author.github})` : ''}
|
||||
- **Beschreibung:** ${description}
|
||||
- **Schwierigkeit:** ${difficulty}
|
||||
- **Komplexität:** ${complexity}
|
||||
- **Steuerung:** ${controls}
|
||||
- **Tags:** ${(tags as string[]).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
|
||||
|
||||
---
|
||||
*Eingereicht am: ${new Date(submittedAt).toLocaleString('de-DE')}*
|
||||
${author.email ? `*Kontakt: ${author.email}*` : ''}`;
|
||||
|
||||
const prResponse = await fetch(
|
||||
`https://api.github.com/repos/${githubOwner}/${githubRepo}/pulls`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
title: `Community: ${title}`,
|
||||
body: prBody,
|
||||
head: branchName,
|
||||
base: defaultBranch,
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (!prResponse.ok) return c.json({ error: 'Failed to create pull request' }, 500);
|
||||
|
||||
const prData = (await prResponse.json()) as { html_url: string; number: number };
|
||||
return c.json({
|
||||
success: true,
|
||||
message: 'Game submitted successfully',
|
||||
prUrl: prData.html_url,
|
||||
prNumber: prData.number,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return c.json({ error: `Failed to submit game: ${message}` }, 500);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowImportingTsExtensions": true,
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
FROM sveltekit-base:local AS builder
|
||||
|
||||
ARG PUBLIC_MANA_AUTH_URL=http://mana-auth:3001
|
||||
ENV PUBLIC_MANA_AUTH_URL=$PUBLIC_MANA_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"]
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
{
|
||||
"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": {
|
||||
"@mana/shared-pwa": "workspace:*",
|
||||
"@mana/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": {
|
||||
"@mana/local-store": "workspace:*",
|
||||
"@mana/shared-auth": "workspace:*",
|
||||
"@mana/shared-auth-ui": "workspace:*",
|
||||
"@mana/shared-branding": "workspace:*",
|
||||
"@mana/shared-error-tracking": "workspace:*",
|
||||
"@mana/feedback": "workspace:*",
|
||||
"@mana/shared-i18n": "workspace:*",
|
||||
"@mana/help": "workspace:*",
|
||||
"@mana/shared-icons": "workspace:*",
|
||||
"@mana/shared-stores": "workspace:*",
|
||||
"@mana/shared-tags": "workspace:*",
|
||||
"@mana/shared-tailwind": "workspace:*",
|
||||
"@mana/shared-theme": "workspace:*",
|
||||
"@mana/shared-theme-ui": "workspace:*",
|
||||
"@mana/shared-ui": "workspace:*",
|
||||
"@mana/shared-utils": "workspace:*",
|
||||
"svelte-i18n": "^4.0.1"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
@import "tailwindcss";
|
||||
@import "@mana/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";
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { initErrorTracking, handleSvelteError } from '@mana/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);
|
||||
};
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
import { setSecurityHeaders } from '@mana/shared-utils/security-headers';
|
||||
|
||||
const PUBLIC_MANA_AUTH_URL_CLIENT =
|
||||
process.env.PUBLIC_MANA_AUTH_URL_CLIENT || process.env.PUBLIC_MANA_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_AUTH_URL__ = ${JSON.stringify(PUBLIC_MANA_AUTH_URL_CLIENT)};
|
||||
window.__PUBLIC_BACKEND_URL__ = ${JSON.stringify(PUBLIC_BACKEND_URL_CLIENT)};
|
||||
window.__PUBLIC_GLITCHTIP_DSN__ = ${JSON.stringify(PUBLIC_GLITCHTIP_DSN)};
|
||||
</script>`;
|
||||
return html.replace('<head>', `<head>${envScript}`);
|
||||
},
|
||||
});
|
||||
|
||||
setSecurityHeaders(response, {
|
||||
connectSrc: [PUBLIC_MANA_AUTH_URL_CLIENT, PUBLIC_BACKEND_URL_CLIENT],
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { SkeletonBox } from '@mana/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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default as AppLoadingSkeleton } from './AppLoadingSkeleton.svelte';
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
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();
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
// No guest seed data needed — games are static HTML files, stats build up from play
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
import { createLocalStore, type BaseRecord } from '@mana/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');
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { useLiveQueryWithDefault } from '@mana/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[]);
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
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 };
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { createFeedbackService } from '@mana/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(),
|
||||
});
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { createAppOnboardingStore, type AppOnboardingStep } from '@mana/shared-ui';
|
||||
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 () => {},
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { createManaAuthStore } from '@mana/shared-auth-ui';
|
||||
|
||||
export const authStore = createManaAuthStore({
|
||||
devBackendPort: 3011,
|
||||
});
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
import { createSimpleNavigationStores } from '@mana/shared-stores';
|
||||
|
||||
export const { isNavCollapsed } = createSimpleNavigationStores({
|
||||
storageKey: 'arcade',
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import { createThemeStore } from '@mana/shared-theme';
|
||||
|
||||
export const theme = createThemeStore({
|
||||
appId: 'arcade',
|
||||
defaultVariant: 'lume',
|
||||
});
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { createUserSettingsStore } from '@mana/shared-theme';
|
||||
import { authStore } from './auth.svelte';
|
||||
|
||||
function getAuthUrl(): string {
|
||||
if (browser && typeof window !== 'undefined') {
|
||||
const injectedUrl = (window as unknown as { __PUBLIC_MANA_AUTH_URL__?: string })
|
||||
.__PUBLIC_MANA_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(),
|
||||
});
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { locale } from 'svelte-i18n';
|
||||
import { PillNavigation, SyncIndicator } from '@mana/shared-ui';
|
||||
import type {
|
||||
PillNavItem,
|
||||
PillDropdownItem,
|
||||
SpotlightAction,
|
||||
ContentSearcher,
|
||||
} from '@mana/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 } from '$lib/data/games';
|
||||
import {
|
||||
THEME_DEFINITIONS,
|
||||
DEFAULT_THEME_VARIANTS,
|
||||
EXTENDED_THEME_VARIANTS,
|
||||
} from '@mana/shared-theme';
|
||||
import type { ThemeVariant } from '@mana/shared-theme';
|
||||
import { filterHiddenNavItems } from '@mana/shared-theme';
|
||||
import { isNavCollapsed as collapsedStore } from '$lib/stores/navigation';
|
||||
import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@mana/shared-i18n';
|
||||
import { getPillAppItems } from '@mana/shared-branding';
|
||||
import { setLocale, supportedLocales } from '$lib/i18n';
|
||||
import { SessionExpiredBanner, AuthGate, GuestWelcomeModal } from '@mana/shared-auth-ui';
|
||||
import { shouldShowGuestWelcome } from '@mana/shared-auth-ui';
|
||||
import { gamesOnboarding } from '$lib/stores/app-onboarding.svelte';
|
||||
import { MiniOnboardingModal } from '@mana/shared-ui';
|
||||
import { gamesStore } from '$lib/data/local-store';
|
||||
import { tagLocalStore, tagMutations, useAllTags as useAllSharedTags } from '@mana/shared-stores';
|
||||
|
||||
const allTags = useAllSharedTags();
|
||||
|
||||
let showGuestWelcome = $state(false);
|
||||
|
||||
function initGuestWelcome() {
|
||||
if (!authStore.isAuthenticated && shouldShowGuestWelcome('arcade')) {
|
||||
showGuestWelcome = true;
|
||||
}
|
||||
}
|
||||
|
||||
let appItems = $derived(getPillAppItems('arcade', undefined, undefined, authStore.user?.tier));
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
// Cmd+K spotlight — nav shortcuts + game search. The PillNavigation
|
||||
// hosts GlobalSpotlight internally, wired via spotlightActions +
|
||||
// contentSearcher below.
|
||||
const spotlightActions: SpotlightAction[] = [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'Alle Spiele',
|
||||
icon: 'gamepad-2',
|
||||
shortcut: '1',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/'),
|
||||
},
|
||||
{
|
||||
id: 'new-game',
|
||||
label: 'Spiel erstellen',
|
||||
icon: 'sparkles',
|
||||
shortcut: '2',
|
||||
category: 'Erstellen',
|
||||
onExecute: () => goto('/create'),
|
||||
},
|
||||
{
|
||||
id: 'community',
|
||||
label: 'Community',
|
||||
icon: 'users',
|
||||
shortcut: '3',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/community'),
|
||||
},
|
||||
{
|
||||
id: 'stats',
|
||||
label: 'Statistiken',
|
||||
icon: 'bar-chart-3',
|
||||
shortcut: '4',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/stats'),
|
||||
},
|
||||
{
|
||||
id: 'settings',
|
||||
label: 'Einstellungen',
|
||||
icon: 'settings',
|
||||
category: 'Navigation',
|
||||
onExecute: () => goto('/settings'),
|
||||
},
|
||||
];
|
||||
|
||||
const contentSearcher: ContentSearcher = async (query) => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
|
||||
const hits = games
|
||||
.filter(
|
||||
(g) => g.title.toLowerCase().includes(q) || g.tags.some((t) => t.toLowerCase().includes(q))
|
||||
)
|
||||
.slice(0, 10);
|
||||
|
||||
if (hits.length === 0) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
appId: 'arcade',
|
||||
appName: 'Spiele',
|
||||
results: hits.map((g, i) => ({
|
||||
id: `game-${g.slug}`,
|
||||
type: 'game',
|
||||
appId: 'arcade',
|
||||
title: g.title,
|
||||
subtitle: g.tags.join(', '),
|
||||
href: `/play/${g.slug}`,
|
||||
score: hits.length - i,
|
||||
})),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
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 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>
|
||||
|
||||
<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"
|
||||
{spotlightActions}
|
||||
{contentSearcher}
|
||||
spotlightPlaceholder="Spiele oder Aktionen suchen..."
|
||||
/>
|
||||
|
||||
<main class="main-content bg-background">
|
||||
<div class="content-wrapper">
|
||||
{@render children()}
|
||||
</div>
|
||||
</main>
|
||||
</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>
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
<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} · 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>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { FeedbackPage } from '@mana/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} />
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
<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} · {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>
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
<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">
|
||||
← {$_('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}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { ProfilePage } from '@mana/shared-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}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme.svelte';
|
||||
import { THEME_DEFINITIONS, EXTENDED_THEME_VARIANTS } from '@mana/shared-theme';
|
||||
import type { ThemeVariant } from '@mana/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>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ForgotPasswordPage } from '@mana/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" />
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { LoginPage } from '@mana/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"
|
||||
/>
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { RegisterPage } from '@mana/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" />
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
<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 '@mana/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}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
// Disable SSR — all data is local-first (IndexedDB + mana-sync)
|
||||
export const ssr = false;
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
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' },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
<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>
|
||||
|
Before Width: | Height: | Size: 385 B |
|
|
@ -1,666 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,677 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,491 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,710 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,180 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,697 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,489 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,483 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,966 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,569 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,886 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,636 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,795 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,662 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,508 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,791 +0,0 @@
|
|||
<!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>
|
||||
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
|
@ -1,21 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { SvelteKitPWA } from '@vite-pwa/sveltekit';
|
||||
import { createPWAConfig } from '@mana/shared-pwa';
|
||||
import { MANA_SHARED_PACKAGES, getBuildDefines } from '@mana/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: [...MANA_SHARED_PACKAGES],
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [...MANA_SHARED_PACKAGES],
|
||||
},
|
||||
define: {
|
||||
...getBuildDefines(),
|
||||
},
|
||||
});
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "arcade",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "AI-powered browser games platform"
|
||||
}
|
||||
|
|
@ -160,10 +160,6 @@
|
|||
"dev:finance:landing": "pnpm --filter @finance/landing dev",
|
||||
"voxel-lava:dev": "turbo run dev --filter=@voxel-lava/web",
|
||||
"dev:voxel-lava:web": "pnpm --filter @voxel-lava/web dev",
|
||||
"arcade:dev": "turbo run dev --filter=arcade...",
|
||||
"dev:arcade:web": "pnpm --filter @arcade/web dev",
|
||||
"dev:arcade:server": "pnpm --filter @arcade/server dev",
|
||||
"dev:arcade:app": "turbo run dev --filter=@arcade/web --filter=@arcade/server",
|
||||
"figgos:dev": "turbo run dev --filter=figgos...",
|
||||
"dev:figgos:mobile": "pnpm --filter @figgos/mobile dev",
|
||||
"dev:figgos:web": "pnpm --filter @figgos/web dev",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@
|
|||
inventory: ['Alles im Überblick behalten', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
uload: ['Links kürzen & verwalten', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
news: ['Nachrichten, kuratiert für dich', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
arcade: ['Spiele direkt im Browser', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
skilltree: ['Dein Fortschritt, sichtbar', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
food: ['Ernährung bewusst leben', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
wisekeep: ['Wissen bewahren & teilen', 'Quelloffen & unabhängig', 'Privat by Design'],
|
||||
|
|
@ -97,7 +96,6 @@
|
|||
inventory: ['Keep track of everything', 'Open-source & independent', 'Private by design'],
|
||||
uload: ['Shorten & manage links', 'Open-source & independent', 'Private by design'],
|
||||
news: ['News, curated for you', 'Open-source & independent', 'Private by design'],
|
||||
arcade: ['Games right in your browser', 'Open-source & independent', 'Private by design'],
|
||||
skilltree: ['Your progress, visualized', 'Open-source & independent', 'Private by design'],
|
||||
food: ['Mindful nutrition tracking', 'Open-source & independent', 'Private by design'],
|
||||
wisekeep: ['Preserve & share knowledge', 'Open-source & independent', 'Private by design'],
|
||||
|
|
|
|||
|
|
@ -176,9 +176,6 @@ export const APP_ICONS = {
|
|||
places: svgToDataUrl(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="plc" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#0ea5e9"/><stop offset="100%" style="stop-color:#0284c7"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#plc)"/><path d="M50 20c-14 0-25 11-25 25 0 20 25 38 25 38s25-18 25-38c0-14-11-25-25-25zm0 34a9 9 0 1 1 0-18 9 9 0 0 1 0 18z" fill="white"/></svg>`
|
||||
),
|
||||
arcade: svgToDataUrl(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="ar" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#ef4444"/><stop offset="100%" style="stop-color:#dc2626"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#ar)"/><rect x="25" y="30" width="50" height="35" rx="5" stroke="white" stroke-width="4" fill="none"/><path d="M38 65v10M62 65v10M32 75h36" stroke="white" stroke-width="4" stroke-linecap="round"/><circle cx="60" cy="44" r="4" fill="white"/><circle cx="68" cy="50" r="3" fill="white" fill-opacity="0.7"/><path d="M35 44h10M40 39v10" stroke="white" stroke-width="3" stroke-linecap="round"/></svg>`
|
||||
),
|
||||
events: svgToDataUrl(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="ev" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#f43f5e"/><stop offset="100%" style="stop-color:#be123c"/></linearGradient></defs><rect width="100" height="100" rx="22" fill="url(#ev)"/><path d="M22 78l14-44 30 30-44 14z" fill="white"/><path d="M36 34c4-6 12-8 18-4M50 22c4-2 10 0 12 6M62 28c6-2 12 2 12 10" stroke="white" stroke-width="3" stroke-linecap="round" fill="none"/><circle cx="74" cy="46" r="2.5" fill="white"/><circle cx="80" cy="58" r="2" fill="white" fill-opacity="0.8"/><circle cx="68" cy="62" r="2" fill="white" fill-opacity="0.7"/></svg>`
|
||||
),
|
||||
|
|
|
|||
|
|
@ -835,23 +835,6 @@ export const MANA_APPS: ManaApp[] = [
|
|||
status: 'development',
|
||||
requiredTier: 'guest',
|
||||
},
|
||||
{
|
||||
id: 'arcade',
|
||||
name: 'Arcade',
|
||||
description: {
|
||||
de: 'KI Browser-Spiele',
|
||||
en: 'AI Browser Games',
|
||||
},
|
||||
longDescription: {
|
||||
de: 'Sammlung von KI-generierten Browser-Spielen zum sofortigen Spielen.',
|
||||
en: 'Collection of AI-generated browser games to play instantly.',
|
||||
},
|
||||
icon: APP_ICONS.arcade,
|
||||
color: '#ef4444',
|
||||
comingSoon: false,
|
||||
status: 'beta',
|
||||
requiredTier: 'guest',
|
||||
},
|
||||
{
|
||||
id: 'drink',
|
||||
name: 'Drink',
|
||||
|
|
@ -1312,8 +1295,6 @@ export const APP_SLIDER_LABELS = {
|
|||
const APP_URL_OVERRIDES: Partial<Record<AppIconId, { dev: string; prod: string }>> = {
|
||||
// The unified app itself lives at the root, not at /mana.
|
||||
mana: { dev: 'http://localhost:5173', prod: 'https://mana.how' },
|
||||
// Standalone apps on their own subdomain / port.
|
||||
arcade: { dev: 'http://localhost:5201', prod: 'https://arcade.mana.how' },
|
||||
};
|
||||
|
||||
export const APP_URLS: Record<AppIconId, { dev: string; prod: string }> = Object.fromEntries(
|
||||
|
|
|
|||