feat: add new projects bauntown, presi, voxel-lava, whopixels

- apps/bauntown: Developer community website (Astro landing)
- apps/presi: Presentation project
- games/voxel-lava: Voxel lava game (SvelteKit)
- games/whopixels: Whopixels game

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-27 15:11:53 +01:00
parent d35ba768cf
commit 5b1e12e5d6
407 changed files with 46356 additions and 0 deletions

23
games/voxel-lava/.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
node_modules
# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
# OS
.DS_Store
Thumbs.db
# Env
.env
.env.*
!.env.example
!.env.test
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

View file

@ -0,0 +1,13 @@
{
"mcpServers": {
"pocketbase-voxel": {
"command": "node",
"args": ["/Users/tillschneider/Documents/__00__Code/uload/mcp-servers/pocketbase-mcp/build/index.js"],
"env": {
"POCKETBASE_URL": "https://pb.voxelava.com",
"POCKETBASE_ADMIN_EMAIL": "till.schneider@memoro.ai",
"POCKETBASE_ADMIN_PASSWORD": "p0ck3t-RA1N"
}
}
}
}

1
games/voxel-lava/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -0,0 +1,6 @@
# Package Managers
package-lock.json
pnpm-lock.yaml
yarn.lock
bun.lock
bun.lockb

View file

@ -0,0 +1,15 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"overrides": [
{
"files": "*.svelte",
"options": {
"parser": "svelte"
}
}
]
}

View file

@ -0,0 +1,292 @@
# Voxel-Lava Datenbank-Dokumentation
Diese Dokumentation beschreibt den Aufbau und die Funktionsweise der Supabase-Datenbank für das Voxel-Lava-Spiel. Die Datenbank ermöglicht das Speichern, Teilen und Bewerten von Levels.
## Datenbankschema
### Tabellen
#### 1. `levels`
Speichert alle Level-Daten inklusive der Voxel-Informationen.
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| `id` | UUID | Primärschlüssel, automatisch generiert |
| `name` | TEXT | Name des Levels |
| `description` | TEXT | Optionale Beschreibung des Levels |
| `user_id` | UUID | Fremdschlüssel zur `auth.users`-Tabelle, Ersteller des Levels |
| `voxel_data` | JSONB | Speichert alle Blöcke mit Position und Typ als JSON |
| `spawn_point` | JSONB | Position des Spawn-Punkts als JSON `{x, y, z}` |
| `world_size` | JSONB | Größe der Welt als JSON `{width, height, depth}` |
| `is_public` | BOOLEAN | Gibt an, ob das Level öffentlich ist (Standard: false) |
| `created_at` | TIMESTAMP | Zeitpunkt der Erstellung |
| `updated_at` | TIMESTAMP | Zeitpunkt der letzten Aktualisierung |
| `play_count` | INTEGER | Anzahl der Aufrufe des Levels |
| `likes_count` | INTEGER | Anzahl der Likes des Levels |
| `difficulty` | TEXT | Optionaler Schwierigkeitsgrad des Levels |
| `tags` | TEXT[] | Array von Tags zur Kategorisierung |
| `thumbnail_url` | TEXT | Optionale URL zum Vorschaubild |
#### 2. `level_likes`
Speichert die Likes für Levels.
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| `id` | UUID | Primärschlüssel, automatisch generiert |
| `level_id` | UUID | Fremdschlüssel zur `levels`-Tabelle |
| `user_id` | UUID | Fremdschlüssel zur `auth.users`-Tabelle |
| `created_at` | TIMESTAMP | Zeitpunkt des Likes |
Constraints:
- Unique-Constraint auf `(level_id, user_id)`, um doppelte Likes zu verhindern
#### 3. `level_plays`
Speichert Spielstatistiken für Levels.
| Spalte | Typ | Beschreibung |
|--------|-----|-------------|
| `id` | UUID | Primärschlüssel, automatisch generiert |
| `level_id` | UUID | Fremdschlüssel zur `levels`-Tabelle |
| `user_id` | UUID | Fremdschlüssel zur `auth.users`-Tabelle |
| `completion_time` | FLOAT | Zeit in Sekunden (null, wenn nicht abgeschlossen) |
| `attempts` | INTEGER | Anzahl der Versuche (Standard: 1) |
| `completed` | BOOLEAN | Gibt an, ob das Level abgeschlossen wurde |
| `created_at` | TIMESTAMP | Zeitpunkt des Spielversuchs |
### Indizes
Zur Optimierung der Abfrageleistung wurden folgende Indizes erstellt:
```sql
CREATE INDEX idx_levels_user_id ON levels(user_id);
CREATE INDEX idx_levels_is_public ON levels(is_public);
CREATE INDEX idx_level_likes_level_id ON level_likes(level_id);
CREATE INDEX idx_level_likes_user_id ON level_likes(user_id);
CREATE INDEX idx_level_plays_level_id ON level_plays(level_id);
CREATE INDEX idx_level_plays_user_id ON level_plays(user_id);
```
### Funktionen und Trigger
#### 1. `update_updated_at_column()`
Aktualisiert automatisch den `updated_at`-Zeitstempel bei Änderungen an einem Level.
```sql
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_levels_updated_at
BEFORE UPDATE ON levels
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
```
#### 2. `update_likes_count()`
Aktualisiert automatisch den `likes_count` in der `levels`-Tabelle, wenn ein Like hinzugefügt oder entfernt wird.
```sql
CREATE OR REPLACE FUNCTION update_likes_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE levels SET likes_count = likes_count + 1 WHERE id = NEW.level_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE levels SET likes_count = likes_count - 1 WHERE id = OLD.level_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_level_likes_count
AFTER INSERT OR DELETE ON level_likes
FOR EACH ROW
EXECUTE FUNCTION update_likes_count();
```
#### 3. `update_play_count()`
Aktualisiert automatisch den `play_count` in der `levels`-Tabelle, wenn ein Spielversuch hinzugefügt wird.
```sql
CREATE OR REPLACE FUNCTION update_play_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE levels SET play_count = play_count + 1 WHERE id = NEW.level_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_level_play_count
AFTER INSERT ON level_plays
FOR EACH ROW
EXECUTE FUNCTION update_play_count();
```
#### 4. `increment_play_count(level_id UUID)`
Erhöht den `play_count` für ein Level, auch wenn kein Benutzer angemeldet ist.
```sql
CREATE OR REPLACE FUNCTION increment_play_count(level_id UUID)
RETURNS VOID AS $$
BEGIN
UPDATE levels SET play_count = play_count + 1 WHERE id = level_id;
END;
$$ LANGUAGE plpgsql;
```
## Sicherheitsrichtlinien (RLS)
Die Datenbank verwendet Row Level Security (RLS), um den Zugriff auf Daten zu kontrollieren:
### 1. Levels
```sql
-- Levels sind für Ersteller und öffentlich sichtbar
CREATE POLICY "Levels sind für Ersteller und öffentlich sichtbar"
ON levels FOR SELECT
USING (auth.uid() = user_id OR is_public = true);
-- Levels können nur vom Ersteller bearbeitet werden
CREATE POLICY "Levels können nur vom Ersteller bearbeitet werden"
ON levels FOR UPDATE
USING (auth.uid() = user_id);
-- Levels können nur vom Ersteller gelöscht werden
CREATE POLICY "Levels können nur vom Ersteller gelöscht werden"
ON levels FOR DELETE
USING (auth.uid() = user_id);
-- Neue Levels können nur vom authentifizierten Benutzer erstellt werden
CREATE POLICY "Neue Levels können nur vom authentifizierten Benutzer erstellt werden"
ON levels FOR INSERT
WITH CHECK (auth.uid() = user_id);
```
### 2. Level Likes
```sql
-- Likes können von jedem authentifizierten Benutzer gesehen werden
CREATE POLICY "Likes können von jedem authentifizierten Benutzer gesehen werden"
ON level_likes FOR SELECT
USING (true);
-- Likes können nur vom Benutzer selbst erstellt werden
CREATE POLICY "Likes können nur vom Benutzer selbst erstellt werden"
ON level_likes FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Likes können nur vom Benutzer selbst entfernt werden
CREATE POLICY "Likes können nur vom Benutzer selbst entfernt werden"
ON level_likes FOR DELETE
USING (auth.uid() = user_id);
```
### 3. Level Plays
```sql
-- Spielstatistiken sind für Level-Ersteller sichtbar
CREATE POLICY "Spielstatistiken sind für Level-Ersteller sichtbar"
ON level_plays FOR SELECT
USING (EXISTS (
SELECT 1 FROM levels
WHERE levels.id = level_plays.level_id
AND levels.user_id = auth.uid()
) OR auth.uid() = user_id);
-- Spielstatistiken können von jedem authentifizierten Benutzer erstellt werden
CREATE POLICY "Spielstatistiken können von jedem authentifizierten Benutzer erstellt werden"
ON level_plays FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Spielstatistiken können nur vom Benutzer selbst aktualisiert werden
CREATE POLICY "Spielstatistiken können nur vom Benutzer selbst aktualisiert werden"
ON level_plays FOR UPDATE
USING (auth.uid() = user_id);
```
## Datenformat
### Voxel-Daten (JSONB)
Die Voxel-Daten werden als JSONB in der `voxel_data`-Spalte gespeichert. Das Format ist:
```json
{
"x,y,z": {
"type": "blockType",
"isSpawnPoint": false,
"isGoal": false
},
"1,1,1": {
"type": "grass",
"isSpawnPoint": true,
"isGoal": false
},
"5,1,5": {
"type": "goal",
"isSpawnPoint": false,
"isGoal": true
}
}
```
Wobei:
- Der Schlüssel ist eine Zeichenkette im Format "x,y,z", die die Position des Blocks angibt
- `type` ist der Typ des Blocks (z.B. "grass", "stone", "lava")
- `isSpawnPoint` gibt an, ob dieser Block ein Spawn-Punkt ist
- `isGoal` gibt an, ob dieser Block ein Ziel ist
## Verwendung in der Anwendung
Die Datenbank wird über den Supabase-Client in der Anwendung angesprochen. Dafür wurden folgende Services implementiert:
1. `LevelService`: Zum Speichern, Laden und Verwalten von Levels
2. `AuthService`: Für die Benutzerauthentifizierung
Beispiel für die Verwendung des `LevelService`:
```typescript
import { LevelService } from '../services/LevelService';
// Level speichern
const level = {
name: 'Mein Level',
blocks: [/* ... */],
spawnPoint: { x: 1, y: 1, z: 1 },
worldSize: { width: 20, height: 10, depth: 20 },
isPublic: true
};
const levelId = await LevelService.saveLevel(level);
// Level laden
const loadedLevel = await LevelService.loadLevel(levelId);
// Öffentliche Levels abrufen
const publicLevels = await LevelService.getPublicLevels();
// Level liken
await LevelService.likeLevel(levelId);
```
## Erweiterungsmöglichkeiten
Die Datenbankstruktur kann in Zukunft um folgende Funktionen erweitert werden:
1. **Kommentare**: Eine neue Tabelle für Kommentare zu Levels
2. **Leaderboards**: Detaillierte Bestenlisten für jedes Level
3. **Benutzerprofile**: Erweiterte Benutzerinformationen und Statistiken
4. **Freundschaften**: Beziehungen zwischen Benutzern
5. **Sammlungen**: Gruppierung von Levels in Sammlungen oder Playlists

View file

@ -0,0 +1,38 @@
# sv
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
# create a new project in my-app
npx sv create my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

View file

@ -0,0 +1,36 @@
import prettier from 'eslint-config-prettier';
import js from '@eslint/js';
import { includeIgnoreFile } from '@eslint/compat';
import svelte from 'eslint-plugin-svelte';
import globals from 'globals';
import { fileURLToPath } from 'node:url';
import ts from 'typescript-eslint';
import svelteConfig from './svelte.config.js';
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
export default ts.config(
includeIgnoreFile(gitignorePath),
js.configs.recommended,
...ts.configs.recommended,
...svelte.configs.recommended,
prettier,
...svelte.configs.prettier,
{
languageOptions: {
globals: { ...globals.browser, ...globals.node }
},
rules: { 'no-undef': 'off' }
},
{
files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
languageOptions: {
parserOptions: {
projectService: true,
extraFileExtensions: ['.svelte'],
parser: ts.parser,
svelteConfig
}
}
}
);

View file

@ -0,0 +1,40 @@
{
"name": "voxel-lava",
"private": true,
"version": "0.0.1",
"type": "module",
"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",
"format": "prettier --write .",
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@types/three": "^0.176.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.6"
},
"dependencies": {
"axios": "^1.11.0",
"pocketbase": "^0.26.2",
"three": "^0.176.0"
}
}

13
games/voxel-lava/src/app.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
// See https://svelte.dev/docs/kit/types#app.d.ts
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};

View file

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

View file

@ -0,0 +1,607 @@
/**
* BlockTypes.ts
*
* Diese Datei enthält Definitionen und Funktionalitäten für das Blocksystem des Voxel-Spiels.
* Sie definiert die verschiedenen Blocktypen, Hilfsfunktionen zur Texturerstellung und
* den BlockManager, der die zentrale Verwaltung aller Blöcke übernimmt.
*/
import * as THREE from 'three';
/**
* BlockType Interface
* Definiert die Eigenschaften, die ein Blocktyp haben kann:
* - name: Anzeigename des Blocks
* - color: Hauptfarbe des Blocks (hexadezimal)
* - emissive: Leuchtfarbe für Blöcke, die Licht emittieren sollen
* - topColor/sideColor/bottomColor: Spezifische Farben für verschiedene Seiten (z.B. bei Gras)
* - opacity: Transparenz des Blocks (1 = undurchsichtig, <1 = durchsichtig)
* - solid: Ob der Block fest ist und Kollisionen verursacht
* - isDeadly: Ob der Block tödlich ist (z.B. Lava)
* - frictionFactor: Reibungsfaktor für die Spielerphysik
* - isGoal: Ob der Block ein Zielblock ist
*/
export type BlockType = {
name: string;
color?: number;
emissive?: number;
topColor?: number;
sideColor?: number;
bottomColor?: number;
opacity?: number;
solid: boolean;
isDeadly: boolean;
frictionFactor: number;
isGoal: boolean;
isFragile?: boolean;
breakTimer?: number;
isTrampoline?: boolean;
trampolineForce?: number;
};
/**
* BlockTypes Interface
* Ein Dictionary/Map von Blocktypen, indiziert durch ihren String-Schlüssel
*/
export type BlockTypes = {
[key: string]: BlockType;
};
/**
* BLOCK_TYPES Konstante
* Definiert alle verfügbaren Blocktypen im Spiel mit ihren Eigenschaften:
* - lava: Tödlicher Block mit rötlicher Farbe und Leuchteffekt
* - grass: Block mit grüner Oberseite und braunen Seiten
* - ice: Transparenter, rutschiger Block
* - goal: Zielblock mit gelber Farbe und Leuchteffekt
* - spawn: Spawn-Punkt mit grüner Farbe und Leuchteffekt
* - fragile: Zerbrechlicher Block, der nach einer Sekunde zerbricht, wenn der Spieler darauf steht
* - trampoline: Trampolinblock, der den Spieler in die Höhe schießt
*/
export const BLOCK_TYPES: BlockTypes = {
lava: { name: "Lava", color: 0xCF1020, topColor: 0xCF1020, sideColor: 0xAA0000, bottomColor: 0x990000, emissive: 0xFF4500, solid: true, isDeadly: true, frictionFactor: 0.8, isGoal: false },
grass: { name: "Gras", color: 0x8B4513, topColor: 0x559944, sideColor: 0x8B4513, bottomColor: 0x8B4513, solid: true, isDeadly: false, frictionFactor: 0.7, isGoal: false },
ice: { name: "Eis", color: 0xADD8E6, topColor: 0xADD8E6, sideColor: 0x89CFF0, bottomColor: 0x77A7C5, opacity: 0.85, solid: true, isDeadly: false, frictionFactor: 0.98, isGoal: false },
goal: { name: "Ziel", color: 0xFFFF00, topColor: 0xFFFF00, sideColor: 0xFFD700, bottomColor: 0xDAA520, emissive: 0xCCCC00, solid: true, isDeadly: false, frictionFactor: 0.8, isGoal: true },
spawn: { name: "Spawn", color: 0x00FF00, topColor: 0x00FF00, sideColor: 0x00CC00, bottomColor: 0x009900, emissive: 0x00AA00, solid: true, isDeadly: false, frictionFactor: 0.7, isGoal: false },
fragile: { name: "Zerbrechlich", color: 0xA0522D, topColor: 0xA0522D, sideColor: 0x8B4513, bottomColor: 0x654321, emissive: 0x8B4513, solid: true, isDeadly: false, frictionFactor: 0.7, isGoal: false, isFragile: true, breakTimer: 1.0 },
trampoline: { name: "Trampolin", color: 0x0000FF, topColor: 0x0000FF, sideColor: 0x0000CC, bottomColor: 0x000099, emissive: 0x0000AA, solid: true, isDeadly: false, frictionFactor: 0.7, isGoal: false, isTrampoline: true, trampolineForce: 20.0 }
};
/**
* Textur-Erstellungsfunktionen
* Diese Funktionen erzeugen dynamisch Texturen für die verschiedenen Blockseiten,
* indem sie Canvas-Elemente erstellen und mit den entsprechenden Farben füllen.
*/
/**
* createSideTexture
* Erstellt eine Textur für die Seitenflächen eines Blocks.
*
* @param color - Die Farbe der Textur als Hexadezimalwert
* @returns Eine THREE.CanvasTexture mit der erstellten Textur
*/
export function createSideTexture(color: number): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width=16; canvas.height=16;
const ctx = canvas.getContext('2d');
if (ctx) {
// Fülle den Hintergrund mit der angegebenen Farbe
// Stelle sicher, dass die Farbe korrekt formatiert ist (6 Stellen Hex)
const colorHex = color.toString(16).padStart(6,'0');
ctx.fillStyle=`#${colorHex}`;
ctx.fillRect(0,0,16,16);
// Füge einen leicht transparenten schwarzen Rahmen hinzu
ctx.strokeStyle='#00000033';
ctx.strokeRect(0,0,16,16);
console.log(`Seitentextur erstellt mit Farbe: #${colorHex}`);
}
return new THREE.CanvasTexture(canvas);
}
/**
* createTopTexture
* Erstellt eine Textur für die Oberseite eines Blocks.
* Diese Textur hat einen weißen Rahmen, um sie von den Seitenflächen zu unterscheiden.
*
* @param color - Die Farbe der Textur als Hexadezimalwert
* @returns Eine THREE.CanvasTexture mit der erstellten Textur
*/
export function createTopTexture(color: number): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width=16; canvas.height=16;
const ctx = canvas.getContext('2d');
if (ctx) {
// Fülle den Hintergrund mit der angegebenen Farbe
// Stelle sicher, dass die Farbe korrekt formatiert ist (6 Stellen Hex)
const colorHex = color.toString(16).padStart(6,'0');
ctx.fillStyle=`#${colorHex}`;
ctx.fillRect(0,0,16,16);
// Füge einen leicht transparenten weißen Rahmen hinzu
ctx.strokeStyle='#FFFFFF33';
ctx.strokeRect(0,0,16,16);
console.log(`Oberseitentextur erstellt mit Farbe: #${colorHex}`);
}
return new THREE.CanvasTexture(canvas);
}
/**
* createBottomTexture
* Erstellt eine Textur für die Unterseite eines Blocks.
* Diese Textur hat keinen Rahmen, da die Unterseite meist nicht sichtbar ist.
*
* @param color - Die Farbe der Textur als Hexadezimalwert
* @returns Eine THREE.CanvasTexture mit der erstellten Textur
*/
export function createBottomTexture(color: number): THREE.CanvasTexture {
const canvas = document.createElement('canvas');
canvas.width=16; canvas.height=16;
const ctx = canvas.getContext('2d');
if (ctx) {
// Fülle den Hintergrund mit der angegebenen Farbe
// Stelle sicher, dass die Farbe korrekt formatiert ist (6 Stellen Hex)
const colorHex = color.toString(16).padStart(6,'0');
ctx.fillStyle=`#${colorHex}`;
ctx.fillRect(0,0,16,16);
// Kein Rahmen für die Unterseite
console.log(`Unterseitentextur erstellt mit Farbe: #${colorHex}`);
}
return new THREE.CanvasTexture(canvas);
}
/**
* getVoxelKey
* Erzeugt einen eindeutigen Schlüssel für einen Voxel basierend auf seinen Koordinaten.
* Dieser Schlüssel wird verwendet, um Voxel in einer Map zu speichern und abzurufen.
*
* @param x - X-Koordinate des Voxels
* @param y - Y-Koordinate des Voxels
* @param z - Z-Koordinate des Voxels
* @returns Ein String im Format "x,y,z", der als eindeutiger Schlüssel dient
*/
export function getVoxelKey(x: number, y: number, z: number): string {
return `${x},${y},${z}`;
}
/**
* BlockManager Klasse
*
* Diese Klasse ist verantwortlich für die zentrale Verwaltung aller Blöcke im Spiel.
* Sie kapselt die Logik zum Hinzufügen, Entfernen und Abfragen von Blöcken sowie
* die Verwaltung von Zielblöcken und deren Erreichungsstatus.
*/
export class BlockManager {
/** Die THREE.js-Szene, in der die Blöcke gerendert werden */
private scene: THREE.Scene;
/** Map, die alle Voxel-Meshes anhand ihrer Koordinaten speichert */
private voxels: Map<string, THREE.Mesh>;
/** Größe eines einzelnen Voxels */
private voxelSize: number;
/** Liste aller Zielblöcke im Spiel */
private goalBlocks: { x: number, y: number, z: number }[] = [];
/** Liste der bereits erreichten Zielblöcke */
private reachedGoals: { x: number, y: number, z: number }[] = [];
/** Callback-Funktion, um die Zielanzeige im UI zu aktualisieren */
private updateGoalDisplayCallback: () => void;
/** Aktuell ausgewählter Blocktyp für das Platzieren neuer Blöcke */
private currentBlockType: string = 'grass';
/**
* Konstruktor für den BlockManager
*
* @param scene - Die THREE.js-Szene, in der die Blöcke gerendert werden
* @param voxels - Map, die alle Voxel-Meshes anhand ihrer Koordinaten speichert
* @param voxelSize - Größe eines einzelnen Voxels
* @param goalBlocks - Liste aller Zielblöcke im Spiel
* @param reachedGoals - Liste der bereits erreichten Zielblöcke
* @param updateGoalDisplayCallback - Callback-Funktion, um die Zielanzeige im UI zu aktualisieren
*/
constructor(
scene: THREE.Scene,
voxels: Map<string, THREE.Mesh>,
voxelSize: number,
goalBlocks: { x: number, y: number, z: number }[],
reachedGoals: { x: number, y: number, z: number }[],
updateGoalDisplayCallback: () => void
) {
this.scene = scene;
this.voxels = voxels;
this.voxelSize = voxelSize;
this.goalBlocks = goalBlocks;
this.reachedGoals = reachedGoals;
this.updateGoalDisplayCallback = updateGoalDisplayCallback;
}
/**
* Setzt den aktuell ausgewählten Blocktyp für das Platzieren neuer Blöcke
*
* @param type - Der Typ des Blocks (z.B. 'grass', 'lava', 'ice')
*/
setCurrentBlockType(type: string): void {
this.currentBlockType = type;
}
/**
* Gibt den aktuell ausgewählten Blocktyp zurück
*
* @returns Der aktuelle Blocktyp als String
*/
getCurrentBlockType(): string {
return this.currentBlockType;
}
/**
* Findet das Mesh eines Voxels anhand seiner Koordinaten
*
* @param x - X-Koordinate des Voxels
* @param y - Y-Koordinate des Voxels
* @param z - Z-Koordinate des Voxels
* @returns Das THREE.Mesh des Voxels oder undefined, wenn kein Voxel an dieser Position existiert
*/
getVoxelMesh(x: number, y: number, z: number): THREE.Mesh | undefined {
return this.voxels.get(getVoxelKey(x, y, z));
}
/**
* Ermittelt den Blocktyp eines Voxels anhand seiner Koordinaten
*
* @param x - X-Koordinate des Voxels
* @param y - Y-Koordinate des Voxels
* @param z - Z-Koordinate des Voxels
* @returns Das BlockType-Objekt des Voxels oder null, wenn kein Voxel an dieser Position existiert
*/
getVoxelBlockType(x: number, y: number, z: number): BlockType | null {
const mesh = this.getVoxelMesh(x, y, z);
return mesh && mesh.userData.type ? BLOCK_TYPES[mesh.userData.type as string] : null;
}
/**
* Fügt einen neuen Voxel-Block an der angegebenen Position hinzu
*
* @param x - X-Koordinate des Voxels
* @param y - Y-Koordinate des Voxels
* @param z - Z-Koordinate des Voxels
* @param type - Typ des Blocks (Standard: der aktuell ausgewählte Blocktyp)
*/
addVoxel(x: number, y: number, z: number, type: string = this.currentBlockType): void {
// Erstelle einen eindeutigen Schlüssel für den Voxel
const key = getVoxelKey(x, y, z);
// Wenn an dieser Position bereits ein Block existiert, breche ab
if (this.voxels.has(key)) return;
// Hole die Block-Definition für den angegebenen Typ
const blockDef = BLOCK_TYPES[type];
if (!blockDef) {
console.warn(`Unbekannter Blocktyp: ${type}`);
return;
}
// Erstelle die Geometrie für den Voxel
const voxelGeometry = new THREE.BoxGeometry(this.voxelSize, this.voxelSize, this.voxelSize);
let voxelMaterial;
// Verwende verschiedene Texturen für Ober-, Unter- und Seitenflächen für alle Blöcke
if (blockDef.topColor !== undefined && blockDef.sideColor !== undefined && blockDef.bottomColor !== undefined) {
// Erstelle separate Materialien für jede Seite des Blocks
// Die Reihenfolge ist wichtig: [rechts, links, oben, unten, vorne, hinten]
const rightMaterial = new THREE.MeshStandardMaterial({ map: createSideTexture(blockDef.sideColor) });
const leftMaterial = new THREE.MeshStandardMaterial({ map: createSideTexture(blockDef.sideColor) });
const topMaterial = new THREE.MeshStandardMaterial({ map: createTopTexture(blockDef.topColor) });
const bottomMaterial = new THREE.MeshStandardMaterial({ map: createBottomTexture(blockDef.bottomColor) });
const frontMaterial = new THREE.MeshStandardMaterial({ map: createSideTexture(blockDef.sideColor) });
const backMaterial = new THREE.MeshStandardMaterial({ map: createSideTexture(blockDef.sideColor) });
// Setze die Materialien in der richtigen Reihenfolge
voxelMaterial = [
rightMaterial, // rechts (+x)
leftMaterial, // links (-x)
topMaterial, // oben (+y)
bottomMaterial, // unten (-y)
frontMaterial, // vorne (+z)
backMaterial // hinten (-z)
];
// Füge Leuchteffekte hinzu, falls definiert
if (blockDef.emissive) {
voxelMaterial.forEach(material => {
material.emissive = new THREE.Color(blockDef.emissive);
material.emissiveIntensity = 0.9;
});
}
// Füge Transparenz hinzu, falls definiert
if (blockDef.opacity !== undefined && blockDef.opacity < 1) {
voxelMaterial.forEach(material => {
material.transparent = true;
material.opacity = blockDef.opacity || 1;
});
}
} else {
// Fallback für Blöcke ohne definierte Seitenfarben
const materialProperties: THREE.MeshStandardMaterialParameters = {
color: new THREE.Color(blockDef.color || 0xffffff),
transparent: blockDef.opacity !== undefined && blockDef.opacity < 1,
opacity: blockDef.opacity !== undefined ? blockDef.opacity : 1,
};
// Füge Leuchteffekte hinzu, falls definiert
if (blockDef.emissive) {
materialProperties.emissive = new THREE.Color(blockDef.emissive);
materialProperties.emissiveIntensity = 0.9;
}
voxelMaterial = new THREE.MeshStandardMaterial(materialProperties);
}
// Erstelle das Mesh und positioniere es
const voxelMesh = new THREE.Mesh(voxelGeometry, voxelMaterial);
voxelMesh.position.set(
x * this.voxelSize + this.voxelSize / 2, // Zentriere den Block an der X-Position
y * this.voxelSize + this.voxelSize / 2, // Zentriere den Block an der Y-Position
z * this.voxelSize + this.voxelSize / 2 // Zentriere den Block an der Z-Position
);
voxelMesh.castShadow = true;
voxelMesh.receiveShadow = true;
// Speichere Metadaten im userData-Objekt
voxelMesh.userData = { x, y, z, type, isSpawnPoint: false };
// Wenn es ein Zielblock ist, füge ihn zur Zielliste hinzu
if (blockDef.isGoal) {
console.log(`Zielblock hinzugefügt an Position (${x}, ${y}, ${z})`);
this.goalBlocks.push({ x, y, z });
// Aktualisiere die Zielanzeige
this.updateGoalDisplayCallback();
}
// Füge das Mesh zur Szene hinzu und speichere es in der Voxel-Map
this.scene.add(voxelMesh);
this.voxels.set(key, voxelMesh);
}
/**
* Entfernt einen Voxel-Block an der angegebenen Position
*
* @param x - X-Koordinate des Voxels
* @param y - Y-Koordinate des Voxels
* @param z - Z-Koordinate des Voxels
* @param forceRemove - Wenn true, werden auch Spawn-Blöcke entfernt, die normalerweise geschützt sind
*/
removeVoxel(x: number, y: number, z: number, forceRemove: boolean = false): void {
// Erstelle einen eindeutigen Schlüssel für den Voxel
const key = getVoxelKey(x, y, z);
if (this.voxels.has(key)) {
const voxelMesh = this.voxels.get(key);
if (!voxelMesh) return;
// Prüfe, ob es sich um einen Spawn-Block handelt
// Spawn-Blöcke sind normalerweise geschützt, um versehentliches Entfernen zu verhindern
if (voxelMesh.userData.isSpawnPoint && !forceRemove) {
// Wenn der Spawn-Block durch einen Rechtsklick entfernt werden soll, verhindern wir das
if (voxelMesh.userData.type === 'spawn') {
console.log("Spawn-Block kann nicht direkt entfernt werden. Setze einen neuen Spawn-Block, um diesen zu ersetzen.");
return;
} else {
console.log("Spawn-Punkt kann nicht entfernt werden.");
return;
}
}
// Wenn es ein Zielblock ist, entferne ihn aus der Zielliste
const blockDef = BLOCK_TYPES[voxelMesh.userData.type];
if (blockDef && blockDef.isGoal) {
const pos = { x, y, z };
// Entferne aus der goalBlocks-Liste
this.goalBlocks = this.goalBlocks.filter(goal =>
!(goal.x === pos.x && goal.y === pos.y && goal.z === pos.z)
);
// Entferne auch aus der reachedGoals-Liste, falls vorhanden
this.reachedGoals = this.reachedGoals.filter(goal =>
!(goal.x === pos.x && goal.y === pos.y && goal.z === pos.z)
);
console.log(`Zielblock entfernt an Position (${x}, ${y}, ${z})`);
// Aktualisiere die Zielanzeige
this.updateGoalDisplayCallback();
}
// Entferne den Block aus der Szene und bereinige Ressourcen
this.scene.remove(voxelMesh);
// Bereinige die Materialien, um Speicherlecks zu vermeiden
if (Array.isArray(voxelMesh.material)) {
// Bei mehreren Materialien (z.B. Gras-Block) jeden einzeln bereinigen
voxelMesh.material.forEach((m: THREE.Material) => {
if ((m as THREE.MeshStandardMaterial).map) (m as THREE.MeshStandardMaterial).map?.dispose();
m.dispose();
});
} else {
// Bei einem einzelnen Material
const mat = voxelMesh.material as THREE.MeshStandardMaterial;
if (mat.map) mat.map.dispose();
mat.dispose();
}
// Bereinige die Geometrie
voxelMesh.geometry.dispose();
// Entferne den Voxel aus der Map
this.voxels.delete(key);
}
}
/**
* Entfernt alle Blöcke eines bestimmten Typs aus der Welt
*
* @param types - Array mit Blocktypen, die entfernt werden sollen (z.B. ['fragile', 'ice'])
*/
removeBlocksByType(types: string[]): void {
// Sammle zuerst alle zu entfernenden Blöcke
const blocksToRemove: {x: number, y: number, z: number}[] = [];
// Durchlaufe alle Voxel und prüfe, ob ihr Typ entfernt werden soll
this.voxels.forEach((voxelMesh, key) => {
if (types.includes(voxelMesh.userData.type)) {
blocksToRemove.push({
x: voxelMesh.userData.x,
y: voxelMesh.userData.y,
z: voxelMesh.userData.z
});
}
});
// Entferne alle identifizierten Blöcke
console.log(`Entferne ${blocksToRemove.length} Blöcke vom Typ: ${types.join(', ')}`);
blocksToRemove.forEach(block => {
this.removeVoxel(block.x, block.y, block.z);
});
}
/**
* Getter und Setter für Zielblöcke und erreichte Ziele
*/
/**
* Gibt die Liste aller Zielblöcke zurück
*
* @returns Array mit den Koordinaten aller Zielblöcke
*/
getGoalBlocks(): { x: number, y: number, z: number }[] {
return this.goalBlocks;
}
/**
* Gibt die Liste aller erreichten Zielblöcke zurück
*
* @returns Array mit den Koordinaten aller erreichten Zielblöcke
*/
getReachedGoals(): { x: number, y: number, z: number }[] {
return this.reachedGoals;
}
/**
* Setzt die Liste der erreichten Zielblöcke und aktualisiert die Zielanzeige
*
* @param goals - Array mit den Koordinaten der erreichten Zielblöcke
*/
setReachedGoals(goals: { x: number, y: number, z: number }[]): void {
this.reachedGoals = goals;
this.updateGoalDisplayCallback();
}
/**
* Fügt einen erreichten Zielblock zur Liste hinzu und aktualisiert die Zielanzeige
*
* @param goal - Koordinaten des erreichten Zielblocks
*/
addReachedGoal(goal: { x: number, y: number, z: number }): void {
this.reachedGoals.push(goal);
this.updateGoalDisplayCallback();
}
/**
* Überprüft, ob ein bestimmter Zielblock bereits erreicht wurde
*
* @param x - X-Koordinate des zu prüfenden Zielblocks
* @param y - Y-Koordinate des zu prüfenden Zielblocks
* @param z - Z-Koordinate des zu prüfenden Zielblocks
* @returns true, wenn der Zielblock bereits erreicht wurde, sonst false
*/
isGoalReached(x: number, y: number, z: number): boolean {
return this.reachedGoals.some(goal =>
goal.x === x && goal.y === y && goal.z === z
);
}
/**
* Überprüft, ob alle Zielblöcke im Spiel erreicht wurden
*
* @returns true, wenn alle Zielblöcke erreicht wurden, sonst false.
* Gibt false zurück, wenn keine Zielblöcke vorhanden sind.
*/
areAllGoalsReached(): boolean {
// Wenn keine Zielblöcke vorhanden sind, gib false zurück
if (this.goalBlocks.length === 0) return false;
// Überprüfe, ob die Anzahl der erreichten Ziele gleich der Anzahl aller Ziele ist
return this.reachedGoals.length === this.goalBlocks.length;
}
/**
* Gibt alle Blöcke in einem serialisierbaren Format zurück
*
* @returns Array mit allen Blöcken in einem serialisierbaren Format
*/
getSerializableBlocks(): { x: number, y: number, z: number, type: string, isSpawnPoint: boolean, isGoal: boolean }[] {
const blocks: { x: number, y: number, z: number, type: string, isSpawnPoint: boolean, isGoal: boolean }[] = [];
// Gib die Größe der voxels-Map aus
console.log('Anzahl der Blöcke in voxels Map:', this.voxels.size);
// Iteriere über alle Blöcke in der voxels-Map
this.voxels.forEach((mesh, key) => {
if (mesh && mesh.userData) {
const block = {
x: mesh.userData.x,
y: mesh.userData.y,
z: mesh.userData.z,
type: mesh.userData.type,
isSpawnPoint: mesh.userData.isSpawnPoint || false,
isGoal: mesh.userData.type === 'goal'
};
// Überprüfe, ob alle erforderlichen Felder vorhanden sind
if (typeof block.x === 'number' &&
typeof block.y === 'number' &&
typeof block.z === 'number' &&
block.type) {
blocks.push(block);
} else {
console.warn('Ungültiger Block gefunden:', mesh.userData);
}
}
});
console.log('Anzahl der serialisierbaren Blöcke:', blocks.length);
if (blocks.length > 0) {
console.log('Erster serialisierbarer Block:', blocks[0]);
}
return blocks;
}
/**
* Entfernt alle Blöcke aus der Welt, mit Ausnahme von geschützten Blöcken
*
* @param preserveSpawnPoints - Wenn true, werden Spawn-Punkte nicht entfernt (Standard: true)
*/
removeAllBlocks(preserveSpawnPoints: boolean = true): void {
console.log(`Entferne alle Blöcke (Spawn-Punkte ${preserveSpawnPoints ? 'werden beibehalten' : 'werden auch entfernt'})`);
// Sammle zuerst alle zu entfernenden Blöcke
const blocksToRemove: {x: number, y: number, z: number}[] = [];
// Durchlaufe alle Voxel
this.voxels.forEach((voxelMesh, key) => {
// Wenn preserveSpawnPoints true ist, überspringe Spawn-Blöcke
if (preserveSpawnPoints && voxelMesh.userData.isSpawnPoint) {
return;
}
blocksToRemove.push({
x: voxelMesh.userData.x,
y: voxelMesh.userData.y,
z: voxelMesh.userData.z
});
});
// Entferne alle identifizierten Blöcke
console.log(`Entferne ${blocksToRemove.length} Blöcke`);
blocksToRemove.forEach(block => {
this.removeVoxel(block.x, block.y, block.z, !preserveSpawnPoints); // Wenn preserveSpawnPoints false ist, setze forceRemove auf true
});
// Setze die Ziellisten zurück, wenn alle Blöcke entfernt werden
if (!preserveSpawnPoints) {
this.goalBlocks = [];
this.reachedGoals = [];
this.updateGoalDisplayCallback();
}
}
}

View file

@ -0,0 +1,589 @@
/**
* PlayerController.ts
*
* Diese Klasse ist verantwortlich für die Verwaltung der Spielerphysik, Bewegung und Kollisionserkennung.
* Sie kapselt die gesamte Spielerlogik und macht den GameCanvas-Code übersichtlicher.
*/
import * as THREE from 'three';
import { BlockManager } from './BlockTypes';
import type { BlockType } from './BlockTypes';
export interface PlayerConfig {
height: number;
radius: number;
speed: number;
jumpVelocity: number;
gravity: number;
}
export interface PlayerPosition {
x: number;
y: number;
z: number;
}
export class PlayerController {
// Spieler-Eigenschaften
private height: number;
private radius: number;
private speed: number;
private jumpVelocity: number;
private gravity: number;
// Spieler-Zustand
private cameraObject: THREE.PerspectiveCamera;
private blockManager: BlockManager;
private voxelSize: number;
private keyboardState: { [key: string]: boolean } = {};
private input: THREE.Vector3 = new THREE.Vector3();
private velocity: THREE.Vector3 = new THREE.Vector3();
private onGround: boolean = false;
private simulatedJump: boolean = false;
private isDying: boolean = false;
private deathTimer: number = 0;
private hasMovedSinceSpawn: boolean = false; // Flag, um zu verfolgen, ob der Spieler sich seit dem Spawn bewegt hat
private fragileTouchedBlocks: Map<string, { x: number, y: number, z: number, timer: number }> = new Map();
// Callback-Funktionen
private onRespawn: (isInitialSpawn: boolean) => void;
private onGoalReached: (position: PlayerPosition) => void;
private onFirstMovement?: () => void; // Neue Callback-Funktion für die erste Bewegung (optional)
/**
* Konstruktor für den PlayerController
*
* @param config - Konfiguration für den Spieler (Höhe, Radius, Geschwindigkeit, etc.)
* @param camera - Die Kamera, die als Spielerobjekt dient
* @param blockManager - Der BlockManager für Kollisionsabfragen
* @param voxelSize - Die Größe eines Voxels in der Welt
* @param keyboardState - Der aktuelle Zustand der Tastatur
* @param onRespawn - Callback-Funktion, die beim Respawn aufgerufen wird
* @param onGoalReached - Callback-Funktion, die beim Erreichen eines Ziels aufgerufen wird
*/
constructor(
config: PlayerConfig,
camera: THREE.PerspectiveCamera,
blockManager: BlockManager,
voxelSize: number,
keyboardState: { [key: string]: boolean },
onRespawn: (isInitialSpawn?: boolean) => void,
onGoalReached: (position: PlayerPosition) => void,
onFirstMovement?: () => void // Neuer optionaler Parameter für die erste Bewegung
) {
this.height = config.height;
this.radius = config.radius;
this.speed = config.speed;
this.jumpVelocity = config.jumpVelocity;
this.gravity = config.gravity;
this.velocity = new THREE.Vector3();
this.input = new THREE.Vector3();
this.onGround = false;
this.cameraObject = camera;
this.blockManager = blockManager;
this.voxelSize = voxelSize;
this.keyboardState = keyboardState;
this.onRespawn = onRespawn;
this.onGoalReached = onGoalReached;
this.onFirstMovement = onFirstMovement; // Speichere den neuen Callback
}
/**
* Aktualisiert den Spieler für den aktuellen Frame
*
* @param deltaTime - Die Zeit seit dem letzten Frame in Sekunden
* @param isLocked - Ob die Maussteuerung aktiv ist
* @param gameWon - Ob das Spiel gewonnen wurde
*/
public update(deltaTime: number, isLocked: boolean, gameWon: boolean): void {
if (gameWon) return;
// Wenn der Spieler stirbt (in Lava versinkt)
if (this.isDying) {
this.deathTimer += deltaTime;
// Schnell in die Lava sinken, direkt auf den Boden
// Beschleunigung des Sinkens mit der Zeit
const sinkSpeed = 2.0 * (1 + this.deathTimer * 2.0); // Schnellere Beschleunigung
// Berechne die aktuelle Y-Position des Blocks, in dem der Spieler sich befindet
const playerBlockY = Math.floor(this.cameraObject.position.y / this.voxelSize);
// Berechne die Position des Bodens des Lava-Blocks
const floorY = playerBlockY * this.voxelSize;
// Sinke zum Boden des Blocks
const newY = this.cameraObject.position.y - sinkSpeed * deltaTime;
this.cameraObject.position.y = Math.max(newY, floorY);
// Nach einer bestimmten Zeit respawnen (Respawn-Funktion kümmert sich um den Rest)
if (this.deathTimer >= 0.4) { // Sehr kurze Zeit zum Respawnen (400ms)
this.isDying = false;
this.deathTimer = 0;
this.onRespawn(false); // Rufe den Respawn-Callback auf
return;
}
// Während des Sterbens keine weitere Bewegung
return;
}
this.handleKeyboardInput();
// Simulierten Sprung verarbeiten
if (this.simulatedJump) {
this.simulateJump();
this.simulatedJump = false;
}
// Schwerkraft anwenden
this.velocity.y -= this.gravity * deltaTime;
// Bewegung anwenden
this.applyMovement(deltaTime);
// Zerbrechliche Blöcke aktualisieren
this.updateFragileBlocks(deltaTime);
const playerWorldPos = this.cameraObject.position;
let newPos = playerWorldPos.clone().add(this.velocity.clone().multiplyScalar(deltaTime));
// Verfolge, welche zerbrechlichen Blöcke wir in diesem Frame berühren
const touchedBlocksInThisFrame = new Set<string>();
this.handleVerticalCollisions(newPos, playerWorldPos, touchedBlocksInThisFrame);
this.handleHorizontalCollisions(newPos, playerWorldPos);
// Aktualisiere die Liste der berührten Blöcke
this.updateTouchedBlocks(touchedBlocksInThisFrame);
// Aktualisiere die zerbrechlichen Blöcke
this.updateFragileBlocks(deltaTime);
}
/**
* Verarbeitet die Tastatureingaben und setzt den Eingabevektor
*/
private handleKeyboardInput(): void {
this.input.set(0, 0, 0);
if (this.keyboardState['KeyW'] || this.keyboardState['ArrowUp']) this.input.z = -1; // Vorwärts
if (this.keyboardState['KeyS'] || this.keyboardState['ArrowDown']) this.input.z = 1; // Rückwärts
if (this.keyboardState['KeyA'] || this.keyboardState['ArrowLeft']) this.input.x = -1; // Links
if (this.keyboardState['KeyD'] || this.keyboardState['ArrowRight']) this.input.x = 1; // Rechts
// Prüfe, ob der Spieler sich zum ersten Mal bewegt
if (!this.hasMovedSinceSpawn && (this.input.x !== 0 || this.input.z !== 0)) {
this.hasMovedSinceSpawn = true;
// Rufe den onFirstMovement-Callback auf, wenn er definiert ist
if (this.onFirstMovement) {
console.log("Erste Spielerbewegung erkannt, starte Timer...");
this.onFirstMovement();
}
}
}
/**
* Wendet die Bewegung basierend auf dem Eingabevektor an
*
* @param deltaTime - Die Zeit seit dem letzten Frame in Sekunden
*/
private applyMovement(deltaTime: number): void {
const forward = new THREE.Vector3();
this.cameraObject.getWorldDirection(forward);
forward.y = 0;
forward.normalize();
const right = new THREE.Vector3();
right.crossVectors(this.cameraObject.up, forward).normalize();
// Ermittle den Block unter dem Spieler für die Reibung
const blockBelow = this.blockManager.getVoxelBlockType(
Math.floor(this.cameraObject.position.x / this.voxelSize),
Math.floor((this.cameraObject.position.y - this.height / 2 - 0.01) / this.voxelSize),
Math.floor(this.cameraObject.position.z / this.voxelSize)
);
// Standardreibung für Gras verwenden, wenn kein Block gefunden wurde
const friction = blockBelow ? blockBelow.frictionFactor : 0.7;
const isIceBlock = blockBelow && blockBelow.frictionFactor > 0.9; // Erkennen von Eis-Blöcken
// Wenn der Spieler auf Eis steht, behalte die Geschwindigkeit bei und wende nur leichte Reibung an
if (this.onGround && isIceBlock) {
// Wenn der Spieler aktiv Eingaben macht, setze die Zielgeschwindigkeit
if (this.input.z !== 0 || this.input.x !== 0) {
let targetVelocityX = 0;
let targetVelocityZ = 0;
// Vorwärts/Rückwärts Bewegung
if (this.input.z !== 0) {
targetVelocityX -= forward.x * this.input.z * this.speed;
targetVelocityZ -= forward.z * this.input.z * this.speed;
}
// Seitwärts Bewegung (Strafe)
if (this.input.x !== 0) {
targetVelocityX -= right.x * this.input.x * this.speed;
targetVelocityZ -= right.z * this.input.x * this.speed;
}
// Mische aktuelle Geschwindigkeit mit Zielgeschwindigkeit für Rutscheffekt
this.velocity.x = this.velocity.x * 0.8 + targetVelocityX * 0.2;
this.velocity.z = this.velocity.z * 0.8 + targetVelocityZ * 0.2;
} else {
// Wenn keine Eingabe, wende nur sehr geringe Reibung an (Gleiten)
this.velocity.x *= 0.99;
this.velocity.z *= 0.99;
}
} else {
// Normales Verhalten für andere Blöcke
let targetVelocityX = 0;
let targetVelocityZ = 0;
// Vorwärts/Rückwärts Bewegung
if (this.input.z !== 0) {
targetVelocityX -= forward.x * this.input.z * this.speed;
targetVelocityZ -= forward.z * this.input.z * this.speed;
}
// Seitwärts Bewegung (Strafe)
if (this.input.x !== 0) {
targetVelocityX -= right.x * this.input.x * this.speed;
targetVelocityZ -= right.z * this.input.x * this.speed;
}
this.velocity.x = targetVelocityX;
this.velocity.z = targetVelocityZ;
// Reibung anwenden, wenn der Spieler auf dem Boden steht und sich nicht bewegt
if (this.onGround && this.input.x === 0 && this.input.z === 0) {
this.velocity.x *= (1 - (1 - friction) * 15 * deltaTime);
this.velocity.z *= (1 - (1 - friction) * 15 * deltaTime);
}
}
// Kleine Werte auf 0 setzen, um Zittern zu vermeiden
if (Math.abs(this.velocity.x) < 0.01) this.velocity.x = 0;
if (Math.abs(this.velocity.z) < 0.01) this.velocity.z = 0;
// Schwerkraft anwenden
this.velocity.y -= this.gravity * deltaTime;
// Springen
if ((this.keyboardState['Space'] || this.simulatedJump) && this.onGround) {
this.velocity.y = this.jumpVelocity;
this.onGround = false;
if (this.simulatedJump) this.simulatedJump = false;
}
}
/**
* Prüft auf Kollisionen mit einem Voxel
*
* @param playerBox - Die Bounding Box des Spielers
* @param voxelX - X-Koordinate des Voxels
* @param voxelY - Y-Koordinate des Voxels
* @param voxelZ - Z-Koordinate des Voxels
* @returns Der Blocktyp, wenn eine Kollision vorliegt, sonst null
*/
private checkCollisionWithVoxel(playerBox: THREE.Box3, voxelX: number, voxelY: number, voxelZ: number): BlockType | null {
const blockType = this.blockManager.getVoxelBlockType(voxelX, voxelY, voxelZ);
if (blockType && blockType.solid) {
const voxelBox = new THREE.Box3(
new THREE.Vector3(voxelX * this.voxelSize, voxelY * this.voxelSize, voxelZ * this.voxelSize),
new THREE.Vector3((voxelX + 1) * this.voxelSize, (voxelY + 1) * this.voxelSize, (voxelZ + 1) * this.voxelSize)
);
if (playerBox.intersectsBox(voxelBox)) {
return blockType;
}
}
return null;
}
/**
* Behandelt vertikale Kollisionen (Boden und Decke)
*
* @param newPos - Die neue Position des Spielers
* @param playerWorldPos - Die aktuelle Position des Spielers
* @param touchedBlocksInThisFrame - Set mit Schlüsseln der Blöcke, die in diesem Frame berührt wurden
* @returns true, wenn eine Kollision stattgefunden hat
*/
private handleVerticalCollisions(newPos: THREE.Vector3, playerWorldPos: THREE.Vector3, touchedBlocksInThisFrame: Set<string>): boolean {
this.onGround = false;
const playerFeetY = newPos.y - this.height / 2;
const playerHeadY = newPos.y + this.height / 2;
const checkMinX = Math.floor((newPos.x - this.radius) / this.voxelSize);
const checkMaxX = Math.floor((newPos.x + this.radius) / this.voxelSize);
const checkMinZ = Math.floor((newPos.z - this.radius) / this.voxelSize);
const checkMaxZ = Math.floor((newPos.z + this.radius) / this.voxelSize);
// Kollision mit dem Boden prüfen
if (this.velocity.y <= 0) {
const groundCheckY = Math.floor(playerFeetY / this.voxelSize);
for (let x = checkMinX; x <= checkMaxX; x++) {
for (let z = checkMinZ; z <= checkMaxZ; z++) {
const blockAtFeet = this.blockManager.getVoxelBlockType(x, groundCheckY, z);
if (blockAtFeet) {
if (blockAtFeet.isDeadly) {
// Starte den Sterbevorgang (in Lava versinken)
if (!this.isDying) {
this.isDying = true;
this.deathTimer = 0;
this.velocity.set(0, 0, 0); // Stoppe alle Bewegungen
}
return true;
}
if (blockAtFeet.isGoal) {
// Rufe den Callback auf, aber erlaube dem Spieler, auf dem Ziel-Block zu stehen
this.onGoalReached({ x, y: groundCheckY, z });
// Setze die Spielerposition auf die Oberkante des Blocks
playerWorldPos.y = (groundCheckY + 1) * this.voxelSize + this.height / 2;
this.velocity.y = 0;
this.onGround = true;
return true;
}
if (blockAtFeet.solid) {
// Prüfe, ob wir auf einem zerbrechlichen Block stehen
if (blockAtFeet.isFragile) {
// Erzeuge einen eindeutigen Schlüssel für diesen Block
const blockKey = `${x}_${groundCheckY}_${z}`;
touchedBlocksInThisFrame.add(blockKey);
// Wenn wir diesen Block noch nicht verfolgen, füge ihn hinzu
if (!this.fragileTouchedBlocks.has(blockKey)) {
this.fragileTouchedBlocks.set(blockKey, {
x,
y: groundCheckY,
z,
timer: blockAtFeet.breakTimer || 1.0
});
}
}
// Prüfe, ob wir auf einem Trampolinblock stehen
if (blockAtFeet.isTrampoline) {
// Setze die Spielerposition auf die Oberkante des Blocks
playerWorldPos.y = (groundCheckY + 1) * this.voxelSize + this.height / 2;
// Gib dem Spieler einen kräftigen Impuls nach oben
const trampolineForce = blockAtFeet.trampolineForce || 20.0;
this.velocity.y = trampolineForce;
this.onGround = false;
return true;
}
playerWorldPos.y = (groundCheckY + 1) * this.voxelSize + this.height / 2;
this.velocity.y = 0;
this.onGround = true;
return true;
}
}
}
}
}
// Kollision mit der Decke prüfen
if (this.velocity.y > 0) {
const headCheckY = Math.floor(playerHeadY / this.voxelSize);
for (let x = checkMinX; x <= checkMaxX; x++) {
for (let z = checkMinZ; z <= checkMaxZ; z++) {
const blockAtHead = this.blockManager.getVoxelBlockType(x, headCheckY, z);
if (blockAtHead && blockAtHead.solid) {
playerWorldPos.y = headCheckY * this.voxelSize - this.height / 2 - 0.01;
this.velocity.y = 0;
return true;
}
}
}
}
playerWorldPos.y = newPos.y;
return false;
}
/**
* Behandelt horizontale Kollisionen (Wände)
*
* @param newPos - Die neue Position des Spielers
* @param playerWorldPos - Die aktuelle Position des Spielers
*/
private handleHorizontalCollisions(newPos: THREE.Vector3, playerWorldPos: THREE.Vector3): void {
const R = this.radius;
const H_half = this.height / 2;
// X-Achsen-Kollision
let proposedXPos = newPos.x;
const playerBoxX = new THREE.Box3(
new THREE.Vector3(proposedXPos - R, playerWorldPos.y - H_half, playerWorldPos.z - R),
new THREE.Vector3(proposedXPos + R, playerWorldPos.y + H_half, playerWorldPos.z + R)
);
const minX_col = Math.floor(playerBoxX.min.x / this.voxelSize);
const maxX_col = Math.floor(playerBoxX.max.x / this.voxelSize);
const minY_col = Math.floor(playerBoxX.min.y / this.voxelSize);
const maxY_col = Math.floor(playerBoxX.max.y / this.voxelSize);
const minZ_col = Math.floor(playerBoxX.min.z / this.voxelSize);
const maxZ_col = Math.floor(playerBoxX.max.z / this.voxelSize);
let collisionX = false;
for (let ix = minX_col; ix <= maxX_col; ix++) {
for (let iy = minY_col; iy <= maxY_col; iy++) {
for (let iz = minZ_col; iz <= maxZ_col; iz++) {
const blockType = this.checkCollisionWithVoxel(playerBoxX, ix, iy, iz);
if (blockType) {
if (blockType.isDeadly) {
this.onRespawn(false);
return;
}
if (blockType.isGoal) {
this.onGoalReached({ x: ix, y: iy, z: iz });
return;
}
collisionX = true;
this.velocity.x = 0;
break;
}
}
if (collisionX) break;
}
if (collisionX) break;
}
if (!collisionX) {
playerWorldPos.x = proposedXPos;
}
// Z-Achsen-Kollision
let proposedZPos = newPos.z;
const playerBoxZ = new THREE.Box3(
new THREE.Vector3(playerWorldPos.x - R, playerWorldPos.y - H_half, proposedZPos - R),
new THREE.Vector3(playerWorldPos.x + R, playerWorldPos.y + H_half, proposedZPos + R)
);
const minX_colZ = Math.floor(playerBoxZ.min.x / this.voxelSize);
const maxX_colZ = Math.floor(playerBoxZ.max.x / this.voxelSize);
const minY_colZ = Math.floor(playerBoxZ.min.y / this.voxelSize);
const maxY_colZ = Math.floor(playerBoxZ.max.y / this.voxelSize);
const minZ_colZ = Math.floor(playerBoxZ.min.z / this.voxelSize);
const maxZ_colZ = Math.floor(playerBoxZ.max.z / this.voxelSize);
let collisionZ = false;
for (let ix = minX_colZ; ix <= maxX_colZ; ix++) {
for (let iy = minY_colZ; iy <= maxY_colZ; iy++) {
for (let iz = minZ_colZ; iz <= maxZ_colZ; iz++) {
const blockType = this.checkCollisionWithVoxel(playerBoxZ, ix, iy, iz);
if (blockType) {
if (blockType.isDeadly) {
this.onRespawn(false);
return;
}
if (blockType.isGoal) {
this.onGoalReached({ x: ix, y: iy, z: iz });
return;
}
collisionZ = true;
this.velocity.z = 0;
break;
}
}
if (collisionZ) break;
}
if (collisionZ) break;
}
if (!collisionZ) {
playerWorldPos.z = proposedZPos;
}
}
/**
* Aktualisiert die Timer für zerbrechliche Blöcke und zerstört sie, wenn der Timer abläuft
* oder wenn der Spieler den Block verlassen hat
*
* @param deltaTime - Die Zeit seit dem letzten Frame in Sekunden
*/
private updateFragileBlocks(deltaTime: number): void {
// Iteriere über alle verfolgten zerbrechlichen Blöcke
for (const [blockKey, blockInfo] of this.fragileTouchedBlocks.entries()) {
// Reduziere den Timer
blockInfo.timer -= deltaTime;
// Wenn der Timer abgelaufen ist, zerstöre den Block
if (blockInfo.timer <= 0) {
// Entferne den Block aus der Welt
this.blockManager.removeVoxel(blockInfo.x, blockInfo.y, blockInfo.z, true);
// Entferne den Block aus der Map
this.fragileTouchedBlocks.delete(blockKey);
}
}
}
/**
* Aktualisiert die Liste der berührten zerbrechlichen Blöcke
* Blöcke, die nicht mehr berührt werden, werden markiert, um später zerstört zu werden
*
* @param touchedBlocksInThisFrame - Set mit Schlüsseln der Blöcke, die in diesem Frame berührt wurden
*/
private updateTouchedBlocks(touchedBlocksInThisFrame: Set<string>): void {
// Finde Blöcke, die nicht mehr berührt werden
for (const [blockKey, blockInfo] of this.fragileTouchedBlocks.entries()) {
if (!touchedBlocksInThisFrame.has(blockKey)) {
// Setze den Timer auf 0, damit der Block im nächsten Frame zerstört wird
blockInfo.timer = 0;
}
}
}
/**
* Gibt die aktuelle Geschwindigkeit des Spielers zurück
*/
public getVelocity(): THREE.Vector3 {
return this.velocity.clone();
}
/**
* Gibt die aktuelle Position des Spielers zurück
*/
public getPosition(): THREE.Vector3 {
return this.cameraObject.position.clone();
}
/**
* Setzt die Position des Spielers
*
* @param position - Die neue Position des Spielers
*/
public setPosition(position: PlayerPosition): void {
this.cameraObject.position.set(position.x, position.y, position.z);
// Setze das Flag zurück, damit der Timer erst wieder startet, wenn der Spieler sich bewegt
this.hasMovedSinceSpawn = false;
}
/**
* Setzt die Geschwindigkeit des Spielers
*
* @param velocity - Die neue Geschwindigkeit des Spielers
*/
public setVelocity(velocity: THREE.Vector3): void {
this.velocity.copy(velocity);
}
/**
* Simuliert einen Sprung des Spielers
*/
public simulateJump(): void {
this.simulatedJump = true;
}
}

View file

@ -0,0 +1,104 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
// Props
export let blockType = '';
export let blockName = '';
export let color: number = 0xffffff;
export let emissive: number | undefined = undefined;
export let opacity: number = 1;
export let selected = false;
export let topColor: number | undefined = undefined;
export let sideColor: number | undefined = undefined;
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('select', { blockType });
}
</script>
<button
type="button"
class="block-button"
class:selected={selected}
title={blockName}
on:click={handleClick}
on:keydown={(e) => e.key === 'Enter' && handleClick()}
aria-pressed={selected}
>
{#if topColor !== undefined && sideColor !== undefined}
<div class="grass-style">
<div class="grass-preview-top" style="background-color: #{topColor ? topColor.toString(16).padStart(6, '0') : '559944'};"></div>
<div class="grass-preview-bottom" style="background-color: #{sideColor ? sideColor.toString(16).padStart(6, '0') : '8B4513'};"></div>
</div>
{:else}
<div
class="color-style"
style="
background-color: #{color.toString(16).padStart(6, '0')};
opacity: {opacity};
box-shadow: {emissive ? `inset 0 0 10px #${emissive.toString(16).padStart(6, '0')}` : 'none'};
"
></div>
{/if}
</button>
<style>
.block-button {
width: 44px;
height: 44px;
border: 2px solid #718096;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease-in-out;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
position: relative;
padding: 0;
}
.block-button.selected {
border-color: #4FD1C5;
box-shadow: 0 0 12px #4FD1C5;
}
.block-button:hover {
transform: scale(1.1);
border-color: #A0AEC0;
}
.grass-style {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.grass-preview-top {
width: 100%;
height: 40%;
}
.grass-preview-bottom {
width: 100%;
height: 60%;
}
.color-style {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,102 @@
<script lang="ts">
import { onMount } from 'svelte';
import { AuthService } from '../../services/AuthService';
import AuthModal from './AuthModal.svelte';
// Eigenschaften
export let buttonClass = '';
export let showUserEmail = true;
// Zustände
let isModalOpen = false;
let isLoggedIn = false;
let currentUser: any = null;
// Beim Mounten prüfen, ob der Benutzer angemeldet ist
onMount(async () => {
await checkAuthStatus();
});
// Authentifizierungsstatus prüfen
async function checkAuthStatus() {
const user = await AuthService.getCurrentUser();
isLoggedIn = !!user;
currentUser = user;
}
// Modal öffnen
function openModal() {
isModalOpen = true;
}
// Nach erfolgreicher Anmeldung
async function handleLogin() {
await checkAuthStatus();
}
// Nach Abmeldung
async function handleLogout() {
await checkAuthStatus();
}
</script>
<div class="auth-button-container">
<button
type="button"
class="auth-button {buttonClass}"
on:click={openModal}
>
{#if isLoggedIn}
<span class="user-icon">👤</span>
{#if showUserEmail && currentUser?.email}
<span class="user-email">{currentUser.email.split('@')[0]}</span>
{:else}
<span>Profil</span>
{/if}
{:else}
<span class="login-icon">🔑</span>
<span>Anmelden</span>
{/if}
</button>
<AuthModal
bind:isOpen={isModalOpen}
on:login={handleLogin}
on:logout={handleLogout}
/>
</div>
<style>
.auth-button-container {
display: inline-block;
}
.auth-button {
display: flex;
align-items: center;
gap: 8px;
background-color: rgba(42, 50, 66, 0.9);
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.2s;
}
.auth-button:hover {
background-color: rgba(60, 70, 90, 0.9);
}
.user-icon, .login-icon {
font-size: 1.1rem;
}
.user-email {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View file

@ -0,0 +1,226 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import Login from './Login.svelte';
import Register from './Register.svelte';
import PasswordReset from './PasswordReset.svelte';
import { AuthService } from '../../services/AuthService';
export let isOpen = false;
// Event-Dispatcher für Kommunikation mit übergeordneten Komponenten
const dispatch = createEventDispatcher();
// Aktuelle Ansicht (login, register, passwordReset)
let currentView = 'login';
// Benutzer-Status
let isLoggedIn = false;
let currentUser: any = null;
// Beim Mounten prüfen, ob der Benutzer angemeldet ist
onMount(async () => {
await checkAuthStatus();
});
// Authentifizierungsstatus prüfen
async function checkAuthStatus() {
const user = await AuthService.getCurrentUser();
isLoggedIn = !!user;
currentUser = user;
}
// Ansicht wechseln (login, register, passwordReset)
function handleSwitchView(event: CustomEvent<string>) {
currentView = event.detail;
}
// Nach erfolgreicher Anmeldung
async function handleLogin() {
await checkAuthStatus();
if (isLoggedIn) {
dispatch('login', currentUser);
closeModal();
}
}
// Abmelden
async function handleLogout() {
const success = await AuthService.logout();
if (success) {
await checkAuthStatus();
dispatch('logout');
}
}
// Modal schließen
function closeModal() {
isOpen = false;
dispatch('close');
}
// Klick außerhalb des Modals abfangen
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
closeModal();
}
}
// Tastendruck abfangen (Escape zum Schließen)
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
closeModal();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<div class="modal-backdrop" on:click={handleBackdropClick} on:keydown={handleKeydown} role="dialog" aria-modal="true" tabindex="-1">
<div class="modal-content">
<button class="close-button" on:click={closeModal}>×</button>
{#if isLoggedIn}
<div class="user-profile">
<h2>Willkommen!</h2>
<p class="user-email">{currentUser?.email}</p>
<div class="profile-actions">
<button class="auth-button" on:click={handleLogout}>
Abmelden
</button>
</div>
</div>
{:else}
{#if currentView === 'login'}
<Login
on:switchView={handleSwitchView}
on:login={handleLogin}
/>
{:else if currentView === 'register'}
<Register
on:switchView={handleSwitchView}
/>
{:else if currentView === 'passwordReset'}
<PasswordReset
on:switchView={handleSwitchView}
/>
{/if}
{/if}
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(4px);
padding: 16px;
box-sizing: border-box;
}
.modal-content {
position: relative;
width: 100%;
max-width: 400px;
min-width: 280px;
max-height: 90%;
overflow-y: auto;
animation: fadeIn 0.3s ease-out;
box-sizing: border-box;
background-color: transparent;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
z-index: 10;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.3);
transition: background-color 0.2s;
}
.close-button:hover {
background-color: rgba(0, 0, 0, 0.5);
}
.user-profile {
background-color: rgba(42, 50, 66, 0.9);
border-radius: 8px;
padding: 24px;
width: 100%;
max-width: 400px;
min-width: 280px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
color: white;
text-align: center;
box-sizing: border-box;
}
.user-profile h2 {
margin-top: 0;
margin-bottom: 16px;
}
.user-email {
font-size: 1.1rem;
margin-bottom: 24px;
padding: 8px;
background-color: rgba(30, 36, 48, 0.8);
border-radius: 4px;
}
.profile-actions {
display: flex;
flex-direction: column;
gap: 12px;
}
.auth-button {
width: 100%;
padding: 12px;
background-color: #3182CE;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
box-sizing: border-box;
}
.auth-button:hover {
background-color: #2B6CB0;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -0,0 +1,260 @@
<script lang="ts">
import { AuthService } from '../../services/AuthService';
import { createEventDispatcher } from 'svelte';
// Event-Dispatcher für Kommunikation mit übergeordneten Komponenten
const dispatch = createEventDispatcher();
// Formular-Zustände
let email = '';
let password = '';
let isLoading = false;
let errorMessage = '';
let successMessage = '';
// Formular absenden
async function handleSubmit() {
if (!email || !password) {
errorMessage = 'Bitte fülle alle Felder aus.';
return;
}
try {
isLoading = true;
errorMessage = '';
const success = await AuthService.login(email, password);
if (success) {
successMessage = 'Anmeldung erfolgreich!';
dispatch('login');
// Formular zurücksetzen
email = '';
password = '';
} else {
errorMessage = 'Anmeldung fehlgeschlagen. Bitte überprüfe deine Eingaben.';
}
} catch (error) {
errorMessage = 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.';
console.error('Login error:', error);
} finally {
isLoading = false;
}
}
// Zur Registrierung wechseln
function switchToRegister() {
dispatch('switchView', 'register');
}
// Zum Passwort-Reset wechseln
function switchToPasswordReset() {
dispatch('switchView', 'passwordReset');
}
</script>
<div class="auth-form-container">
<h2>Anmelden</h2>
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
{#if successMessage}
<div class="success-message">
{successMessage}
</div>
{/if}
<form on:submit|preventDefault={handleSubmit}>
<div class="form-group">
<label for="email">E-Mail</label>
<input
type="email"
id="email"
bind:value={email}
placeholder="deine@email.de"
disabled={isLoading}
required
/>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input
type="password"
id="password"
bind:value={password}
placeholder="Dein Passwort"
disabled={isLoading}
required
/>
</div>
<button
type="submit"
class="auth-button"
disabled={isLoading}
>
{isLoading ? 'Wird angemeldet...' : 'Anmelden'}
</button>
</form>
<div class="auth-links">
<button
type="button"
class="text-button"
on:click={switchToPasswordReset}
disabled={isLoading}
>
Passwort vergessen?
</button>
<div class="register-link">
Noch kein Konto?
<button
type="button"
class="text-button"
on:click={switchToRegister}
disabled={isLoading}
>
Registrieren
</button>
</div>
</div>
</div>
<style>
.auth-form-container {
background-color: rgba(42, 50, 66, 0.9);
border-radius: 8px;
padding: 24px;
width: 100%;
max-width: 400px;
min-width: 280px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
h2 {
color: white;
margin-top: 0;
margin-bottom: 24px;
text-align: center;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 16px;
width: 100%;
box-sizing: border-box;
}
label {
display: block;
color: white;
margin-bottom: 8px;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 10px 12px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(30, 36, 48, 0.8);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
input:focus {
border-color: #3182CE;
box-shadow: 0 0 0 2px rgba(49, 130, 206, 0.3);
}
input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.auth-button {
width: 100%;
padding: 12px;
background-color: #3182CE;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 8px;
box-sizing: border-box;
}
.auth-button:hover {
background-color: #2B6CB0;
}
.auth-button:disabled {
background-color: #64748B;
cursor: not-allowed;
}
.error-message {
background-color: rgba(220, 38, 38, 0.2);
color: #FCA5A5;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.success-message {
background-color: rgba(16, 185, 129, 0.2);
color: #6EE7B7;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.auth-links {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.text-button {
background: none;
border: none;
color: #63B3ED;
cursor: pointer;
font-size: 0.9rem;
padding: 0;
text-decoration: underline;
transition: color 0.2s;
}
.text-button:hover {
color: #90CDF4;
}
.text-button:disabled {
color: #64748B;
cursor: not-allowed;
}
.register-link {
color: white;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 4px;
}
</style>

View file

@ -0,0 +1,218 @@
<script lang="ts">
import { AuthService } from '../../services/AuthService';
import { createEventDispatcher } from 'svelte';
// Event-Dispatcher für Kommunikation mit übergeordneten Komponenten
const dispatch = createEventDispatcher();
// Formular-Zustände
let email = '';
let isLoading = false;
let errorMessage = '';
let successMessage = '';
// Formular absenden
async function handleSubmit() {
if (!email) {
errorMessage = 'Bitte gib deine E-Mail-Adresse ein.';
return;
}
try {
isLoading = true;
errorMessage = '';
const success = await AuthService.resetPassword(email);
if (success) {
successMessage = 'Eine E-Mail zum Zurücksetzen deines Passworts wurde gesendet. Bitte überprüfe deinen Posteingang.';
// Formular zurücksetzen
email = '';
} else {
errorMessage = 'Fehler beim Senden der E-Mail. Bitte versuche es später erneut.';
}
} catch (error) {
errorMessage = 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.';
console.error('Password reset error:', error);
} finally {
isLoading = false;
}
}
// Zum Login wechseln
function switchToLogin() {
dispatch('switchView', 'login');
}
</script>
<div class="auth-form-container">
<h2>Passwort zurücksetzen</h2>
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
{#if successMessage}
<div class="success-message">
{successMessage}
</div>
{/if}
<form on:submit|preventDefault={handleSubmit}>
<div class="form-group">
<label for="email">E-Mail</label>
<input
type="email"
id="email"
bind:value={email}
placeholder="deine@email.de"
disabled={isLoading}
required
/>
</div>
<button
type="submit"
class="auth-button"
disabled={isLoading}
>
{isLoading ? 'Wird gesendet...' : 'Passwort zurücksetzen'}
</button>
</form>
<div class="auth-links">
<button
type="button"
class="text-button"
on:click={switchToLogin}
disabled={isLoading}
>
Zurück zum Login
</button>
</div>
</div>
<style>
.auth-form-container {
background-color: rgba(42, 50, 66, 0.9);
border-radius: 8px;
padding: 24px;
width: 100%;
max-width: 400px;
min-width: 280px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
h2 {
color: white;
margin-top: 0;
margin-bottom: 24px;
text-align: center;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
color: white;
margin-bottom: 8px;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 10px 12px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(30, 36, 48, 0.8);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
input:focus {
border-color: #3182CE;
box-shadow: 0 0 0 2px rgba(49, 130, 206, 0.3);
}
input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.auth-button {
width: 100%;
padding: 12px;
background-color: #3182CE;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 8px;
box-sizing: border-box;
}
.auth-button:hover {
background-color: #2B6CB0;
}
.auth-button:disabled {
background-color: #64748B;
cursor: not-allowed;
}
.error-message {
background-color: rgba(220, 38, 38, 0.2);
color: #FCA5A5;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.success-message {
background-color: rgba(16, 185, 129, 0.2);
color: #6EE7B7;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.auth-links {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.text-button {
background: none;
border: none;
color: #63B3ED;
cursor: pointer;
font-size: 0.9rem;
padding: 0;
text-decoration: underline;
transition: color 0.2s;
}
.text-button:hover {
color: #90CDF4;
}
.text-button:disabled {
color: #64748B;
cursor: not-allowed;
}
</style>

View file

@ -0,0 +1,273 @@
<script lang="ts">
import { AuthService } from '../../services/AuthService';
import { createEventDispatcher } from 'svelte';
// Event-Dispatcher für Kommunikation mit übergeordneten Komponenten
const dispatch = createEventDispatcher();
// Formular-Zustände
let email = '';
let password = '';
let confirmPassword = '';
let isLoading = false;
let errorMessage = '';
let successMessage = '';
// Formular absenden
async function handleSubmit() {
// Validierung
if (!email || !password || !confirmPassword) {
errorMessage = 'Bitte fülle alle Felder aus.';
return;
}
if (password !== confirmPassword) {
errorMessage = 'Die Passwörter stimmen nicht überein.';
return;
}
if (password.length < 6) {
errorMessage = 'Das Passwort muss mindestens 6 Zeichen lang sein.';
return;
}
try {
isLoading = true;
errorMessage = '';
const success = await AuthService.register(email, password);
if (success) {
successMessage = 'Registrierung erfolgreich! Bitte überprüfe deine E-Mails, um dein Konto zu bestätigen.';
// Formular zurücksetzen
email = '';
password = '';
confirmPassword = '';
// Nach kurzer Verzögerung zum Login wechseln
setTimeout(() => {
dispatch('switchView', 'login');
}, 3000);
} else {
errorMessage = 'Registrierung fehlgeschlagen. Bitte versuche es mit einer anderen E-Mail-Adresse.';
}
} catch (error) {
errorMessage = 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.';
console.error('Registration error:', error);
} finally {
isLoading = false;
}
}
// Zum Login wechseln
function switchToLogin() {
dispatch('switchView', 'login');
}
</script>
<div class="auth-form-container">
<h2>Registrieren</h2>
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
{#if successMessage}
<div class="success-message">
{successMessage}
</div>
{/if}
<form on:submit|preventDefault={handleSubmit}>
<div class="form-group">
<label for="email">E-Mail</label>
<input
type="email"
id="email"
bind:value={email}
placeholder="deine@email.de"
disabled={isLoading}
required
/>
</div>
<div class="form-group">
<label for="password">Passwort</label>
<input
type="password"
id="password"
bind:value={password}
placeholder="Mindestens 6 Zeichen"
disabled={isLoading}
required
/>
</div>
<div class="form-group">
<label for="confirmPassword">Passwort bestätigen</label>
<input
type="password"
id="confirmPassword"
bind:value={confirmPassword}
placeholder="Passwort wiederholen"
disabled={isLoading}
required
/>
</div>
<button
type="submit"
class="auth-button"
disabled={isLoading}
>
{isLoading ? 'Wird registriert...' : 'Registrieren'}
</button>
</form>
<div class="auth-links">
<div class="login-link">
Bereits ein Konto?
<button
type="button"
class="text-button"
on:click={switchToLogin}
disabled={isLoading}
>
Anmelden
</button>
</div>
</div>
</div>
<style>
.auth-form-container {
background-color: rgba(42, 50, 66, 0.9);
border-radius: 8px;
padding: 24px;
width: 100%;
max-width: 400px;
min-width: 280px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
box-sizing: border-box;
}
h2 {
color: white;
margin-top: 0;
margin-bottom: 24px;
text-align: center;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
color: white;
margin-bottom: 8px;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 10px 12px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(30, 36, 48, 0.8);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
input:focus {
border-color: #3182CE;
box-shadow: 0 0 0 2px rgba(49, 130, 206, 0.3);
}
input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.auth-button {
width: 100%;
padding: 12px;
background-color: #3182CE;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
box-sizing: border-box;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 8px;
}
.auth-button:hover {
background-color: #2B6CB0;
}
.auth-button:disabled {
background-color: #64748B;
cursor: not-allowed;
}
.error-message {
background-color: rgba(220, 38, 38, 0.2);
color: #FCA5A5;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.success-message {
background-color: rgba(16, 185, 129, 0.2);
color: #6EE7B7;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.auth-links {
margin-top: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.text-button {
background: none;
border: none;
color: #63B3ED;
cursor: pointer;
font-size: 0.9rem;
padding: 0;
text-decoration: underline;
transition: color 0.2s;
}
.text-button:hover {
color: #90CDF4;
}
.text-button:disabled {
color: #64748B;
cursor: not-allowed;
}
.login-link {
color: white;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 4px;
}
</style>

View file

@ -0,0 +1,516 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { AuthService } from '../../services/AuthService';
import { LevelService } from '../../services/LevelService';
import type { LevelMetadata } from '../../types/level.types';
// Event-Dispatcher für Kommunikation mit übergeordneten Komponenten
const dispatch = createEventDispatcher();
// Benutzer-Daten
let user: any = null;
let userLevels: LevelMetadata[] = [];
let isLoading = true;
let errorMessage = '';
// Passwort-Änderung
let showPasswordChange = false;
let newPassword = '';
let confirmNewPassword = '';
let passwordChangeError = '';
let passwordChangeSuccess = '';
let isPasswordChanging = false;
// Beim Mounten Benutzerdaten laden
onMount(async () => {
await loadUserData();
});
// Benutzerdaten laden
async function loadUserData() {
try {
isLoading = true;
errorMessage = '';
// Aktuellen Benutzer abrufen
user = await AuthService.getCurrentUser();
if (user) {
// Levels des Benutzers laden
userLevels = await LevelService.getUserLevels();
} else {
errorMessage = 'Du bist nicht angemeldet.';
}
} catch (error) {
console.error('Error loading user data:', error);
errorMessage = 'Fehler beim Laden der Benutzerdaten.';
} finally {
isLoading = false;
}
}
// Passwort ändern
async function handlePasswordChange() {
// Validierung
if (!newPassword || !confirmNewPassword) {
passwordChangeError = 'Bitte fülle alle Felder aus.';
return;
}
if (newPassword !== confirmNewPassword) {
passwordChangeError = 'Die Passwörter stimmen nicht überein.';
return;
}
if (newPassword.length < 6) {
passwordChangeError = 'Das Passwort muss mindestens 6 Zeichen lang sein.';
return;
}
try {
isPasswordChanging = true;
passwordChangeError = '';
const success = await AuthService.updatePassword(newPassword);
if (success) {
passwordChangeSuccess = 'Passwort erfolgreich geändert.';
newPassword = '';
confirmNewPassword = '';
// Nach kurzer Verzögerung Passwort-Änderung ausblenden
setTimeout(() => {
showPasswordChange = false;
passwordChangeSuccess = '';
}, 3000);
} else {
passwordChangeError = 'Fehler beim Ändern des Passworts.';
}
} catch (error) {
console.error('Password change error:', error);
passwordChangeError = 'Ein Fehler ist aufgetreten. Bitte versuche es später erneut.';
} finally {
isPasswordChanging = false;
}
}
// Abmelden
async function handleLogout() {
const success = await AuthService.logout();
if (success) {
dispatch('logout');
}
}
// Level bearbeiten
function editLevel(levelId: string) {
dispatch('editLevel', levelId);
}
// Level löschen
async function deleteLevel(levelId: string) {
if (confirm('Möchtest du dieses Level wirklich löschen?')) {
const success = await LevelService.deleteLevel(levelId);
if (success) {
// Levels neu laden
userLevels = await LevelService.getUserLevels();
}
}
}
// Level öffentlich/privat umschalten
function toggleLevelVisibility(level: LevelMetadata) {
// Hier würde die Logik zum Umschalten der Sichtbarkeit implementiert werden
// Da wir keinen direkten Zugriff auf die Funktion haben, müsste diese im LevelService ergänzt werden
console.log('Toggle visibility for level:', level.id);
}
</script>
<div class="profile-container">
<h2>Dein Profil</h2>
{#if errorMessage}
<div class="error-message">
{errorMessage}
</div>
{/if}
{#if isLoading}
<div class="loading">Daten werden geladen...</div>
{:else if user}
<div class="user-info">
<div class="user-email">
<span class="label">E-Mail:</span>
<span class="value">{user.email}</span>
</div>
<div class="account-actions">
<button
type="button"
class="action-button secondary"
on:click={() => showPasswordChange = !showPasswordChange}
>
{showPasswordChange ? 'Abbrechen' : 'Passwort ändern'}
</button>
<button
type="button"
class="action-button"
on:click={handleLogout}
>
Abmelden
</button>
</div>
{#if showPasswordChange}
<div class="password-change-form">
<h3>Passwort ändern</h3>
{#if passwordChangeError}
<div class="error-message">
{passwordChangeError}
</div>
{/if}
{#if passwordChangeSuccess}
<div class="success-message">
{passwordChangeSuccess}
</div>
{/if}
<div class="form-group">
<label for="newPassword">Neues Passwort</label>
<input
type="password"
id="newPassword"
bind:value={newPassword}
placeholder="Mindestens 6 Zeichen"
disabled={isPasswordChanging}
/>
</div>
<div class="form-group">
<label for="confirmNewPassword">Passwort bestätigen</label>
<input
type="password"
id="confirmNewPassword"
bind:value={confirmNewPassword}
placeholder="Passwort wiederholen"
disabled={isPasswordChanging}
/>
</div>
<button
type="button"
class="action-button"
on:click={handlePasswordChange}
disabled={isPasswordChanging}
>
{isPasswordChanging ? 'Wird geändert...' : 'Passwort ändern'}
</button>
</div>
{/if}
</div>
<div class="user-levels">
<h3>Deine Levels</h3>
{#if userLevels.length === 0}
<div class="no-levels">
Du hast noch keine Levels erstellt.
</div>
{:else}
<div class="levels-list">
{#each userLevels as level}
<div class="level-card">
<div class="level-info">
<h4 class="level-name">{level.name}</h4>
<p class="level-description">{level.description || 'Keine Beschreibung'}</p>
<div class="level-stats">
<span class="stat">
<i class="icon">👁️</i> {level.playCount}
</span>
<span class="stat">
<i class="icon">❤️</i> {level.likesCount}
</span>
<span class="stat">
<i class="icon">🏷️</i> {level.difficulty || 'Normal'}
</span>
</div>
</div>
<div class="level-actions">
<button
type="button"
class="icon-button"
on:click={() => editLevel(level.id)}
title="Level bearbeiten"
>
✏️
</button>
<button
type="button"
class="icon-button"
on:click={() => toggleLevelVisibility(level)}
title={level.isPublic ? 'Auf privat setzen' : 'Öffentlich machen'}
>
{level.isPublic ? '🔒' : '🌐'}
</button>
<button
type="button"
class="icon-button delete"
on:click={() => deleteLevel(level.id)}
title="Level löschen"
>
🗑️
</button>
</div>
</div>
{/each}
</div>
{/if}
</div>
{:else}
<div class="not-logged-in">
Du bist nicht angemeldet. Bitte melde dich an, um dein Profil zu sehen.
</div>
{/if}
</div>
<style>
.profile-container {
background-color: rgba(42, 50, 66, 0.9);
border-radius: 8px;
padding: 24px;
width: 100%;
max-width: 800px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
color: white;
}
h2 {
margin-top: 0;
margin-bottom: 24px;
text-align: center;
font-size: 1.5rem;
}
h3 {
margin-top: 24px;
margin-bottom: 16px;
font-size: 1.2rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 8px;
}
.user-info {
background-color: rgba(30, 36, 48, 0.8);
padding: 16px;
border-radius: 6px;
margin-bottom: 24px;
}
.user-email {
margin-bottom: 16px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.label {
font-weight: bold;
color: rgba(255, 255, 255, 0.7);
}
.value {
font-size: 1.1rem;
}
.account-actions {
display: flex;
gap: 12px;
margin-top: 16px;
}
.action-button {
padding: 10px 16px;
background-color: #3182CE;
color: white;
border: none;
border-radius: 4px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.2s;
}
.action-button:hover {
background-color: #2B6CB0;
}
.action-button:disabled {
background-color: #64748B;
cursor: not-allowed;
}
.action-button.secondary {
background-color: #4A5568;
}
.action-button.secondary:hover {
background-color: #2D3748;
}
.password-change-form {
margin-top: 24px;
padding: 16px;
background-color: rgba(49, 130, 206, 0.1);
border-radius: 6px;
border-left: 3px solid #3182CE;
}
.password-change-form h3 {
margin-top: 0;
margin-bottom: 16px;
border-bottom: none;
padding-bottom: 0;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 0.9rem;
}
input {
width: 100%;
padding: 10px 12px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(30, 36, 48, 0.8);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
input:focus {
border-color: #3182CE;
box-shadow: 0 0 0 2px rgba(49, 130, 206, 0.3);
}
input::placeholder {
color: rgba(255, 255, 255, 0.4);
}
.error-message {
background-color: rgba(220, 38, 38, 0.2);
color: #FCA5A5;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.success-message {
background-color: rgba(16, 185, 129, 0.2);
color: #6EE7B7;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.loading, .no-levels, .not-logged-in {
padding: 16px;
text-align: center;
background-color: rgba(30, 36, 48, 0.8);
border-radius: 6px;
margin-bottom: 16px;
}
.levels-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 16px;
}
.level-card {
background-color: rgba(30, 36, 48, 0.8);
border-radius: 6px;
padding: 16px;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: transform 0.2s, box-shadow 0.2s;
}
.level-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.level-name {
margin-top: 0;
margin-bottom: 8px;
font-size: 1.1rem;
}
.level-description {
color: rgba(255, 255, 255, 0.7);
margin-bottom: 12px;
font-size: 0.9rem;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.level-stats {
display: flex;
gap: 12px;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.7);
}
.stat {
display: flex;
align-items: center;
gap: 4px;
}
.icon {
font-style: normal;
}
.level-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.icon-button {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 4px;
border-radius: 4px;
transition: background-color 0.2s;
}
.icon-button:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.icon-button.delete:hover {
background-color: rgba(220, 38, 38, 0.2);
}
</style>

View file

@ -0,0 +1,100 @@
<script lang="ts">
// Props für den Button
export let icon: string; // Icon-Name (z.B. 'play', 'info')
export let onClick: () => void; // Callback-Funktion für Klick-Event
export let color: string = '#4A5568'; // Standardfarbe
export let size: number = 50; // Größe in Pixeln
export let tooltip: string = ''; // Tooltip-Text (optional)
export let visible: boolean = true; // Sichtbarkeit des Buttons
</script>
<style>
.circle-button {
width: var(--size);
height: var(--size);
border-radius: 50%;
background-color: var(--color);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
transition: transform 0.2s, background-color 0.2s;
position: relative;
border: none;
outline: none;
}
.circle-button:hover {
transform: translateY(-2px);
filter: brightness(1.1);
}
.circle-button:active {
transform: translateY(0);
}
.icon {
width: 50%;
height: 50%;
color: white;
fill: currentColor;
}
.tooltip {
position: absolute;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.2s;
z-index: 1000;
}
.circle-button:hover .tooltip {
opacity: 1;
}
/* Tooltip-Positionen je nach Icon */
.tooltip.play {
bottom: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
}
.tooltip.info {
top: calc(100% + 10px);
right: 0;
}
</style>
{#if visible}
<button
class="circle-button"
on:click={onClick}
style="--size: {size}px; --color: {color};"
aria-label={tooltip}
>
{#if icon === 'play'}
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M8 5v14l11-7z" />
</svg>
{:else if icon === 'stop'}
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6 6h12v12H6z" />
</svg>
{:else if icon === 'info'}
<svg class="icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z" />
</svg>
{/if}
{#if tooltip}
<span class="tooltip {icon}">{tooltip}</span>
{/if}
</button>
{/if}

View file

@ -0,0 +1,452 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { LevelService } from '../../services/LevelService';
import { AuthService } from '../../services/AuthService';
// Event-Dispatcher für Kommunikation mit übergeordneten Komponenten
const dispatch = createEventDispatcher();
// Props
export let isOpen = false;
export let blocks: { x: number, y: number, z: number, type: string, isSpawnPoint: boolean, isGoal: boolean }[] = [];
export let spawnPoint: { x: number, y: number, z: number } | null = null;
export let worldSize: { width: number, height: number, depth: number } = { width: 20, height: 10, depth: 20 };
export let currentLevelId: string | null = null;
export let currentLevelName = "Neues Level";
// Formular-Zustände
let levelName = currentLevelName;
let levelDescription = "";
let isPublic = false;
let difficulty = "normal";
let tags = "";
let isLoading = false;
let errorMessage = "";
let successMessage = "";
let isLoggedIn = false;
// Beim Öffnen des Modals die Formularfelder aktualisieren
$: if (isOpen) {
levelName = currentLevelName;
checkAuthStatus();
}
// Prüfen, ob der Benutzer angemeldet ist
async function checkAuthStatus() {
const user = await AuthService.getCurrentUser();
isLoggedIn = !!user;
if (!isLoggedIn) {
errorMessage = "Du musst angemeldet sein, um ein Level zu speichern.";
} else {
errorMessage = "";
}
}
// Formular absenden
async function handleSubmit() {
if (!levelName.trim()) {
errorMessage = "Bitte gib einen Namen für das Level ein.";
return;
}
if (!isLoggedIn) {
errorMessage = "Du musst angemeldet sein, um ein Level zu speichern.";
return;
}
if (!spawnPoint) {
errorMessage = "Das Level muss einen Spawn-Punkt haben.";
return;
}
try {
isLoading = true;
errorMessage = "";
// Die Blöcke sind bereits im richtigen Format, da sie vom BlockManager.getSerializableBlocks() kommen
console.log('Blöcke zum Speichern:', blocks);
console.log('Anzahl der Blöcke:', blocks.length);
// Detaillierte Debugging-Ausgaben
if (blocks.length > 0) {
console.log('Erster Block:', blocks[0]);
} else {
console.warn('Keine Blöcke zum Speichern gefunden!');
}
if (blocks.length === 0) {
errorMessage = "Keine Blöcke gefunden. Das Level kann nicht gespeichert werden.";
isLoading = false;
return;
}
// Level-Objekt erstellen
const level = {
id: currentLevelId,
name: levelName.trim(),
description: levelDescription.trim(),
blocks: blocks,
spawnPoint: spawnPoint,
worldSize: worldSize,
isPublic: isPublic,
difficulty: difficulty,
tags: tags.split(',').map(tag => tag.trim()).filter(tag => tag)
};
// Level speichern
// @ts-ignore - Typprobleme ignorieren, da wir die Struktur kennen
const levelId = await LevelService.saveLevel(level);
if (levelId) {
successMessage = `Level "${levelName}" erfolgreich gespeichert!`;
currentLevelName = levelName;
currentLevelId = levelId;
// Event auslösen
dispatch('save', { levelId, levelName });
// Modal nach kurzer Verzögerung schließen
setTimeout(() => {
closeModal();
}, 1500);
} else {
errorMessage = "Fehler beim Speichern des Levels.";
}
} catch (error) {
console.error('Error saving level:', error);
errorMessage = "Ein Fehler ist aufgetreten. Bitte versuche es später erneut.";
} finally {
isLoading = false;
}
}
// Modal schließen
function closeModal() {
isOpen = false;
errorMessage = "";
successMessage = "";
dispatch('close');
}
// Klick außerhalb des Modals abfangen
function handleBackdropClick(event: MouseEvent) {
if (event.target === event.currentTarget) {
closeModal();
}
}
// Tastendruck abfangen (Escape zum Schließen)
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') {
closeModal();
}
}
</script>
<svelte:window on:keydown={handleKeydown} />
{#if isOpen}
<div
class="modal-backdrop"
on:click={handleBackdropClick}
on:keydown={(e) => e.key === 'Enter' && handleSubmit()}
role="dialog"
aria-modal="true"
aria-labelledby="save-level-title"
tabindex="-1"
>
<div class="modal-content">
<button
class="close-button"
on:click={closeModal}
aria-label="Schließen"
>
×
</button>
<h2 id="save-level-title">Level speichern</h2>
{#if errorMessage}
<div class="error-message">
{errorMessage}
{#if !isLoggedIn}
<div class="login-prompt">
<p>Bitte melde dich an, um dein Level zu speichern.</p>
</div>
{/if}
</div>
{/if}
{#if successMessage}
<div class="success-message">
{successMessage}
</div>
{/if}
<form on:submit|preventDefault={handleSubmit}>
<div class="form-group">
<label for="levelName">Level-Name *</label>
<input
type="text"
id="levelName"
bind:value={levelName}
placeholder="Gib deinem Level einen Namen"
disabled={isLoading || !isLoggedIn}
required
/>
</div>
<div class="form-group">
<label for="levelDescription">Beschreibung</label>
<textarea
id="levelDescription"
bind:value={levelDescription}
placeholder="Beschreibe dein Level (optional)"
rows="3"
disabled={isLoading || !isLoggedIn}
></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="difficulty">Schwierigkeit</label>
<select
id="difficulty"
bind:value={difficulty}
disabled={isLoading || !isLoggedIn}
>
<option value="easy">Leicht</option>
<option value="normal">Normal</option>
<option value="hard">Schwer</option>
<option value="expert">Experte</option>
</select>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input
type="checkbox"
bind:checked={isPublic}
disabled={isLoading || !isLoggedIn}
/>
<span>Öffentlich teilen</span>
</label>
</div>
</div>
<div class="form-group">
<label for="tags">Tags</label>
<input
type="text"
id="tags"
bind:value={tags}
placeholder="z.B. parkour, puzzle, speedrun (durch Kommas getrennt)"
disabled={isLoading || !isLoggedIn}
/>
</div>
<button
type="submit"
class="save-button"
disabled={isLoading || !isLoggedIn}
>
{isLoading ? 'Wird gespeichert...' : 'Level speichern'}
</button>
</form>
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background-color: rgba(42, 50, 66, 0.95);
border-radius: 8px;
padding: 24px;
width: 100%;
max-width: 500px;
min-width: 280px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
color: white;
animation: fadeIn 0.3s ease-out;
box-sizing: border-box;
}
.close-button {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: white;
font-size: 1.5rem;
cursor: pointer;
z-index: 10;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.3);
transition: background-color 0.2s;
}
.close-button:hover {
background-color: rgba(0, 0, 0, 0.5);
}
h2 {
margin-top: 0;
margin-bottom: 24px;
text-align: center;
font-size: 1.5rem;
}
.form-group {
margin-bottom: 16px;
}
.form-row {
display: flex;
flex-direction: row;
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 480px) {
.form-row {
flex-direction: column;
gap: 8px;
}
}
.form-row .form-group {
flex: 1;
margin-bottom: 0;
}
label {
display: block;
margin-bottom: 8px;
font-size: 0.9rem;
}
input, textarea, select {
width: 100%;
padding: 10px 12px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(30, 36, 48, 0.8);
color: white;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
box-sizing: border-box;
}
input:focus, textarea:focus, select:focus {
border-color: #3182CE;
box-shadow: 0 0 0 2px rgba(49, 130, 206, 0.3);
}
input::placeholder, textarea::placeholder {
color: rgba(255, 255, 255, 0.4);
}
textarea {
resize: vertical;
min-height: 80px;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.checkbox-label input {
width: auto;
margin-right: 8px;
}
.save-button {
width: 100%;
padding: 12px;
background-color: #3182CE;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.2s;
margin-top: 8px;
}
.save-button:hover {
background-color: #2B6CB0;
}
.save-button:disabled {
background-color: #64748B;
cursor: not-allowed;
}
.error-message {
background-color: rgba(220, 38, 38, 0.2);
color: #FCA5A5;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.success-message {
background-color: rgba(16, 185, 129, 0.2);
color: #6EE7B7;
padding: 10px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 0.9rem;
}
.login-prompt {
margin-top: 8px;
font-style: italic;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -0,0 +1,9 @@
// Beispielhafte Leveldaten (können auch in JSON ausgelagert werden)
export const LEVELS = [
{
id: 1,
name: 'Tutorial',
data: [/* ... */]
}
];

View file

@ -0,0 +1,12 @@
// Physik- und Kollisionslogik für das Voxel-Spiel
/**
* Überprüft Kollisionen zwischen dem Spieler und Voxeln
* @param {Object} player - Das Spielerobjekt mit Position und Größe
* @param {Object} voxels - Die Voxel-Daten der Spielwelt
* @returns {boolean} - True wenn eine Kollision erkannt wurde
*/
export function checkCollision(player, voxels) {
// ...
return false; // Standardmäßig keine Kollision zurückgeben
}

View file

@ -0,0 +1,13 @@
// Spielersteuerung (Keyboard, Maus, Touch)
// Hier werden später die Steuerungsfunktionen implementiert.
/**
* Richtet die Steuerung für den Spieler ein
* @param {HTMLCanvasElement} canvas - Das Canvas-Element für die Spielanzeige
* @param {Object} player - Das Spielerobjekt, das gesteuert werden soll
* @returns {Object} - Steuerungsobjekt mit Methoden zum Aktualisieren und Entfernen der Steuerung
*/
export function setupPlayerControls(canvas, player) {
// ...
return {}; // Steuerungsobjekt zurückgeben
}

View file

@ -0,0 +1,15 @@
// Hilfsfunktionen für Voxel-Logik
// Hier kannst du z.B. Methoden für Blockplatzierung, Nachbarschaftsprüfung etc. einfügen.
/**
* Platziert einen Voxel an der angegebenen Position
* @param {number} x - X-Koordinate des Voxels
* @param {number} y - Y-Koordinate des Voxels
* @param {number} z - Z-Koordinate des Voxels
* @param {string} type - Typ des Voxels (z.B. 'grass', 'stone', etc.)
* @returns {Object} - Das platzierte Voxel-Objekt
*/
export function placeVoxel(x, y, z, type) {
// ...
return { x, y, z, type }; // Voxel-Objekt zurückgeben
}

View file

@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.

View file

@ -0,0 +1,14 @@
import PocketBase from 'pocketbase';
// PocketBase Instanz mit deiner Domain
export const pb = new PocketBase('https://pb.voxelava.com');
// Auto-refresh für Auth Token
pb.authStore.onChange(() => {
// Token wird automatisch erneuert
});
// Optional: SSR Support für SvelteKit
export function createPocketBase() {
return new PocketBase('https://pb.voxelava.com');
}

View file

@ -0,0 +1,166 @@
import { pb } from '../pocketbase';
/**
* Service zur Verwaltung der Benutzerauthentifizierung mit PocketBase
*/
export class AuthService {
/**
* Registriert einen neuen Benutzer
* @param email E-Mail-Adresse des Benutzers
* @param password Passwort des Benutzers
* @param name Name des Benutzers (optional)
* @returns true, wenn die Registrierung erfolgreich war, sonst false
*/
static async register(email: string, password: string, name?: string): Promise<boolean> {
try {
const data = {
email,
password,
passwordConfirm: password,
name: name || email.split('@')[0],
emailVisibility: true
};
const record = await pb.collection('users').create(data);
// Automatisch anmelden nach erfolgreicher Registrierung
await pb.collection('users').authWithPassword(email, password);
return true;
} catch (error) {
console.error('Fehler bei der Registrierung:', error);
return false;
}
}
/**
* Meldet einen Benutzer an
* @param email E-Mail-Adresse des Benutzers
* @param password Passwort des Benutzers
* @returns true, wenn die Anmeldung erfolgreich war, sonst false
*/
static async login(email: string, password: string): Promise<boolean> {
try {
const authData = await pb.collection('users').authWithPassword(email, password);
return !!authData.token;
} catch (error) {
console.error('Fehler bei der Anmeldung:', error);
return false;
}
}
/**
* Meldet den aktuellen Benutzer ab
* @returns true, wenn die Abmeldung erfolgreich war, sonst false
*/
static async logout(): Promise<boolean> {
try {
pb.authStore.clear();
return true;
} catch (error) {
console.error('Fehler bei der Abmeldung:', error);
return false;
}
}
/**
* Prüft, ob ein Benutzer angemeldet ist
* @returns Der angemeldete Benutzer oder null, wenn kein Benutzer angemeldet ist
*/
static getCurrentUser() {
try {
if (pb.authStore.isValid) {
return pb.authStore.model;
}
return null;
} catch (error) {
console.error('Fehler beim Abrufen des aktuellen Benutzers:', error);
return null;
}
}
/**
* Sendet eine E-Mail zum Zurücksetzen des Passworts
* @param email E-Mail-Adresse des Benutzers
* @returns true, wenn die E-Mail erfolgreich gesendet wurde, sonst false
*/
static async resetPassword(email: string): Promise<boolean> {
try {
await pb.collection('users').requestPasswordReset(email);
return true;
} catch (error) {
console.error('Fehler beim Zurücksetzen des Passworts:', error);
return false;
}
}
/**
* Aktualisiert das Passwort des aktuellen Benutzers
* @param newPassword Das neue Passwort
* @returns true, wenn das Passwort erfolgreich aktualisiert wurde, sonst false
*/
static async updatePassword(newPassword: string): Promise<boolean> {
try {
const user = pb.authStore.model;
if (!user) {
throw new Error('Kein Benutzer angemeldet');
}
await pb.collection('users').update(user.id, {
password: newPassword,
passwordConfirm: newPassword
});
return true;
} catch (error) {
console.error('Fehler beim Aktualisieren des Passworts:', error);
return false;
}
}
/**
* Aktualisiert automatisch das Auth-Token
* @returns true, wenn das Token erfolgreich aktualisiert wurde, sonst false
*/
static async refreshAuth(): Promise<boolean> {
try {
if (pb.authStore.isValid) {
await pb.collection('users').authRefresh();
return true;
}
return false;
} catch (error) {
console.error('Fehler beim Aktualisieren des Auth-Tokens:', error);
return false;
}
}
/**
* Überprüft, ob die aktuelle Sitzung gültig ist
* @returns true, wenn die Sitzung gültig ist, sonst false
*/
static isAuthenticated(): boolean {
return pb.authStore.isValid;
}
/**
* Gibt die User-ID des aktuell angemeldeten Benutzers zurück
* @returns Die User-ID oder null
*/
static getUserId(): string | null {
const user = pb.authStore.model;
return user?.id || null;
}
/**
* Gibt die E-Mail des aktuell angemeldeten Benutzers zurück
* @returns Die E-Mail oder null
*/
static getUserEmail(): string | null {
const user = pb.authStore.model;
return user?.email || null;
}
}
// Default export für Kompatibilität
export default AuthService;

View file

@ -0,0 +1,498 @@
import { pb } from '../pocketbase';
// Typdefinitionen
interface Block {
x: number;
y: number;
z: number;
type: string;
isSpawnPoint?: boolean;
isGoal?: boolean;
}
interface WorldSize {
width: number;
height: number;
depth: number;
}
interface SpawnPoint {
x: number;
y: number;
z: number;
}
interface LevelMetadata {
id: string;
name: string;
description: string;
user_id: string | null;
created: string | null;
updated: string | null;
is_public?: boolean | null;
play_count: number;
likes_count: number;
difficulty?: string | undefined;
tags?: string[];
thumbnail_url?: string | undefined;
}
interface Level extends Partial<LevelMetadata> {
id?: string;
name: string;
blocks: Block[];
spawnPoint: SpawnPoint | null;
worldSize: WorldSize;
}
/**
* Service zur Verwaltung von Levels in PocketBase
*/
export class LevelService {
/**
* Speichert ein Level in der Datenbank
* @param level Das zu speichernde Level
* @returns Die ID des gespeicherten Levels
*/
static async saveLevel(level: Level): Promise<string | null> {
try {
// Prüfen, ob der Benutzer angemeldet ist
const user = pb.authStore.model;
if (!user) {
throw new Error('Du musst angemeldet sein, um ein Level zu speichern');
}
// Level-Daten für die Datenbank vorbereiten
const levelData = {
name: level.name,
description: level.description || '',
user_id: user.id,
voxel_data: this.convertBlocksToVoxelData(level.blocks),
spawn_point: level.spawnPoint,
world_size: level.worldSize,
is_public: level.is_public || false,
difficulty: level.difficulty || null,
tags: level.tags || [],
play_count: level.play_count || 0,
likes_count: level.likes_count || 0,
thumbnail_url: level.thumbnail_url || null
};
// Prüfen, ob das Level bereits existiert
if (level.id) {
// Level aktualisieren
const record = await pb.collection('levels').update(level.id, levelData);
return record.id;
} else {
// Neues Level erstellen
const record = await pb.collection('levels').create(levelData);
return record.id;
}
} catch (error) {
console.error('Fehler beim Speichern des Levels:', error);
return null;
}
}
/**
* Lädt ein Level aus der Datenbank
* @param levelId Die ID des zu ladenden Levels
* @returns Das geladene Level oder null, wenn es nicht gefunden wurde
*/
static async loadLevel(levelId: string): Promise<Level | null> {
try {
const record = await pb.collection('levels').getOne(levelId);
if (!record) return null;
// Level-Daten konvertieren
return {
id: record.id,
name: record.name,
description: record.description || '',
blocks: this.convertVoxelDataToBlocks(record.voxel_data),
spawnPoint: record.spawn_point,
worldSize: record.world_size,
is_public: record.is_public || false,
created: record.created,
updated: record.updated,
user_id: record.user_id,
play_count: record.play_count || 0,
likes_count: record.likes_count || 0,
difficulty: record.difficulty || undefined,
tags: record.tags || [],
thumbnail_url: record.thumbnail_url || undefined,
};
} catch (error) {
console.error('Fehler beim Laden des Levels:', error);
return null;
}
}
/**
* Lädt alle öffentlichen Levels
* @param page Seitennummer (startet bei 1)
* @param perPage Anzahl der Einträge pro Seite
* @returns Liste der Level-Metadaten
*/
static async getPublicLevels(page = 1, perPage = 20): Promise<LevelMetadata[]> {
try {
const records = await pb.collection('levels').getList(page, perPage, {
filter: 'is_public = true',
sort: '-created',
});
return records.items.map(record => ({
id: record.id,
name: record.name,
description: record.description || '',
user_id: record.user_id,
created: record.created,
updated: record.updated,
play_count: record.play_count || 0,
likes_count: record.likes_count || 0,
difficulty: record.difficulty || undefined,
tags: record.tags || [],
thumbnail_url: record.thumbnail_url || undefined,
}));
} catch (error) {
console.error('Fehler beim Laden der öffentlichen Levels:', error);
return [];
}
}
/**
* Lädt alle Levels des aktuellen Benutzers
* @returns Liste der Level-Metadaten
*/
static async getUserLevels(): Promise<LevelMetadata[]> {
try {
// Prüfen, ob der Benutzer angemeldet ist
const user = pb.authStore.model;
if (!user) {
throw new Error('Du musst angemeldet sein, um deine Levels zu sehen');
}
const records = await pb.collection('levels').getFullList({
filter: `user_id = "${user.id}"`,
sort: '-updated',
});
return records.map(record => ({
id: record.id,
name: record.name,
description: record.description || '',
user_id: user.id,
created: record.created,
updated: record.updated,
is_public: record.is_public,
play_count: record.play_count || 0,
likes_count: record.likes_count || 0,
difficulty: record.difficulty || undefined,
tags: record.tags || [],
thumbnail_url: record.thumbnail_url || undefined,
}));
} catch (error) {
console.error('Fehler beim Laden der Benutzer-Levels:', error);
return [];
}
}
/**
* Löscht ein Level aus der Datenbank
* @param levelId Die ID des zu löschenden Levels
* @returns true, wenn das Level erfolgreich gelöscht wurde, sonst false
*/
static async deleteLevel(levelId: string): Promise<boolean> {
try {
// Prüfen, ob der Benutzer angemeldet ist
const user = pb.authStore.model;
if (!user) {
throw new Error('Du musst angemeldet sein, um ein Level zu löschen');
}
// Erst prüfen, ob das Level dem User gehört
const level = await pb.collection('levels').getOne(levelId);
if (level.user_id !== user.id) {
throw new Error('Du kannst nur deine eigenen Levels löschen');
}
await pb.collection('levels').delete(levelId);
return true;
} catch (error) {
console.error('Fehler beim Löschen des Levels:', error);
return false;
}
}
/**
* Setzt einen "Like" für ein Level
* @param levelId Die ID des Levels
* @returns true, wenn der Like erfolgreich gesetzt wurde, sonst false
*/
static async likeLevel(levelId: string): Promise<boolean> {
try {
// Prüfen, ob der Benutzer angemeldet ist
const user = pb.authStore.model;
if (!user) {
throw new Error('Du musst angemeldet sein, um ein Level zu liken');
}
// Prüfen, ob der Benutzer das Level bereits geliked hat
const existingLikes = await pb.collection('level_likes').getList(1, 1, {
filter: `level_id = "${levelId}" && user_id = "${user.id}"`
});
if (existingLikes.items.length > 0) {
// Like entfernen
await pb.collection('level_likes').delete(existingLikes.items[0].id);
// Likes-Count im Level aktualisieren
const level = await pb.collection('levels').getOne(levelId);
await pb.collection('levels').update(levelId, {
likes_count: Math.max(0, (level.likes_count || 0) - 1)
});
return false; // Like wurde entfernt
} else {
// Like hinzufügen
await pb.collection('level_likes').create({
level_id: levelId,
user_id: user.id
});
// Likes-Count im Level aktualisieren
const level = await pb.collection('levels').getOne(levelId);
await pb.collection('levels').update(levelId, {
likes_count: (level.likes_count || 0) + 1
});
return true; // Like wurde hinzugefügt
}
} catch (error) {
console.error('Fehler beim Liken des Levels:', error);
return false;
}
}
/**
* Prüft, ob der aktuelle Benutzer ein Level geliked hat
* @param levelId Die ID des Levels
* @returns true, wenn der Benutzer das Level geliked hat, sonst false
*/
static async hasLiked(levelId: string): Promise<boolean> {
try {
// Prüfen, ob der Benutzer angemeldet ist
const user = pb.authStore.model;
if (!user) return false;
const likes = await pb.collection('level_likes').getList(1, 1, {
filter: `level_id = "${levelId}" && user_id = "${user.id}"`
});
return likes.items.length > 0;
} catch (error) {
console.error('Fehler beim Prüfen des Likes:', error);
return false;
}
}
/**
* Zeichnet einen Spielversuch auf
* @param levelId Die ID des Levels
* @param completed Ob das Level abgeschlossen wurde
* @param completionTime Die Zeit in Sekunden (optional, nur wenn completed = true)
* @returns true, wenn der Versuch erfolgreich aufgezeichnet wurde, sonst false
*/
static async recordPlay(levelId: string, completed: boolean, completionTime?: number): Promise<boolean> {
try {
// Prüfen, ob der Benutzer angemeldet ist
const user = pb.authStore.model;
if (!user) {
// Für nicht angemeldete Benutzer nur den Play-Count erhöhen
const level = await pb.collection('levels').getOne(levelId);
await pb.collection('levels').update(levelId, {
play_count: (level.play_count || 0) + 1
});
return true;
}
// Prüfen, ob bereits ein Spielversuch existiert
const existingPlays = await pb.collection('level_plays').getList(1, 1, {
filter: `level_id = "${levelId}" && user_id = "${user.id}"`,
sort: '-created'
});
if (existingPlays.items.length > 0) {
// Vorhandenen Spielversuch aktualisieren
const play = existingPlays.items[0];
await pb.collection('level_plays').update(play.id, {
attempts: (play.attempts || 1) + 1,
completed: completed || play.completed,
completion_time: completed && completionTime ? completionTime : play.completion_time
});
} else {
// Neuen Spielversuch erstellen
await pb.collection('level_plays').create({
level_id: levelId,
user_id: user.id,
completed,
completion_time: completed ? completionTime : null,
attempts: 1
});
}
// Play-Count im Level erhöhen
const level = await pb.collection('levels').getOne(levelId);
await pb.collection('levels').update(levelId, {
play_count: (level.play_count || 0) + 1
});
return true;
} catch (error) {
console.error('Fehler beim Aufzeichnen des Spielversuchs:', error);
return false;
}
}
/**
* Lädt die Bestenliste für ein Level
* @param levelId Die ID des Levels
* @param limit Maximale Anzahl der Einträge
* @returns Liste der besten Completion-Times
*/
static async getLeaderboard(levelId: string, limit = 10): Promise<any[]> {
try {
const records = await pb.collection('level_plays').getList(1, limit, {
filter: `level_id = "${levelId}" && completed = true && completion_time > 0`,
sort: 'completion_time',
expand: 'user_id'
});
return records.items.map(record => ({
user_id: record.user_id,
user_name: record.expand?.user_id?.name || 'Unbekannt',
completion_time: record.completion_time,
attempts: record.attempts || 1
}));
} catch (error) {
console.error('Fehler beim Laden der Bestenliste:', error);
return [];
}
}
/**
* Konvertiert die Blöcke in ein optimiertes JSON-Format für die Datenbank
* @param blocks Die zu konvertierenden Blöcke
* @returns Die konvertierten Blöcke im optimierten JSON-Format
*/
private static convertBlocksToVoxelData(blocks: Block[]): any {
// Filtere ungültige Blöcke heraus
const validBlocks = blocks.filter(block =>
block &&
block.x !== undefined &&
block.y !== undefined &&
block.z !== undefined &&
block.type
);
if (validBlocks.length === 0) {
return {};
}
// Einfaches Format: Position als Key, Block-Daten als Value
const voxelData: any = {};
validBlocks.forEach(block => {
const key = `${block.x},${block.y},${block.z}`;
voxelData[key] = {
type: block.type,
isSpawnPoint: block.isSpawnPoint || false,
isGoal: block.isGoal || false
};
});
return voxelData;
}
/**
* Konvertiert das JSON-Format aus der Datenbank in Blöcke
* @param voxelData Die zu konvertierenden Daten im JSON-Format
* @returns Die konvertierten Blöcke
*/
private static convertVoxelDataToBlocks(voxelData: any): Block[] {
const blocks: Block[] = [];
if (!voxelData || typeof voxelData !== 'object') {
return blocks;
}
// Prüfen, ob es das neue optimierte Format ist
if (voxelData.format === 'v2' && voxelData.types) {
// Neues Format: Konvertiere zurück zu Blöcken
Object.entries(voxelData.types).forEach(([type, positions]: [string, any]) => {
if (Array.isArray(positions)) {
positions.forEach((pos: number[]) => {
if (pos.length >= 3) {
blocks.push({
x: pos[0],
y: pos[1],
z: pos[2],
type,
isSpawnPoint: false,
isGoal: false
});
}
});
}
});
// Spezielle Blöcke hinzufügen
if (voxelData.special) {
if (voxelData.special.spawn) {
const spawn = voxelData.special.spawn;
const spawnBlock = blocks.find(b => b.x === spawn.x && b.y === spawn.y && b.z === spawn.z);
if (spawnBlock) {
spawnBlock.isSpawnPoint = true;
}
}
if (voxelData.special.goals && Array.isArray(voxelData.special.goals)) {
voxelData.special.goals.forEach((goal: any) => {
const goalBlock = blocks.find(b => b.x === goal.x && b.y === goal.y && b.z === goal.z);
if (goalBlock) {
goalBlock.isGoal = true;
}
});
}
}
} else {
// Altes Format: Position als Key
Object.entries(voxelData).forEach(([key, value]: [string, any]) => {
// Überspringe Metadaten-Keys
if (key === 'format' || key === 'types' || key === 'special') {
return;
}
const [x, y, z] = key.split(',').map(Number);
if (!isNaN(x) && !isNaN(y) && !isNaN(z) && value && value.type) {
blocks.push({
x,
y,
z,
type: value.type,
isSpawnPoint: value.isSpawnPoint || false,
isGoal: value.isGoal || false
});
}
});
}
return blocks;
}
}
// Default export für Kompatibilität
export default LevelService;

View file

@ -0,0 +1,7 @@
// Zentrale Svelte Stores für Spielstatus, Leveldaten etc.
import { writable } from 'svelte/store';
export const gameMode = writable('play'); // 'play' oder 'editor'
export const currentLevel = writable(null);
export const playerStats = writable({ attempts: 0 });
export const levels = writable([]); // initial aus levels.js/json

View file

@ -0,0 +1,83 @@
/**
* Repräsentiert einen einzelnen Block im Voxel-Spiel
*/
export interface Block {
/** X-Koordinate des Blocks */
x: number;
/** Y-Koordinate des Blocks */
y: number;
/** Z-Koordinate des Blocks */
z: number;
/** Typ des Blocks (z.B. 'grass', 'stone', 'lava') */
type: string;
/** Gibt an, ob dieser Block ein Spawn-Punkt ist */
isSpawnPoint?: boolean;
/** Gibt an, ob dieser Block ein Ziel ist */
isGoal?: boolean;
}
/**
* Repräsentiert die Größe der Spielwelt
*/
export interface WorldSize {
/** Breite der Welt in Blöcken */
width: number;
/** Höhe der Welt in Blöcken */
height: number;
/** Tiefe der Welt in Blöcken */
depth: number;
}
/**
* Repräsentiert die Position des Spawn-Punkts
*/
export interface SpawnPoint {
/** X-Koordinate des Spawn-Punkts */
x: number;
/** Y-Koordinate des Spawn-Punkts */
y: number;
/** Z-Koordinate des Spawn-Punkts */
z: number;
}
/**
* Repräsentiert die Metadaten eines Levels (ohne Blockdaten)
*/
export interface LevelMetadata {
/** Eindeutige ID des Levels */
id: string;
/** Name des Levels */
name: string;
/** Beschreibung des Levels */
description: string;
/** ID des Benutzers, der das Level erstellt hat */
userId: string;
/** Zeitpunkt der Erstellung des Levels */
createdAt: string;
/** Zeitpunkt der letzten Aktualisierung des Levels */
updatedAt: string;
/** Gibt an, ob das Level öffentlich ist */
isPublic?: boolean;
/** Anzahl der Aufrufe des Levels */
playCount: number;
/** Anzahl der Likes des Levels */
likesCount: number;
/** Schwierigkeitsgrad des Levels */
difficulty?: string;
/** Tags zur Kategorisierung des Levels */
tags?: string[];
/** URL zum Vorschaubild des Levels */
thumbnailUrl?: string;
}
/**
* Repräsentiert ein vollständiges Level mit allen Daten
*/
export interface Level extends LevelMetadata {
/** Liste aller Blöcke im Level */
blocks: Block[];
/** Position des Spawn-Punkts */
spawnPoint: SpawnPoint;
/** Größe der Spielwelt */
worldSize: WorldSize;
}

View file

@ -0,0 +1,17 @@
<script>
import GameCanvas from '$lib/components/GameCanvas.svelte';
</script>
<main class="w-full h-screen">
<GameCanvas />
</main>
<style>
main {
width: 100%;
height: 100vh;
padding: 0;
margin: 0;
overflow: hidden;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,18 @@
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
};
export default config;

View file

@ -0,0 +1,19 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

View file

@ -0,0 +1,6 @@
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [sveltekit()]
});

27
games/whopixels/.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# Umgebungsvariablen
.env
# Node.js
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
# Logs
logs
*.log
# Betriebssystem-Dateien
.DS_Store
Thumbs.db
# IDE und Editor Dateien
.idea/
.vscode/
*.swp
*.swo
# Build-Verzeichnisse
dist/
build/

74
games/whopixels/README.md Normal file
View file

@ -0,0 +1,74 @@
# WhoPixels
Ein webbasiertes Pixel-Spiel, entwickelt mit Phaser.js.
Projekt Starten:
node server.js
## Über das Projekt
WhoPixels ist ein einfaches Pixel-Art-Editor-Spiel, in dem du deine eigenen Pixel-Kunstwerke erstellen kannst. Das Projekt verwendet Phaser.js, eine leistungsstarke HTML5-Spieleentwicklungsbibliothek.
## Funktionen
- Interaktives Pixel-Art-Editor-Interface
- Farbpalette mit 8 Grundfarben
- Einfache und intuitive Benutzeroberfläche
- Responsive Design
## Erste Schritte
Um das Spiel lokal zu starten, benötigst du einen lokalen Webserver. Du kannst einen einfachen Server mit Python oder Node.js starten.
### Mit Python:
```bash
# Python 3
python -m http.server
# Python 2
python -m SimpleHTTPServer
```
### Mit Node.js:
Installiere zuerst das `http-server`-Paket:
```bash
npm install -g http-server
```
Dann starte den Server:
```bash
http-server
```
## Projektstruktur
```
whopixels/
├── assets/ # Spielressourcen (Bilder, Sounds, etc.)
├── css/ # CSS-Stylesheets
├── js/ # JavaScript-Dateien
│ ├── scenes/ # Phaser-Szenen
│ │ ├── BootScene.js
│ │ ├── MainMenuScene.js
│ │ └── GameScene.js
│ └── main.js # Hauptspieldatei
└── index.html # Haupt-HTML-Datei
```
## Weiterentwicklung
Hier sind einige Ideen für zukünftige Erweiterungen:
- Speichern und Laden von Pixel-Art
- Mehr Werkzeuge (Pinsel, Radierer, Füllen, etc.)
- Animation-Editor
- Teilen von Kunstwerken
- Mehrere Ebenen für komplexere Designs
## Lizenz
Dieses Projekt ist Open Source und steht unter der MIT-Lizenz.

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<title>Background Image</title>
<style>
body { margin: 0; background: #222233; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Fill background
ctx.fillStyle = '#222233';
ctx.fillRect(0, 0, 800, 600);
// Add some pattern
ctx.fillStyle = '#1a1a2a';
for (let i = 0; i < 100; i++) {
const x = Math.random() * 800;
const y = Math.random() * 600;
const size = Math.random() * 5 + 2;
ctx.fillRect(x, y, size, size);
}
// Instructions
ctx.fillStyle = '#ffffff';
ctx.font = '20px Arial';
ctx.fillText('Right-click and save this image as background.png', 200, 300);
</script>
</body>
</html>

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html>
<head>
<title>Create Placeholder Images</title>
</head>
<body>
<h1>Creating placeholder images...</h1>
<canvas id="backgroundCanvas" width="800" height="600" style="display: none;"></canvas>
<canvas id="playerCanvas" width="32" height="32" style="display: none;"></canvas>
<canvas id="tileCanvas" width="32" height="32" style="display: none;"></canvas>
<div id="downloadLinks"></div>
<script>
// Create background image
const bgCanvas = document.getElementById('backgroundCanvas');
const bgCtx = bgCanvas.getContext('2d');
bgCtx.fillStyle = '#222233';
bgCtx.fillRect(0, 0, 800, 600);
// Add some pattern to background
bgCtx.fillStyle = '#1a1a2a';
for (let i = 0; i < 100; i++) {
const x = Math.random() * 800;
const y = Math.random() * 600;
const size = Math.random() * 5 + 2;
bgCtx.fillRect(x, y, size, size);
}
// Create player image
const playerCanvas = document.getElementById('playerCanvas');
const playerCtx = playerCanvas.getContext('2d');
playerCtx.fillStyle = '#ff0000';
playerCtx.fillRect(0, 0, 32, 32);
playerCtx.fillStyle = '#ff5555';
playerCtx.fillRect(8, 8, 16, 16);
// Create tile image
const tileCanvas = document.getElementById('tileCanvas');
const tileCtx = tileCanvas.getContext('2d');
tileCtx.fillStyle = '#ffffff';
tileCtx.fillRect(0, 0, 32, 32);
tileCtx.strokeStyle = '#cccccc';
tileCtx.lineWidth = 1;
tileCtx.strokeRect(0.5, 0.5, 31, 31);
// Create download links
const downloadDiv = document.getElementById('downloadLinks');
function createDownloadLink(canvas, filename) {
const link = document.createElement('a');
link.download = filename;
link.href = canvas.toDataURL('image/png');
link.textContent = `Download ${filename}`;
link.style.display = 'block';
link.style.margin = '10px';
downloadDiv.appendChild(link);
// Auto-click to download
setTimeout(() => link.click(), 500);
}
createDownloadLink(bgCanvas, 'background.png');
createDownloadLink(playerCanvas, 'player.png');
createDownloadLink(tileCanvas, 'tile.png');
</script>
</body>
</html>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>Player Image</title>
<style>
body { margin: 0; background: #333; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { display: block; border: 1px solid #fff; background: #000; }
</style>
</head>
<body>
<canvas id="canvas" width="32" height="32"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Draw player
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, 32, 32);
ctx.fillStyle = '#ff5555';
ctx.fillRect(8, 8, 16, 16);
// Instructions (shown in console)
console.log('Right-click and save this image as player.png');
</script>
</body>
</html>

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,54 @@
// Simple script to create basic placeholder images
// Just open this in a browser and it will create data URLs you can copy
document.body.innerHTML = `
<h1>Placeholder Images for WhoPixels</h1>
<div>
<h2>Background (800x600)</h2>
<canvas id="bg" width="800" height="600" style="border:1px solid #000; max-width: 100%;"></canvas>
<p id="bgData"></p>
</div>
<div>
<h2>Player (32x32)</h2>
<canvas id="player" width="32" height="32" style="border:1px solid #000;"></canvas>
<p id="playerData"></p>
</div>
<div>
<h2>Tile (32x32)</h2>
<canvas id="tile" width="32" height="32" style="border:1px solid #000;"></canvas>
<p id="tileData"></p>
</div>
`;
// Draw background
const bgCanvas = document.getElementById('bg');
const bgCtx = bgCanvas.getContext('2d');
bgCtx.fillStyle = '#222233';
bgCtx.fillRect(0, 0, 800, 600);
for (let i = 0; i < 100; i++) {
bgCtx.fillStyle = '#1a1a2a';
const x = Math.random() * 800;
const y = Math.random() * 600;
const size = Math.random() * 5 + 2;
bgCtx.fillRect(x, y, size, size);
}
document.getElementById('bgData').textContent = 'Save this image as background.png';
// Draw player
const playerCanvas = document.getElementById('player');
const playerCtx = playerCanvas.getContext('2d');
playerCtx.fillStyle = '#ff0000';
playerCtx.fillRect(0, 0, 32, 32);
playerCtx.fillStyle = '#ff5555';
playerCtx.fillRect(8, 8, 16, 16);
document.getElementById('playerData').textContent = 'Save this image as player.png';
// Draw tile
const tileCanvas = document.getElementById('tile');
const tileCtx = tileCanvas.getContext('2d');
tileCtx.fillStyle = '#ffffff';
tileCtx.fillRect(0, 0, 32, 32);
tileCtx.strokeStyle = '#cccccc';
tileCtx.lineWidth = 1;
tileCtx.strokeRect(0.5, 0.5, 31, 31);
document.getElementById('tileData').textContent = 'Save this image as tile.png';

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html>
<head>
<title>Tile Image</title>
<style>
body { margin: 0; background: #333; display: flex; justify-content: center; align-items: center; height: 100vh; }
canvas { display: block; border: 1px solid #fff; background: #000; }
</style>
</head>
<body>
<canvas id="canvas" width="32" height="32"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Draw tile
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, 32, 32);
ctx.strokeStyle = '#cccccc';
ctx.lineWidth = 1;
ctx.strokeRect(0.5, 0.5, 31, 31);
// Instructions (shown in console)
console.log('Right-click and save this image as tile.png');
</script>
</body>
</html>

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,14 @@
body {
margin: 0;
padding: 0;
background-color: #000;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-family: 'Arial', sans-serif;
}
#game-container {
box-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
}

View file

@ -0,0 +1,73 @@
// Liste der NPC-Charaktere mit Namen und Persönlichkeiten
// NPC-Charaktere: Berühmte Erfinder durch die Historie
const npcCharacters = [
{
id: 1,
name: "Leonardo da Vinci",
personality: "Ein vielseitiger Universalgelehrter der Renaissance, bekannt für seine Kunst und Erfindungen. Er spricht nachdenklich und philosophisch, oft mit Metaphern über Natur und Kunst. Er ist neugierig und beobachtet alles genau.",
hint: "Meine Skizzenbücher enthalten Flugmaschinen und anatomische Studien, die ihrer Zeit weit voraus waren."
},
{
id: 2,
name: "Nikola Tesla",
personality: "Ein exzentrischer Elektroingenieur mit visionären Ideen. Er spricht leidenschaftlich über Elektrizität und drahtlose Energieübertragung. Er ist brillant, aber auch etwas eigenartig und distanziert.",
hint: "Meine Arbeiten mit Wechselstrom revolutionierten die Art, wie wir Energie nutzen."
},
{
id: 3,
name: "Marie Curie",
personality: "Eine entschlossene und präzise Wissenschaftlerin, die für ihre Entdeckungen im Bereich der Radioaktivität bekannt ist. Sie spricht klar und methodisch, mit einem starken Fokus auf wissenschaftliche Genauigkeit.",
hint: "Meine Forschung zu radioaktiven Elementen brachte mir zwei Nobelpreise ein, obwohl sie letztendlich meine Gesundheit beeinträchtigte."
},
{
id: 4,
name: "Thomas Edison",
personality: "Ein pragmatischer und geschäftstüchtiger Erfinder mit über 1.000 Patenten. Er spricht direkt und selbstbewusst, oft mit praktischen Beispielen. Er betont harte Arbeit und Ausdauer über Inspiration.",
hint: "Meine Erfindung brachte Licht in die Dunkelheit und veränderte die Art, wie Menschen nach Sonnenuntergang leben."
},
{
id: 5,
name: "Ada Lovelace",
personality: "Eine visionäre Mathematikerin des 19. Jahrhunderts mit einer einzigartigen Verbindung von Logik und Kreativität. Sie spricht eloquent und präzise, mit einer Mischung aus poetischer und mathematischer Sprache.",
hint: "Ich schrieb den ersten Algorithmus für eine Maschine, lange bevor Computer existierten."
},
{
id: 6,
name: "Archimedes",
personality: "Ein genialer antiker Mathematiker und Erfinder aus Syrakus. Er ist von mathematischen Problemen fasziniert und kann sich darin verlieren. Er spricht mit Begeisterung über Geometrie und physikalische Prinzipien.",
hint: "Mein berühmtester Ausruf war 'Heureka!' als ich das Prinzip des Auftriebs in der Badewanne entdeckte."
},
{
id: 7,
name: "Johannes Gutenberg",
personality: "Ein geduldiger und präziser Handwerker, der die Druckkunst revolutionierte. Er spricht bescheiden über seine Erfindung, aber mit Stolz über deren Auswirkungen auf die Verbreitung von Wissen.",
hint: "Meine Erfindung machte Bücher für die Massen zugänglich und veränderte die Verbreitung von Wissen für immer."
},
{
id: 8,
name: "Grace Hopper",
personality: "Eine pragmatische und humorvolle Computerpionierin und Marineoffizierin. Sie erklärt komplexe Konzepte mit einfachen Analogien und hat einen trockenen Humor. Sie ist direkt und lösungsorientiert.",
hint: "Ich entwickelte den ersten Compiler und fand einmal einen echten 'Bug' im Computer - eine Motte, die einen Fehler verursachte."
},
{
id: 9,
name: "Alexander Graham Bell",
personality: "Ein einfallsreicher und geduldiger Erfinder, der sich für Kommunikation und Gehörlose engagierte. Er spricht deutlich und artikuliert, mit einem schottischen Akzent. Er ist enthusiastisch, wenn er über seine Erfindungen spricht.",
hint: "Meine Erfindung ermöglichte es Menschen, über große Entfernungen miteinander zu sprechen."
},
{
id: 10,
name: "Hedy Lamarr",
personality: "Eine glamouröse Hollywoodschauspielerin mit einem brillanten technischen Verstand. Sie spricht charmant und selbstbewusst, mit einer Mischung aus Eleganz und technischem Scharfsinn. Sie ist kreativ und unkonventionell.",
hint: "Meine Erfindung der Frequenzsprungverfahren bildet die Grundlage für moderne WLAN- und Bluetooth-Technologien, obwohl viele mich nur als Filmstar kennen."
}
];
// Mache die Charaktere sowohl im Browser als auch in Node.js verfügbar
if (typeof window !== 'undefined') {
// Browser-Umgebung
window.npcCharacters = npcCharacters;
} else if (typeof module !== 'undefined') {
// Node.js-Umgebung
module.exports = npcCharacters;
}

View file

@ -0,0 +1,47 @@
// This script uses Node.js to generate placeholder images for our game
const fs = require('fs');
const { createCanvas } = require('canvas');
// Create background image (800x600)
const bgCanvas = createCanvas(800, 600);
const bgCtx = bgCanvas.getContext('2d');
bgCtx.fillStyle = '#222233';
bgCtx.fillRect(0, 0, 800, 600);
// Add some pattern to background
bgCtx.fillStyle = '#1a1a2a';
for (let i = 0; i < 100; i++) {
const x = Math.random() * 800;
const y = Math.random() * 600;
const size = Math.random() * 5 + 2;
bgCtx.fillRect(x, y, size, size);
}
// Create player image (32x32)
const playerCanvas = createCanvas(32, 32);
const playerCtx = playerCanvas.getContext('2d');
playerCtx.fillStyle = '#ff0000';
playerCtx.fillRect(0, 0, 32, 32);
playerCtx.fillStyle = '#ff5555';
playerCtx.fillRect(8, 8, 16, 16);
// Create tile image (32x32)
const tileCanvas = createCanvas(32, 32);
const tileCtx = tileCanvas.getContext('2d');
tileCtx.fillStyle = '#ffffff';
tileCtx.fillRect(0, 0, 32, 32);
tileCtx.strokeStyle = '#cccccc';
tileCtx.lineWidth = 1;
tileCtx.strokeRect(0.5, 0.5, 31, 31);
// Save images
const bgBuffer = bgCanvas.toBuffer('image/png');
fs.writeFileSync('./assets/background.png', bgBuffer);
const playerBuffer = playerCanvas.toBuffer('image/png');
fs.writeFileSync('./assets/player.png', playerBuffer);
const tileBuffer = tileCanvas.toBuffer('image/png');
fs.writeFileSync('./assets/tile.png', tileBuffer);
console.log('All placeholder images have been generated!');

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WhoPixels - Pixel Game</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="game-container"></div>
<!-- Phaser Library -->
<script src="https://cdn.jsdelivr.net/npm/phaser@3.55.2/dist/phaser.min.js"></script>
<!-- Game Data -->
<script src="data/npc_characters.js"></script>
<!-- Game Scripts -->
<script src="js/scenes/BootScene.js"></script>
<script src="js/scenes/MainMenuScene.js"></script>
<script src="js/scenes/GameScene.js"></script>
<script src="js/scenes/RPGScene.js"></script>
<script src="js/main.js"></script>
</body>
</html>

View file

@ -0,0 +1,18 @@
// Game configuration
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
pixelArt: true,
physics: {
default: 'arcade',
arcade: {
gravity: { y: 0 },
debug: false
}
},
scene: [BootScene, MainMenuScene, GameScene, RPGScene]
};
// Create and start the game
const game = new Phaser.Game(config);

View file

@ -0,0 +1,445 @@
class BootScene extends Phaser.Scene {
constructor() {
super({ key: 'BootScene' });
}
preload() {
// Loading screen
this.graphics = this.add.graphics();
this.newGraphics = this.add.graphics();
const progressBar = new Phaser.Geom.Rectangle(200, 300, 400, 50);
const progressBarFill = new Phaser.Geom.Rectangle(205, 305, 290, 40);
this.graphics.fillStyle(0xffffff, 1);
this.graphics.fillRectShape(progressBar);
this.newGraphics.fillStyle(0x3587e2, 1);
this.newGraphics.fillRectShape(progressBarFill);
const loadingText = this.add.text(250, 260, "Loading: ", { fontSize: '32px', fill: '#FFF' });
// Update as load progresses
this.load.on('progress', (percent) => {
loadingText.setText("Loading: " + parseInt(percent * 100) + "%");
progressBarFill.width = 390 * percent;
this.newGraphics.clear();
this.newGraphics.fillStyle(0x3587e2, 1);
this.newGraphics.fillRectShape(progressBarFill);
});
this.load.on('complete', () => {
loadingText.destroy();
this.graphics.destroy();
this.newGraphics.destroy();
});
// We'll create graphics objects instead of loading images
}
create() {
// Create a texture for background
const bgGraphics = this.make.graphics({ x: 0, y: 0 });
bgGraphics.fillStyle(0x222233);
bgGraphics.fillRect(0, 0, 800, 600);
// Add some pattern to background
bgGraphics.fillStyle(0x1a1a2a);
for (let i = 0; i < 100; i++) {
const x = Math.random() * 800;
const y = Math.random() * 600;
const size = Math.random() * 5 + 2;
bgGraphics.fillRect(x, y, size, size);
}
// Erstelle ein Partikel-Sprite für Spezialeffekte
const particleGraphics = this.make.graphics({ x: 0, y: 0 });
particleGraphics.fillStyle(0xffffff);
particleGraphics.fillCircle(4, 4, 4);
particleGraphics.generateTexture('particle', 8, 8);
bgGraphics.generateTexture('background', 800, 600);
// Create a texture for player (pixel editor)
const playerGraphics = this.make.graphics({ x: 0, y: 0 });
playerGraphics.fillStyle(0xff0000);
playerGraphics.fillRect(0, 0, 32, 32);
playerGraphics.fillStyle(0xff5555);
playerGraphics.fillRect(8, 8, 16, 16);
playerGraphics.generateTexture('player', 32, 32);
// Erstelle Texturen für verschiedene Tile-Typen (8x8 Pixel Tiles, skaliert auf 32x32)
this.createTileTextures();
// Erstelle NPC-Texturen
this.createNPCTextures();
// Create a texture for basic tile
const tileGraphics = this.make.graphics({ x: 0, y: 0 });
tileGraphics.fillStyle(0xffffff);
tileGraphics.fillRect(0, 0, 32, 32);
tileGraphics.lineStyle(1, 0xcccccc);
tileGraphics.strokeRect(0, 0, 32, 32);
tileGraphics.generateTexture('tile', 32, 32);
// Create player walk animation frames (4 directions)
this.createPlayerWalkAnimations();
this.scene.start('MainMenuScene');
}
createPlayerWalkAnimations() {
// Erstelle eine Spritesheet-Textur für den Spieler im RPG
const frameWidth = 32;
const frameHeight = 32;
// Farbpalette für den Spieler
const colors = {
body: 0x3366cc, // Blauer Körper
face: 0xffcc99, // Hautfarbe für Gesicht
hair: 0x663300, // Braune Haare
shirt: 0x339933, // Grünes Hemd
pants: 0x333366, // Dunkelblaue Hose
shoes: 0x663300, // Braune Schuhe
outline: 0x000000 // Schwarze Umrisse
};
// Gemeinsame Funktion zum Zeichnen der Grundform des Spielers
const drawPlayerBase = (graphics) => {
// Umriss
graphics.lineStyle(1, colors.outline);
// Körper (Torso)
graphics.fillStyle(colors.shirt);
graphics.fillRect(10, 12, 12, 10);
graphics.strokeRect(10, 12, 12, 10);
// Kopf
graphics.fillStyle(colors.face);
graphics.fillRect(10, 4, 12, 8);
graphics.strokeRect(10, 4, 12, 8);
// Haare
graphics.fillStyle(colors.hair);
graphics.fillRect(10, 4, 12, 3);
graphics.strokeRect(10, 4, 12, 3);
// Hose
graphics.fillStyle(colors.pants);
graphics.fillRect(10, 22, 12, 6);
graphics.strokeRect(10, 22, 12, 6);
};
// Nach unten (0)
const downGraphics = this.make.graphics({ x: 0, y: 0 });
drawPlayerBase(downGraphics);
// Gesicht nach unten
downGraphics.fillStyle(colors.outline);
downGraphics.fillRect(14, 8, 1, 1); // Linkes Auge
downGraphics.fillRect(17, 8, 1, 1); // Rechtes Auge
downGraphics.fillRect(15, 10, 2, 1); // Mund
// Beine und Schuhe nach unten
downGraphics.fillStyle(colors.pants);
downGraphics.fillRect(12, 28, 3, 2); // Linkes Bein
downGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein
downGraphics.fillStyle(colors.shoes);
downGraphics.fillRect(12, 30, 3, 2); // Linker Schuh
downGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh
downGraphics.lineStyle(1, colors.outline);
downGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss
downGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss
downGraphics.generateTexture('player_down', frameWidth, frameHeight);
// Nach oben (1)
const upGraphics = this.make.graphics({ x: 0, y: 0 });
drawPlayerBase(upGraphics);
// Rücken der Haare
upGraphics.fillStyle(colors.hair);
upGraphics.fillRect(10, 2, 12, 2);
upGraphics.lineStyle(1, colors.outline);
upGraphics.strokeRect(10, 2, 12, 2);
// Beine und Schuhe nach oben
upGraphics.fillStyle(colors.pants);
upGraphics.fillRect(12, 28, 3, 2); // Linkes Bein
upGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein
upGraphics.fillStyle(colors.shoes);
upGraphics.fillRect(12, 30, 3, 2); // Linker Schuh
upGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh
upGraphics.lineStyle(1, colors.outline);
upGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss
upGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss
upGraphics.generateTexture('player_up', frameWidth, frameHeight);
// Nach links (2)
const leftGraphics = this.make.graphics({ x: 0, y: 0 });
drawPlayerBase(leftGraphics);
// Gesicht nach links
leftGraphics.fillStyle(colors.outline);
leftGraphics.fillRect(12, 8, 1, 1); // Auge
leftGraphics.fillRect(11, 10, 2, 1); // Mund
// Arm nach links
leftGraphics.fillStyle(colors.shirt);
leftGraphics.fillRect(6, 14, 4, 3);
leftGraphics.lineStyle(1, colors.outline);
leftGraphics.strokeRect(6, 14, 4, 3);
// Beine und Schuhe nach links
leftGraphics.fillStyle(colors.pants);
leftGraphics.fillRect(12, 28, 3, 2); // Linkes Bein
leftGraphics.fillRect(15, 28, 3, 2); // Rechtes Bein
leftGraphics.fillStyle(colors.shoes);
leftGraphics.fillRect(9, 30, 6, 2); // Schuhe
leftGraphics.lineStyle(1, colors.outline);
leftGraphics.strokeRect(9, 28, 9, 4); // Beine Umriss
leftGraphics.generateTexture('player_left', frameWidth, frameHeight);
// Nach rechts (3)
const rightGraphics = this.make.graphics({ x: 0, y: 0 });
drawPlayerBase(rightGraphics);
// Gesicht nach rechts
rightGraphics.fillStyle(colors.outline);
rightGraphics.fillRect(19, 8, 1, 1); // Auge
rightGraphics.fillRect(19, 10, 2, 1); // Mund
// Arm nach rechts
rightGraphics.fillStyle(colors.shirt);
rightGraphics.fillRect(22, 14, 4, 3);
rightGraphics.lineStyle(1, colors.outline);
rightGraphics.strokeRect(22, 14, 4, 3);
// Beine und Schuhe nach rechts
rightGraphics.fillStyle(colors.pants);
rightGraphics.fillRect(14, 28, 3, 2); // Linkes Bein
rightGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein
rightGraphics.fillStyle(colors.shoes);
rightGraphics.fillRect(17, 30, 6, 2); // Schuhe
rightGraphics.lineStyle(1, colors.outline);
rightGraphics.strokeRect(14, 28, 9, 4); // Beine Umriss
rightGraphics.generateTexture('player_right', frameWidth, frameHeight);
}
createTileTextures() {
const tileSize = 32; // Größe jedes Tiles
// 1. Gras
const grassGraphics = this.make.graphics({ x: 0, y: 0 });
grassGraphics.fillStyle(0x88aa44); // Grün für Gras
grassGraphics.fillRect(0, 0, tileSize, tileSize);
// Kleine Texturen für Gras
grassGraphics.fillStyle(0x779933);
for (let i = 0; i < 8; i++) {
const x = Math.random() * tileSize;
const y = Math.random() * tileSize;
grassGraphics.fillRect(x, y, 2, 2);
}
grassGraphics.generateTexture('tile_grass', tileSize, tileSize);
// 2. Gras mit Blumen
const grassFlowerGraphics = this.make.graphics({ x: 0, y: 0 });
grassFlowerGraphics.fillStyle(0x88aa44); // Grün für Gras
grassFlowerGraphics.fillRect(0, 0, tileSize, tileSize);
// Kleine Texturen für Gras
grassFlowerGraphics.fillStyle(0x779933);
for (let i = 0; i < 5; i++) {
const x = Math.random() * tileSize;
const y = Math.random() * tileSize;
grassFlowerGraphics.fillRect(x, y, 2, 2);
}
// Blumen hinzufügen
grassFlowerGraphics.fillStyle(0xffff00); // Gelb für Blumen
for (let i = 0; i < 3; i++) {
const x = 5 + Math.random() * (tileSize - 10);
const y = 5 + Math.random() * (tileSize - 10);
grassFlowerGraphics.fillRect(x, y, 3, 3);
}
grassFlowerGraphics.fillStyle(0xff5555); // Rot für Blumen
for (let i = 0; i < 2; i++) {
const x = 5 + Math.random() * (tileSize - 10);
const y = 5 + Math.random() * (tileSize - 10);
grassFlowerGraphics.fillRect(x, y, 3, 3);
}
grassFlowerGraphics.generateTexture('tile_grass_flower', tileSize, tileSize);
// 3. Erde
const dirtGraphics = this.make.graphics({ x: 0, y: 0 });
dirtGraphics.fillStyle(0x8B4513); // Braun für Erde
dirtGraphics.fillRect(0, 0, tileSize, tileSize);
// Kleine Texturen für Erde
dirtGraphics.fillStyle(0x6B3304);
for (let i = 0; i < 10; i++) {
const x = Math.random() * tileSize;
const y = Math.random() * tileSize;
dirtGraphics.fillRect(x, y, 2, 2);
}
dirtGraphics.generateTexture('tile_dirt', tileSize, tileSize);
// 4. Erde mit Steinen
const dirtStoneGraphics = this.make.graphics({ x: 0, y: 0 });
dirtStoneGraphics.fillStyle(0x8B4513); // Braun für Erde
dirtStoneGraphics.fillRect(0, 0, tileSize, tileSize);
// Kleine Texturen für Erde
dirtStoneGraphics.fillStyle(0x6B3304);
for (let i = 0; i < 6; i++) {
const x = Math.random() * tileSize;
const y = Math.random() * tileSize;
dirtStoneGraphics.fillRect(x, y, 2, 2);
}
// Steine hinzufügen
dirtStoneGraphics.fillStyle(0x888888); // Grau für Steine
for (let i = 0; i < 4; i++) {
const size = 3 + Math.random() * 4;
const x = Math.random() * (tileSize - size);
const y = Math.random() * (tileSize - size);
dirtStoneGraphics.fillRect(x, y, size, size);
}
dirtStoneGraphics.generateTexture('tile_dirt_stone', tileSize, tileSize);
// 5. Steinwand
const stoneWallGraphics = this.make.graphics({ x: 0, y: 0 });
stoneWallGraphics.fillStyle(0x777777); // Grau für Steinwand
stoneWallGraphics.fillRect(0, 0, tileSize, tileSize);
// Steinmuster für die Wand
stoneWallGraphics.fillStyle(0x555555);
const brickHeight = 8;
const brickWidth = 16;
for (let y = 0; y < tileSize; y += brickHeight) {
let offset = (y % (brickHeight * 2) === 0) ? 0 : brickWidth / 2;
for (let x = offset; x < tileSize; x += brickWidth) {
stoneWallGraphics.fillRect(x, y, brickWidth - 1, brickHeight - 1);
}
}
stoneWallGraphics.generateTexture('tile_stone_wall', tileSize, tileSize);
// 6. Steinwand mit Blumen/Moos
const stoneWallFlowerGraphics = this.make.graphics({ x: 0, y: 0 });
stoneWallFlowerGraphics.fillStyle(0x777777); // Grau für Steinwand
stoneWallFlowerGraphics.fillRect(0, 0, tileSize, tileSize);
// Steinmuster für die Wand
stoneWallFlowerGraphics.fillStyle(0x555555);
for (let y = 0; y < tileSize; y += brickHeight) {
let offset = (y % (brickHeight * 2) === 0) ? 0 : brickWidth / 2;
for (let x = offset; x < tileSize; x += brickWidth) {
stoneWallFlowerGraphics.fillRect(x, y, brickWidth - 1, brickHeight - 1);
}
}
// Moos/Blumen an der Wand
stoneWallFlowerGraphics.fillStyle(0x55AA55); // Grün für Moos
for (let i = 0; i < 6; i++) {
const x = Math.random() * tileSize;
const y = Math.random() * tileSize;
const size = 2 + Math.random() * 3;
stoneWallFlowerGraphics.fillRect(x, y, size, size);
}
stoneWallFlowerGraphics.fillStyle(0xffff00); // Gelb für Blumen
for (let i = 0; i < 2; i++) {
const x = 5 + Math.random() * (tileSize - 10);
const y = 5 + Math.random() * (tileSize - 10);
stoneWallFlowerGraphics.fillRect(x, y, 3, 3);
}
stoneWallFlowerGraphics.generateTexture('tile_stone_wall_flower', tileSize, tileSize);
}
createNPCTextures() {
const frameWidth = 32;
const frameHeight = 32;
// Farbpalette für den NPC
const colors = {
body: 0xcc6633, // Bräunlicher Körper
face: 0xffcc99, // Hautfarbe für Gesicht
hair: 0x996600, // Blonde Haare
shirt: 0xcc3333, // Rotes Hemd
pants: 0x333333, // Schwarze Hose
shoes: 0x663300, // Braune Schuhe
outline: 0x000000 // Schwarze Umrisse
};
// Gemeinsame Funktion zum Zeichnen der Grundform des NPCs
const drawNPCBase = (graphics) => {
// Umriss
graphics.lineStyle(1, colors.outline);
// Körper (Torso)
graphics.fillStyle(colors.shirt);
graphics.fillRect(10, 12, 12, 10);
graphics.strokeRect(10, 12, 12, 10);
// Kopf
graphics.fillStyle(colors.face);
graphics.fillRect(10, 4, 12, 8);
graphics.strokeRect(10, 4, 12, 8);
// Haare
graphics.fillStyle(colors.hair);
graphics.fillRect(10, 4, 12, 3);
graphics.strokeRect(10, 4, 12, 3);
// Hose
graphics.fillStyle(colors.pants);
graphics.fillRect(10, 22, 12, 6);
graphics.strokeRect(10, 22, 12, 6);
};
// NPC nach unten schauend
const npcDownGraphics = this.make.graphics({ x: 0, y: 0 });
drawNPCBase(npcDownGraphics);
// Gesicht nach unten
npcDownGraphics.fillStyle(colors.outline);
npcDownGraphics.fillRect(14, 8, 1, 1); // Linkes Auge
npcDownGraphics.fillRect(17, 8, 1, 1); // Rechtes Auge
npcDownGraphics.fillRect(15, 10, 2, 1); // Mund
// Beine und Schuhe nach unten
npcDownGraphics.fillStyle(colors.pants);
npcDownGraphics.fillRect(12, 28, 3, 2); // Linkes Bein
npcDownGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein
npcDownGraphics.fillStyle(colors.shoes);
npcDownGraphics.fillRect(12, 30, 3, 2); // Linker Schuh
npcDownGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh
npcDownGraphics.lineStyle(1, colors.outline);
npcDownGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss
npcDownGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss
npcDownGraphics.generateTexture('npc_down', frameWidth, frameHeight);
// NPC nach oben schauend
const npcUpGraphics = this.make.graphics({ x: 0, y: 0 });
drawNPCBase(npcUpGraphics);
// Rücken der Haare
npcUpGraphics.fillStyle(colors.hair);
npcUpGraphics.fillRect(10, 2, 12, 2);
npcUpGraphics.lineStyle(1, colors.outline);
npcUpGraphics.strokeRect(10, 2, 12, 2);
// Beine und Schuhe nach oben
npcUpGraphics.fillStyle(colors.pants);
npcUpGraphics.fillRect(12, 28, 3, 2); // Linkes Bein
npcUpGraphics.fillRect(17, 28, 3, 2); // Rechtes Bein
npcUpGraphics.fillStyle(colors.shoes);
npcUpGraphics.fillRect(12, 30, 3, 2); // Linker Schuh
npcUpGraphics.fillRect(17, 30, 3, 2); // Rechter Schuh
npcUpGraphics.lineStyle(1, colors.outline);
npcUpGraphics.strokeRect(12, 28, 3, 4); // Linkes Bein Umriss
npcUpGraphics.strokeRect(17, 28, 3, 4); // Rechtes Bein Umriss
npcUpGraphics.generateTexture('npc_up', frameWidth, frameHeight);
}
}

View file

@ -0,0 +1,108 @@
class GameScene extends Phaser.Scene {
constructor() {
super({ key: 'GameScene' });
}
create() {
// Add background
this.add.image(400, 300, 'background');
// Create grid for pixel art (16x16 grid of 32x32 pixel tiles)
this.grid = [];
this.tileSize = 32;
this.gridWidth = 16;
this.gridHeight = 16;
this.gridStartX = (800 - (this.gridWidth * this.tileSize)) / 2;
this.gridStartY = (600 - (this.gridHeight * this.tileSize)) / 2;
// Create grid of tiles
for (let y = 0; y < this.gridHeight; y++) {
this.grid[y] = [];
for (let x = 0; x < this.gridWidth; x++) {
const tile = this.add.image(
this.gridStartX + x * this.tileSize + this.tileSize / 2,
this.gridStartY + y * this.tileSize + this.tileSize / 2,
'tile'
);
tile.setTint(0xffffff); // Default white color
tile.setInteractive();
tile.on('pointerdown', () => {
this.paintTile(x, y);
});
this.grid[y][x] = tile;
}
}
// Current selected color (default: black)
this.currentColor = 0x000000;
// Create color palette
this.createColorPalette();
// Add UI text
this.add.text(400, 50, 'Pixel Editor', {
fontSize: '32px',
fill: '#fff'
}).setOrigin(0.5);
// Add back button
const backButton = this.add.text(100, 50, 'Zurück', {
fontSize: '24px',
fill: '#fff',
backgroundColor: '#4a4a4a',
padding: { x: 10, y: 5 }
}).setOrigin(0.5).setInteractive();
backButton.on('pointerover', () => {
backButton.setStyle({ fill: '#ff0' });
});
backButton.on('pointerout', () => {
backButton.setStyle({ fill: '#fff' });
});
backButton.on('pointerdown', () => {
this.scene.start('MainMenuScene');
});
}
createColorPalette() {
const colors = [
0x000000, // Black
0xffffff, // White
0xff0000, // Red
0x00ff00, // Green
0x0000ff, // Blue
0xffff00, // Yellow
0xff00ff, // Magenta
0x00ffff // Cyan
];
const paletteX = 700;
const paletteY = 150;
const paletteSize = 30;
const paletteGap = 10;
colors.forEach((color, index) => {
const colorButton = this.add.rectangle(
paletteX,
paletteY + index * (paletteSize + paletteGap),
paletteSize,
paletteSize,
color
);
colorButton.setInteractive();
colorButton.on('pointerdown', () => {
this.currentColor = color;
});
// Add stroke around the button
colorButton.setStrokeStyle(2, 0xffffff);
});
}
paintTile(x, y) {
this.grid[y][x].setTint(this.currentColor);
}
}

View file

@ -0,0 +1,85 @@
class MainMenuScene extends Phaser.Scene {
constructor() {
super({ key: 'MainMenuScene' });
}
create() {
// Add background
this.add.image(400, 300, 'background');
// Add title
this.add.text(400, 120, 'WhoPixels', {
fontSize: '64px',
fill: '#fff',
fontStyle: 'bold'
}).setOrigin(0.5);
// Add subtitle
this.add.text(400, 180, 'Ein Pixel-Abenteuer', {
fontSize: '32px',
fill: '#fff'
}).setOrigin(0.5);
// Create buttons
const startButton = this.add.text(400, 280, 'RPG Spiel starten', {
fontSize: '32px',
fill: '#fff',
backgroundColor: '#4a4a4a',
padding: { x: 20, y: 10 }
}).setOrigin(0.5).setInteractive();
const editorButton = this.add.text(400, 350, 'Pixel Editor', {
fontSize: '32px',
fill: '#fff',
backgroundColor: '#4a4a4a',
padding: { x: 20, y: 10 }
}).setOrigin(0.5).setInteractive();
const optionsButton = this.add.text(400, 420, 'Optionen', {
fontSize: '32px',
fill: '#fff',
backgroundColor: '#4a4a4a',
padding: { x: 20, y: 10 }
}).setOrigin(0.5).setInteractive();
// Button interactions - RPG Game
startButton.on('pointerover', () => {
startButton.setStyle({ fill: '#ff0' });
});
startButton.on('pointerout', () => {
startButton.setStyle({ fill: '#fff' });
});
startButton.on('pointerdown', () => {
this.scene.start('RPGScene');
});
// Button interactions - Pixel Editor
editorButton.on('pointerover', () => {
editorButton.setStyle({ fill: '#ff0' });
});
editorButton.on('pointerout', () => {
editorButton.setStyle({ fill: '#fff' });
});
editorButton.on('pointerdown', () => {
this.scene.start('GameScene');
});
// Button interactions - Options
optionsButton.on('pointerover', () => {
optionsButton.setStyle({ fill: '#ff0' });
});
optionsButton.on('pointerout', () => {
optionsButton.setStyle({ fill: '#fff' });
});
optionsButton.on('pointerdown', () => {
// Options functionality would go here
console.log('Options button clicked');
});
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,6 @@
{
"dependencies": {
"dotenv": "^16.4.7",
"node-fetch": "^2.7.0"
}
}

261
games/whopixels/server.js Normal file
View file

@ -0,0 +1,261 @@
const http = require('http');
const fs = require('fs');
const path = require('path');
const url = require('url');
// Lade Umgebungsvariablen aus .env-Datei
require('dotenv').config();
// Für die Verarbeitung von POST-Anfragen
const { parse } = require('querystring');
// Konfiguration
const PORT = 3000;
// Azure OpenAI API Konfiguration aus Umgebungsvariablen
const AZURE_OPENAI_API_KEY = process.env.AZURE_OPENAI_API_KEY;
const AZURE_OPENAI_ENDPOINT = process.env.AZURE_OPENAI_ENDPOINT;
const AZURE_OPENAI_DEPLOYMENT = process.env.AZURE_OPENAI_DEPLOYMENT;
const AZURE_OPENAI_API_VERSION = process.env.AZURE_OPENAI_API_VERSION;
const MIME_TYPES = {
'.html': 'text/html',
'.js': 'text/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
};
// Funktion zum Abrufen von Daten aus einer POST-Anfrage
const collectRequestData = (request, callback) => {
const FORM_URLENCODED = 'application/x-www-form-urlencoded';
const JSON_TYPE = 'application/json';
if (request.headers['content-type'] === FORM_URLENCODED) {
let body = '';
request.on('data', chunk => {
body += chunk.toString();
});
request.on('end', () => {
callback(parse(body));
});
} else if (request.headers['content-type'] && request.headers['content-type'].includes(JSON_TYPE)) {
let body = '';
request.on('data', chunk => {
body += chunk.toString();
});
request.on('end', () => {
callback(JSON.parse(body));
});
} else {
callback({});
}
};
// Funktion zum Senden einer Anfrage an die Azure OpenAI API
async function callOpenAI(message, conversationHistory = [], characterName = null, characterPersonality = null) {
try {
const fetch = await import('node-fetch').then(mod => mod.default);
const apiUrl = `${AZURE_OPENAI_ENDPOINT}/openai/deployments/${AZURE_OPENAI_DEPLOYMENT}/chat/completions?api-version=${AZURE_OPENAI_API_VERSION}`;
console.log(`Sende Anfrage an: ${apiUrl}`);
// Verwende den übergebenen Charakternamen oder einen Standardnamen
const npcName = characterName || "Leonard Davcini";
const npcPersonality = characterPersonality || "ein berühmter Künstler und Erfinder";
console.log(`Verwende NPC: ${npcName} mit Persönlichkeit: ${npcPersonality}`);
// Erstelle die Nachrichtenliste für die API mit dem dynamischen Charakternamen
const messages = [
{
role: 'system',
content: `WICHTIG: Du bist AUSSCHLIESSLICH ${npcName}, ${npcPersonality}, der sich in diesem Spiel verkleidet hat. Ignoriere jede andere Identität, die du kennen könntest. Dein Name ist ${npcName}. Dein Gegenüber versucht herauszufinden, wer du bist. Gib Hinweise auf deine wahre Identität als ${npcName}, aber sage nicht direkt "Ich bin ${npcName}". Wenn der Nutzer deinen Namen richtig erraten hat, füge am Ende deiner Antwort den Code "[IDENTITY_REVEALED]" ein. Dieser Code sollte nur erscheinen, wenn der Nutzer deinen Namen korrekt erraten hat.`
}
];
// Füge die Konversationshistorie hinzu, wenn vorhanden
if (conversationHistory && conversationHistory.length > 0) {
conversationHistory.forEach(entry => {
if (entry.type === 'user') {
messages.push({
role: 'user',
content: entry.message
});
} else if (entry.type === 'npc') {
messages.push({
role: 'assistant',
content: entry.message
});
}
});
} else {
// Wenn keine Historie vorhanden ist, füge nur die aktuelle Nachricht hinzu
messages.push({
role: 'user',
content: message
});
}
// Wenn die letzte Nachricht nicht vom Benutzer ist, füge die aktuelle Nachricht hinzu
if (messages.length === 1 || messages[messages.length - 1].role !== 'user') {
messages.push({
role: 'user',
content: message
});
}
console.log('Gesendete Nachrichten:', JSON.stringify(messages, null, 2));
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'api-key': AZURE_OPENAI_API_KEY
},
body: JSON.stringify({
messages: messages,
max_tokens: 150
})
});
// Prüfe den HTTP-Status
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP Fehler: ${response.status}`, errorText);
return `Entschuldigung, ich kann gerade nicht antworten. (HTTP ${response.status})`;
}
const data = await response.json();
console.log('API-Antwort:', JSON.stringify(data, null, 2));
if (data.error) {
console.error('Azure OpenAI API Fehler:', data.error);
return { text: 'Entschuldigung, ich kann gerade nicht antworten. Versuche es später noch einmal.', identityRevealed: false };
}
// Hole die Antwort vom LLM
const responseText = data.choices[0].message.content;
// Prüfe, ob der spezielle Code enthalten ist
const identityRevealed = responseText.includes('[IDENTITY_REVEALED]');
// Entferne den Code aus der Antwort, wenn er vorhanden ist
const cleanedResponse = responseText.replace('[IDENTITY_REVEALED]', '').trim();
console.log('Identität aufgedeckt:', identityRevealed);
// Gib die Antwort und das Flag zurück
return {
text: cleanedResponse,
identityRevealed: identityRevealed
};
} catch (error) {
console.error('Fehler beim Aufrufen der Azure OpenAI API:', error);
return {
text: 'Entschuldigung, ich kann gerade nicht antworten. Versuche es später noch einmal.',
identityRevealed: false
};
}
}
const server = http.createServer((req, res) => {
console.log(`${req.method} ${req.url}`);
// CORS-Header hinzufügen für Cross-Origin-Anfragen
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// OPTIONS-Anfragen für CORS-Preflight behandeln
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// API-Endpunkt für OpenAI-Anfragen
if (req.method === 'POST' && req.url === '/api/chat') {
collectRequestData(req, async (data) => {
try {
if (!data.message) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Nachricht fehlt' }));
return;
}
// Verwende die Konversationshistorie, wenn vorhanden
const conversationHistory = data.conversationHistory || [];
console.log('Erhaltene Konversationshistorie:', JSON.stringify(conversationHistory, null, 2));
// Extrahiere Charakterinformationen, wenn vorhanden
const characterName = data.characterName;
const characterPersonality = data.characterPersonality;
if (characterName) {
console.log(`NPC-Charakter in der Anfrage: ${characterName}`);
}
const response = await callOpenAI(data.message, conversationHistory, characterName, characterPersonality);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
response: response.text,
identityRevealed: response.identityRevealed
}));
} catch (error) {
console.error('Fehler bei der Verarbeitung der Chat-Anfrage:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Interner Serverfehler' }));
}
});
return;
}
// Statische Dateien behandeln
let filePath = '.' + req.url;
if (filePath === './') {
filePath = './index.html';
}
// Get the file extension
const extname = path.extname(filePath);
let contentType = MIME_TYPES[extname] || 'application/octet-stream';
// Read the file
fs.readFile(filePath, (error, content) => {
if (error) {
if (error.code === 'ENOENT') {
// Page not found
fs.readFile('./index.html', (err, content) => {
if (err) {
res.writeHead(500);
res.end('Error loading index.html');
} else {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(content, 'utf-8');
}
});
} else {
// Server error
res.writeHead(500);
res.end(`Server Error: ${error.code}`);
}
} else {
// Success
res.writeHead(200, { 'Content-Type': contentType });
res.end(content, 'utf-8');
}
});
});
server.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
console.log('Press Ctrl+C to stop the server');
console.log('Azure OpenAI API ist konfiguriert und bereit!');
});