fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
24
breakpilot-drive/Dockerfile
Normal file
24
breakpilot-drive/Dockerfile
Normal file
@@ -0,0 +1,24 @@
|
||||
# ==============================================
|
||||
# Breakpilot Drive - WebGL Game Container
|
||||
# ==============================================
|
||||
# Dient den Unity WebGL Build ueber Nginx
|
||||
# Unterstuetzt Gzip-Kompression fuer grosse WASM-Dateien
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
# WebGL-spezifische MIME-Types und Konfiguration
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# WebGL Build kopieren (wird vom Unity Build Process befuellt)
|
||||
# Im Entwicklungsmodus: Placeholder-Seite
|
||||
COPY Build/ /usr/share/nginx/html/Build/
|
||||
COPY TemplateData/ /usr/share/nginx/html/TemplateData/
|
||||
COPY index.html /usr/share/nginx/html/
|
||||
|
||||
# Health-Check Endpunkt
|
||||
RUN echo '{"status":"healthy","service":"breakpilot-drive"}' > /usr/share/nginx/html/health.json
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# Nginx im Vordergrund starten
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
377
breakpilot-drive/PREFAB_CONFIG.md
Normal file
377
breakpilot-drive/PREFAB_CONFIG.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# Breakpilot Drive - Prefab Konfiguration
|
||||
|
||||
Diese Anleitung beschreibt, wie die Prefabs fuer das Spiel konfiguriert werden.
|
||||
|
||||
---
|
||||
|
||||
## 1. Spieler-Auto (Player)
|
||||
|
||||
### Prefab erstellen
|
||||
|
||||
1. Aus Kenney Car Kit ein Auto waehlen
|
||||
2. In die Szene ziehen
|
||||
3. Umbenennen zu "PlayerCar"
|
||||
|
||||
### Komponenten
|
||||
|
||||
| Komponente | Einstellungen |
|
||||
|------------|---------------|
|
||||
| **Transform** | Position: (0, 0, 0), Scale: (1, 1, 1) |
|
||||
| **Rigidbody** | Use Gravity: false, Is Kinematic: true |
|
||||
| **Box Collider** | Is Trigger: false, Size an Auto anpassen |
|
||||
| **PlayerController** | Lane Distance: 3, Lane Switch Speed: 10 |
|
||||
|
||||
### Inspector-Einstellungen (PlayerController)
|
||||
|
||||
```
|
||||
Bewegung:
|
||||
Lane Distance: 3
|
||||
Lane Switch Speed: 10
|
||||
Forward Speed: 10
|
||||
|
||||
Spuren:
|
||||
Current Lane: 1 (Mitte)
|
||||
|
||||
Touch-Steuerung:
|
||||
Swipe Threshold: 50
|
||||
|
||||
Effekte:
|
||||
Crash Effect: (ParticleSystem zuweisen)
|
||||
Collect Effect: (ParticleSystem zuweisen)
|
||||
```
|
||||
|
||||
### Tag
|
||||
|
||||
- Tag: **Player**
|
||||
|
||||
### Speichern
|
||||
|
||||
- Aus Hierarchy in `Assets/Prefabs/Player/` ziehen
|
||||
- Name: `PlayerCar.prefab`
|
||||
|
||||
---
|
||||
|
||||
## 2. Strecken-Segment (Track)
|
||||
|
||||
### Prefab erstellen
|
||||
|
||||
1. Leeres GameObject: "TrackSegment"
|
||||
2. Boden erstellen:
|
||||
- 3D Object → Plane
|
||||
- Scale: (5, 1, 5) fuer 50m Laenge
|
||||
- Material: Strassen-Textur
|
||||
|
||||
### Struktur
|
||||
|
||||
```
|
||||
TrackSegment
|
||||
├── Ground (Plane mit Strassen-Material)
|
||||
├── LeftBorder (optional, Leitplanke)
|
||||
├── RightBorder (optional, Leitplanke)
|
||||
└── SpawnPoints (leeres GO fuer Hindernis-Positionen)
|
||||
```
|
||||
|
||||
### Speichern
|
||||
|
||||
- Mehrere Varianten erstellen (gerade, Kurve, etc.)
|
||||
- In `Assets/Prefabs/Track/` speichern
|
||||
|
||||
---
|
||||
|
||||
## 3. Hindernisse (Obstacles)
|
||||
|
||||
### Hindernis-Prefab erstellen
|
||||
|
||||
1. Aus Kenney Nature Kit: Stein, Baum, etc. waehlen
|
||||
2. In Szene ziehen
|
||||
3. Umbenennen (z.B. "Obstacle_Rock")
|
||||
|
||||
### Komponenten
|
||||
|
||||
| Komponente | Einstellungen |
|
||||
|------------|---------------|
|
||||
| **Box Collider** | Is Trigger: true, Size anpassen |
|
||||
|
||||
### Tag
|
||||
|
||||
- Tag: **Obstacle**
|
||||
|
||||
### Prefab-Varianten
|
||||
|
||||
| Name | Beschreibung | Groesse |
|
||||
|------|--------------|---------|
|
||||
| `Obstacle_Rock.prefab` | Kleiner Stein | 1 Spur |
|
||||
| `Obstacle_Tree.prefab` | Umgefallener Baum | 2 Spuren |
|
||||
| `Obstacle_Barrier.prefab` | Absperrung | 1-2 Spuren |
|
||||
| `Obstacle_Cone.prefab` | Verkehrskegel | 1 Spur |
|
||||
|
||||
### Speichern
|
||||
|
||||
- In `Assets/Prefabs/Obstacles/` speichern
|
||||
|
||||
---
|
||||
|
||||
## 4. Sammelbare Items
|
||||
|
||||
### Coin-Prefab
|
||||
|
||||
1. 3D Object → Cylinder (oder Kenney Asset)
|
||||
2. Scale: (0.5, 0.1, 0.5)
|
||||
3. Material: Gold
|
||||
|
||||
| Komponente | Einstellungen |
|
||||
|------------|---------------|
|
||||
| **Sphere Collider** | Is Trigger: true, Radius: 0.5 |
|
||||
| **Rotation Script** | (optional, fuer Dreh-Animation) |
|
||||
|
||||
- Tag: **Coin**
|
||||
- Prefab: `Assets/Prefabs/Items/Coin.prefab`
|
||||
|
||||
### Star-Prefab
|
||||
|
||||
1. Star-Modell (oder Kenney Asset)
|
||||
2. Groesser als Coin
|
||||
|
||||
| Komponente | Einstellungen |
|
||||
|------------|---------------|
|
||||
| **Sphere Collider** | Is Trigger: true, Radius: 0.7 |
|
||||
|
||||
- Tag: **Star**
|
||||
- Prefab: `Assets/Prefabs/Items/Star.prefab`
|
||||
|
||||
### Shield-Prefab
|
||||
|
||||
1. Shield-Modell
|
||||
2. Blauer Schimmer-Effekt
|
||||
|
||||
| Komponente | Einstellungen |
|
||||
|------------|---------------|
|
||||
| **Sphere Collider** | Is Trigger: true, Radius: 0.7 |
|
||||
|
||||
- Tag: **Shield**
|
||||
- Prefab: `Assets/Prefabs/Items/Shield.prefab`
|
||||
|
||||
---
|
||||
|
||||
## 5. Quiz-Trigger (Visual Triggers)
|
||||
|
||||
### Bridge-Prefab
|
||||
|
||||
1. Aus Kenney Assets oder selbst erstellen
|
||||
2. Muss ueber der Strasse sein (Y-Position erhoehen)
|
||||
|
||||
### Struktur
|
||||
|
||||
```
|
||||
Bridge
|
||||
├── BridgeModel (visuelles Modell)
|
||||
└── TriggerZone (leeres GO mit Collider)
|
||||
└── Box Collider (Is Trigger: true)
|
||||
└── VisualTrigger.cs (Trigger Type: "bridge")
|
||||
```
|
||||
|
||||
### Komponenten (TriggerZone)
|
||||
|
||||
| Komponente | Einstellungen |
|
||||
|------------|---------------|
|
||||
| **Box Collider** | Is Trigger: true, Size: (10, 5, 2) |
|
||||
| **VisualTrigger** | Trigger Type: "bridge" |
|
||||
|
||||
- Tag: **QuizTrigger**
|
||||
|
||||
### Weitere Trigger
|
||||
|
||||
| Prefab | Trigger Type | Beschreibung |
|
||||
|--------|--------------|--------------|
|
||||
| `Bridge.prefab` | "bridge" | Bruecke ueber Strasse |
|
||||
| `Tree.prefab` | "tree" | Grosser Baum am Rand |
|
||||
| `House.prefab` | "house" | Haus am Rand |
|
||||
| `Sign.prefab` | "sign" | Schild mit Bild |
|
||||
|
||||
### Speichern
|
||||
|
||||
- In `Assets/Prefabs/VisualTriggers/` speichern
|
||||
|
||||
---
|
||||
|
||||
## 6. Manager-Objekte
|
||||
|
||||
Diese werden nicht als Prefabs, sondern direkt in der Szene erstellt:
|
||||
|
||||
### GameManager
|
||||
|
||||
```
|
||||
GameObject: GameManager
|
||||
Scripts:
|
||||
- GameManager.cs
|
||||
|
||||
Inspector:
|
||||
Start Lives: 3
|
||||
Pause Question Interval: 500
|
||||
Game Over Panel: (UI Referenz)
|
||||
Pause Panel: (UI Referenz)
|
||||
```
|
||||
|
||||
### QuizManager
|
||||
|
||||
```
|
||||
GameObject: QuizManager
|
||||
Scripts:
|
||||
- QuizManager.cs
|
||||
|
||||
Inspector:
|
||||
Quick Quiz Panel: (UI Referenz)
|
||||
Pause Quiz Panel: (UI Referenz)
|
||||
Question Text: (TextMeshPro Referenz)
|
||||
Answer Buttons: [Button0, Button1, Button2, Button3]
|
||||
Timer Text: (TextMeshPro Referenz)
|
||||
Timer Slider: (Slider Referenz)
|
||||
Points Correct: 500
|
||||
Points Wrong: -100
|
||||
Pause Question Interval: 500
|
||||
```
|
||||
|
||||
### TrackGenerator
|
||||
|
||||
```
|
||||
GameObject: TrackGenerator
|
||||
Scripts:
|
||||
- TrackGenerator.cs
|
||||
|
||||
Inspector:
|
||||
Track Segment Prefabs: [Segment1, Segment2, ...]
|
||||
Start Segment Prefab: (Start-Segment)
|
||||
Segment Length: 50
|
||||
Visible Segments: 5
|
||||
Despawn Distance: -20
|
||||
Base Speed: 10
|
||||
```
|
||||
|
||||
### ObstacleSpawner
|
||||
|
||||
```
|
||||
GameObject: ObstacleSpawner
|
||||
Scripts:
|
||||
- ObstacleSpawner.cs
|
||||
|
||||
Inspector:
|
||||
Obstacle Prefabs: [Rock, Tree, Barrier, ...]
|
||||
Large Obstacle Prefabs: [LargeTree, ...]
|
||||
Coin Prefab: (Coin.prefab)
|
||||
Star Prefab: (Star.prefab)
|
||||
Shield Prefab: (Shield.prefab)
|
||||
Bridge Prefab: (Bridge.prefab)
|
||||
Tree Prefab: (Tree.prefab)
|
||||
House Prefab: (House.prefab)
|
||||
|
||||
Lane Distance: 3
|
||||
Min Obstacle Spacing: 10
|
||||
Max Obstacle Spacing: 30
|
||||
|
||||
Obstacle Chance: 0.6
|
||||
Coin Chance: 0.3
|
||||
Star Chance: 0.05
|
||||
Shield Chance: 0.02
|
||||
Quiz Trigger Chance: 0.1
|
||||
```
|
||||
|
||||
### BreakpilotAPI
|
||||
|
||||
```
|
||||
GameObject: BreakpilotAPI
|
||||
Scripts:
|
||||
- BreakpilotAPI.cs
|
||||
|
||||
Inspector:
|
||||
Base URL: http://localhost:8000/api/game
|
||||
Use Offline Cache: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. UI Canvas
|
||||
|
||||
### Canvas erstellen
|
||||
|
||||
1. Rechtsklick in Hierarchy → UI → Canvas
|
||||
2. Canvas Scaler:
|
||||
- UI Scale Mode: Scale With Screen Size
|
||||
- Reference Resolution: 1920 x 1080
|
||||
|
||||
### UI Struktur
|
||||
|
||||
```
|
||||
Canvas
|
||||
├── GameHUD
|
||||
│ ├── ScorePanel
|
||||
│ │ ├── ScoreIcon
|
||||
│ │ └── ScoreText (TextMeshPro)
|
||||
│ ├── LivesPanel
|
||||
│ │ ├── Heart1, Heart2, Heart3 (Images)
|
||||
│ │ └── LivesText (TextMeshPro)
|
||||
│ └── DistanceText (TextMeshPro)
|
||||
│
|
||||
├── QuickQuizPanel (anfangs deaktiviert)
|
||||
│ ├── QuestionText (TextMeshPro)
|
||||
│ ├── AnswerButtons
|
||||
│ │ ├── Button1, Button2, Button3
|
||||
│ ├── TimerSlider
|
||||
│ └── TimerText
|
||||
│
|
||||
├── PauseQuizPanel (anfangs deaktiviert)
|
||||
│ ├── Background (halbtransparent)
|
||||
│ ├── QuestionPanel
|
||||
│ │ ├── QuestionText
|
||||
│ │ ├── Button1, Button2, Button3, Button4
|
||||
│ │ └── HintText (optional)
|
||||
│
|
||||
├── GameOverPanel (anfangs deaktiviert)
|
||||
│ ├── GameOverText
|
||||
│ ├── FinalScoreText
|
||||
│ ├── RestartButton
|
||||
│ └── MainMenuButton
|
||||
│
|
||||
└── PausePanel (anfangs deaktiviert)
|
||||
├── PauseText
|
||||
├── ResumeButton
|
||||
└── MainMenuButton
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Checkliste
|
||||
|
||||
### Prefabs erstellt?
|
||||
|
||||
- [ ] PlayerCar.prefab
|
||||
- [ ] TrackSegment.prefab (mindestens 1)
|
||||
- [ ] Obstacle_Rock.prefab
|
||||
- [ ] Coin.prefab
|
||||
- [ ] Star.prefab
|
||||
- [ ] Shield.prefab
|
||||
- [ ] Bridge.prefab (Quiz-Trigger)
|
||||
|
||||
### Manager konfiguriert?
|
||||
|
||||
- [ ] GameManager - alle UI Referenzen gesetzt
|
||||
- [ ] QuizManager - alle UI Referenzen gesetzt
|
||||
- [ ] TrackGenerator - Prefabs zugewiesen
|
||||
- [ ] ObstacleSpawner - Prefabs zugewiesen
|
||||
- [ ] BreakpilotAPI - Base URL korrekt
|
||||
|
||||
### Tags erstellt?
|
||||
|
||||
- [ ] Player
|
||||
- [ ] Obstacle
|
||||
- [ ] Coin
|
||||
- [ ] Star
|
||||
- [ ] Shield
|
||||
- [ ] QuizTrigger
|
||||
|
||||
### UI Canvas?
|
||||
|
||||
- [ ] Canvas Scaler konfiguriert
|
||||
- [ ] GameHUD erstellt
|
||||
- [ ] QuickQuizPanel erstellt
|
||||
- [ ] PauseQuizPanel erstellt
|
||||
- [ ] GameOverPanel erstellt
|
||||
213
breakpilot-drive/README.md
Normal file
213
breakpilot-drive/README.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Breakpilot Drive - Unity Lernspiel
|
||||
|
||||
Ein Endless Runner Lernspiel fuer Kinder der Klassen 2-6 mit Quiz-Integration.
|
||||
|
||||
## Uebersicht
|
||||
|
||||
- **Video-Version**: Visuelles 3D-Spiel mit 3-Spur-Steuerung
|
||||
- **Audio-Version**: Barrierefreier Hoerspiel-Modus mit raeumlichem Audio
|
||||
- **Quiz-Integration**: Hybrid-Modus mit schnellen visuellen Fragen UND Denkpausen
|
||||
|
||||
## Schnellstart (Docker)
|
||||
|
||||
```bash
|
||||
# Game-Container starten (mit Backend)
|
||||
docker-compose --profile game up -d
|
||||
|
||||
# Nur das Backend starten (fuer API-Tests)
|
||||
docker-compose up backend -d
|
||||
|
||||
# Game oeffnen
|
||||
open http://localhost:3001
|
||||
```
|
||||
|
||||
## Unity Projekt erstellen
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Unity 2022.3 LTS oder neuer
|
||||
- WebGL Build Support Modul installiert
|
||||
|
||||
### Neues Projekt erstellen
|
||||
|
||||
```bash
|
||||
# Unity Hub oeffnen und neues 3D Projekt erstellen
|
||||
# Projektname: BreakpilotDrive
|
||||
# Speicherort: /path/to/breakpilot-pwa/breakpilot-drive/Unity/
|
||||
```
|
||||
|
||||
### Empfohlene Projektstruktur
|
||||
|
||||
```
|
||||
Unity/
|
||||
├── Assets/
|
||||
│ ├── Scripts/
|
||||
│ │ ├── Core/
|
||||
│ │ │ ├── GameManager.cs # Spielzustand, Score
|
||||
│ │ │ ├── DifficultyManager.cs # Schwierigkeitsanpassung
|
||||
│ │ │ └── AudioManager.cs # Sound-Verwaltung
|
||||
│ │ ├── Player/
|
||||
│ │ │ ├── PlayerController.cs # 3-Spur-Bewegung
|
||||
│ │ │ └── PlayerInput.cs # Touch/Keyboard Input
|
||||
│ │ ├── Track/
|
||||
│ │ │ ├── TrackGenerator.cs # Endlose Strecke
|
||||
│ │ │ ├── ObstacleSpawner.cs # Hindernisse
|
||||
│ │ │ └── VisualTrigger.cs # Quiz-Trigger (Bruecke, etc.)
|
||||
│ │ ├── Quiz/
|
||||
│ │ │ ├── QuizManager.cs # Quick/Pause-Modus
|
||||
│ │ │ ├── QuestionDisplay.cs # UI fuer Fragen
|
||||
│ │ │ └── QuizAudio.cs # TTS fuer Audio-Version
|
||||
│ │ ├── Network/
|
||||
│ │ │ ├── BreakpilotAPI.cs # REST API Client
|
||||
│ │ │ └── OfflineCache.cs # Lokaler Fragen-Cache
|
||||
│ │ └── UI/
|
||||
│ │ ├── MainMenu.cs # Startbildschirm
|
||||
│ │ ├── GameHUD.cs # Score, Leben
|
||||
│ │ └── QuizOverlay.cs # Frage-Anzeige
|
||||
│ ├── Prefabs/
|
||||
│ │ ├── Player/
|
||||
│ │ │ └── Car.prefab # Spieler-Auto
|
||||
│ │ ├── Obstacles/
|
||||
│ │ │ ├── Rock.prefab
|
||||
│ │ │ └── Barrier.prefab
|
||||
│ │ ├── Items/
|
||||
│ │ │ ├── Star.prefab
|
||||
│ │ │ ├── Coin.prefab
|
||||
│ │ │ └── Shield.prefab
|
||||
│ │ └── VisualTriggers/
|
||||
│ │ ├── Bridge.prefab # Loest Quick-Quiz aus
|
||||
│ │ ├── House.prefab
|
||||
│ │ └── Tree.prefab
|
||||
│ ├── Audio/
|
||||
│ │ ├── Music/
|
||||
│ │ │ └── GameLoop.mp3
|
||||
│ │ ├── SFX/
|
||||
│ │ │ ├── Coin.wav
|
||||
│ │ │ ├── Crash.wav
|
||||
│ │ │ └── Correct.wav
|
||||
│ │ └── Voice/
|
||||
│ │ └── (TTS-generiert)
|
||||
│ ├── Scenes/
|
||||
│ │ ├── MainMenu.unity
|
||||
│ │ ├── Game_Video.unity
|
||||
│ │ └── Game_Audio.unity
|
||||
│ └── WebGLTemplates/
|
||||
│ └── Breakpilot/
|
||||
│ └── index.html # Custom WebGL Template
|
||||
├── Packages/
|
||||
│ └── manifest.json
|
||||
└── ProjectSettings/
|
||||
└── ProjectSettings.asset
|
||||
```
|
||||
|
||||
## API-Endpunkte
|
||||
|
||||
Das Backend stellt folgende Endpunkte bereit:
|
||||
|
||||
### Lernniveau
|
||||
```
|
||||
GET /api/game/learning-level/{user_id}
|
||||
→ { overall_level: 1-5, math_level, german_level, english_level }
|
||||
```
|
||||
|
||||
### Schwierigkeit
|
||||
```
|
||||
GET /api/game/difficulty/{level}
|
||||
→ { lane_speed, obstacle_frequency, question_complexity, answer_time }
|
||||
```
|
||||
|
||||
### Quiz-Fragen
|
||||
```
|
||||
GET /api/game/quiz/questions?difficulty=3&count=10&mode=quick
|
||||
→ [{ id, question_text, options, correct_index, visual_trigger, time_limit_seconds }]
|
||||
|
||||
GET /api/game/quiz/questions?difficulty=3&count=5&mode=pause
|
||||
→ [{ id, question_text, options, correct_index }]
|
||||
```
|
||||
|
||||
### Visuelle Trigger
|
||||
```
|
||||
GET /api/game/quiz/visual-triggers
|
||||
→ [{ trigger: "bridge", question_count: 2, difficulties: [1,2] }]
|
||||
```
|
||||
|
||||
### Session speichern
|
||||
```
|
||||
POST /api/game/session
|
||||
{ user_id, game_mode, duration_seconds, score, questions_answered, questions_correct }
|
||||
→ { session_id, status, new_level? }
|
||||
```
|
||||
|
||||
## WebGL Build erstellen
|
||||
|
||||
1. Unity oeffnen → File → Build Settings
|
||||
2. Platform: WebGL auswaehlen
|
||||
3. Player Settings:
|
||||
- Compression Format: Gzip
|
||||
- Decompression Fallback: Aktiviert
|
||||
- WebGL Template: Breakpilot (custom)
|
||||
4. Build → Zielordner: `breakpilot-drive/Build/`
|
||||
|
||||
Nach dem Build:
|
||||
```bash
|
||||
# Docker Container neu bauen
|
||||
docker-compose --profile game build breakpilot-drive
|
||||
|
||||
# Container starten
|
||||
docker-compose --profile game up -d breakpilot-drive
|
||||
```
|
||||
|
||||
## Quiz-Modus Implementierung
|
||||
|
||||
### Quick Mode (waehrend der Fahrt)
|
||||
|
||||
```csharp
|
||||
// Wenn Spieler sich einem VisualTrigger naehert
|
||||
public class VisualTrigger : MonoBehaviour
|
||||
{
|
||||
public string triggerType; // "bridge", "tree", etc.
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
// Frage anzeigen, Spiel laeuft weiter
|
||||
QuizManager.Instance.ShowQuickQuestion(triggerType);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pause Mode (Spiel pausiert)
|
||||
|
||||
```csharp
|
||||
// Alle X Meter/Punkte eine Denkaufgabe
|
||||
public class QuizManager : MonoBehaviour
|
||||
{
|
||||
public void ShowPauseQuestion()
|
||||
{
|
||||
Time.timeScale = 0; // Spiel pausieren
|
||||
// Komplexe Frage anzeigen
|
||||
// Nach Antwort: Time.timeScale = 1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entwicklungs-Workflow
|
||||
|
||||
1. **Lokal entwickeln**: Unity Editor mit Play-Modus
|
||||
2. **API testen**: `docker-compose up backend -d`
|
||||
3. **WebGL testen**: Build erstellen, Container starten
|
||||
4. **Commit**: Git-Aenderungen committen (ohne Build-Artefakte)
|
||||
|
||||
## Bekannte Einschraenkungen
|
||||
|
||||
- Unity WebGL unterstuetzt kein natives Audio-Recording (fuer Sprachsteuerung)
|
||||
- Mobile Browser haben unterschiedliche Touch-Implementierungen
|
||||
- WASM-Dateien koennen gross werden (>50MB) - Kompression wichtig
|
||||
|
||||
## Ressourcen
|
||||
|
||||
- [Unity WebGL Doku](https://docs.unity3d.com/Manual/webgl.html)
|
||||
- [UnityWebRequest](https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.html)
|
||||
- [Breakpilot Drive Plan](../docs/breakpilot-drive-plan.md)
|
||||
2
breakpilot-drive/TemplateData/.gitkeep
Normal file
2
breakpilot-drive/TemplateData/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Placeholder fuer Unity WebGL Template Data
|
||||
# Dieser Ordner enthaelt Icons, Loading Bar, etc.
|
||||
137
breakpilot-drive/ThirdPartyNotices.md
Normal file
137
breakpilot-drive/ThirdPartyNotices.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Third Party Notices - Breakpilot Drive
|
||||
|
||||
Dieses Projekt verwendet folgende Open-Source-Komponenten und Assets.
|
||||
|
||||
---
|
||||
|
||||
## Assets
|
||||
|
||||
### Kenney Game Assets - Car Kit
|
||||
|
||||
- **Quelle**: https://kenney.nl/assets/car-kit
|
||||
- **Autor**: Kenney (www.kenney.nl)
|
||||
- **Lizenz**: CC0 1.0 Universal (Public Domain Dedication)
|
||||
- **Verwendung**: Fahrzeug-Modelle, Strassenelemente
|
||||
|
||||
```
|
||||
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
||||
|
||||
Die Person, die ein Werk mit dieser Deed verknuepft hat, hat dieses Werk
|
||||
der Oeffentlichkeit gewidmet, indem sie weltweit auf alle Rechte an dem
|
||||
Werk unter dem Urheberrecht verzichtet hat, einschliesslich aller verwandten
|
||||
und benachbarten Rechte, soweit dies gesetzlich zulaessig ist.
|
||||
|
||||
Sie koennen das Werk kopieren, veraendern, verbreiten und auffuehren,
|
||||
selbst zu kommerziellen Zwecken, ohne um Erlaubnis zu fragen.
|
||||
|
||||
https://creativecommons.org/publicdomain/zero/1.0/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Kenney Game Assets - Nature Kit
|
||||
|
||||
- **Quelle**: https://kenney.nl/assets/nature-kit
|
||||
- **Autor**: Kenney (www.kenney.nl)
|
||||
- **Lizenz**: CC0 1.0 Universal (Public Domain Dedication)
|
||||
- **Verwendung**: Baeume, Steine, Natur-Elemente (Hindernisse, Quiz-Trigger)
|
||||
|
||||
```
|
||||
CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
|
||||
|
||||
https://creativecommons.org/publicdomain/zero/1.0/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schriftarten
|
||||
|
||||
### Verwendete Schriftart (zu aktualisieren)
|
||||
|
||||
Falls Sie eine Google Font verwenden:
|
||||
|
||||
- **Quelle**: https://fonts.google.com/
|
||||
- **Lizenz**: Open Font License (OFL)
|
||||
- **Verwendung**: UI-Text
|
||||
|
||||
---
|
||||
|
||||
## Audio (zu aktualisieren)
|
||||
|
||||
Falls Sie Audio von Freesound.org verwenden:
|
||||
|
||||
### [Sound-Name]
|
||||
|
||||
- **Quelle**: https://freesound.org/people/[username]/sounds/[id]/
|
||||
- **Autor**: [Username]
|
||||
- **Lizenz**: CC0 oder CC-BY
|
||||
- **Verwendung**: [Beschreibung]
|
||||
|
||||
---
|
||||
|
||||
## Code
|
||||
|
||||
### Breakpilot Drive Scripts
|
||||
|
||||
- **Quelle**: Eigenentwicklung
|
||||
- **Lizenz**: MIT
|
||||
- **Copyright**: (c) 2024 Breakpilot
|
||||
|
||||
```
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Breakpilot
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unity Packages
|
||||
|
||||
### TextMeshPro
|
||||
|
||||
- **Quelle**: Unity Technologies
|
||||
- **Lizenz**: Unity Companion License
|
||||
- **Verwendung**: Text-Rendering fuer UI
|
||||
|
||||
---
|
||||
|
||||
## Hinweise
|
||||
|
||||
### Kenney.nl Assets
|
||||
|
||||
Alle Kenney Assets sind unter CC0 lizenziert und koennen:
|
||||
- Kommerziell verwendet werden
|
||||
- Modifiziert werden
|
||||
- Ohne Attribution verwendet werden (Attribution ist aber erwuenscht)
|
||||
|
||||
Empfohlene Attribution (optional aber nett):
|
||||
> Assets by Kenney (www.kenney.nl)
|
||||
|
||||
### Lizenz-Compliance
|
||||
|
||||
Bei Hinzufuegen neuer Assets:
|
||||
1. Lizenz pruefen (muss kommerziell nutzbar sein)
|
||||
2. Diese Datei aktualisieren
|
||||
3. NC (Non-Commercial) und ND (No-Derivatives) vermeiden
|
||||
|
||||
---
|
||||
|
||||
*Letzte Aktualisierung: Januar 2024*
|
||||
278
breakpilot-drive/UNITY_SETUP.md
Normal file
278
breakpilot-drive/UNITY_SETUP.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# Breakpilot Drive - Unity Projekt Setup
|
||||
|
||||
## Schritt-fuer-Schritt Anleitung
|
||||
|
||||
Diese Anleitung fuehrt Sie durch die Einrichtung des Unity-Projekts nach der Installation.
|
||||
|
||||
---
|
||||
|
||||
## 1. Neues Projekt erstellen
|
||||
|
||||
1. **Unity Hub** oeffnen
|
||||
2. **"Projects"** Tab waehlen
|
||||
3. **"New project"** klicken
|
||||
4. Einstellungen:
|
||||
- Template: **3D (Built-in Render Pipeline)**
|
||||
- Project name: **BreakpilotDrive**
|
||||
- Location: `/pfad/zu/breakpilot-pwa/breakpilot-drive/Unity/`
|
||||
5. **"Create project"** klicken
|
||||
|
||||
---
|
||||
|
||||
## 2. Ordnerstruktur erstellen
|
||||
|
||||
Nach dem Projekt-Start in Unity:
|
||||
|
||||
### 2.1 Im Project-Fenster (unten)
|
||||
|
||||
Rechtsklick → Create → Folder fuer jeden Ordner:
|
||||
|
||||
```
|
||||
Assets/
|
||||
├── Scripts/
|
||||
│ ├── Core/
|
||||
│ ├── Player/
|
||||
│ ├── Track/
|
||||
│ ├── Quiz/
|
||||
│ ├── Network/
|
||||
│ └── UI/
|
||||
├── Prefabs/
|
||||
│ ├── Player/
|
||||
│ ├── Obstacles/
|
||||
│ ├── Items/
|
||||
│ └── VisualTriggers/
|
||||
├── Audio/
|
||||
│ ├── Music/
|
||||
│ ├── SFX/
|
||||
│ └── Voice/
|
||||
├── Scenes/
|
||||
├── Materials/
|
||||
├── Plugins/
|
||||
│ └── WebGL/
|
||||
├── ThirdParty/
|
||||
│ └── Kenney/
|
||||
└── WebGLTemplates/
|
||||
└── Breakpilot/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Scripts importieren
|
||||
|
||||
### 3.1 Im Finder/Explorer
|
||||
|
||||
1. Oeffnen Sie: `breakpilot-pwa/breakpilot-drive/UnityScripts/`
|
||||
2. Kopieren Sie die Dateien in die entsprechenden Unity-Ordner:
|
||||
|
||||
| Quelle | Ziel |
|
||||
|--------|------|
|
||||
| `UnityScripts/Core/*.cs` | `Assets/Scripts/Core/` |
|
||||
| `UnityScripts/Player/*.cs` | `Assets/Scripts/Player/` |
|
||||
| `UnityScripts/Track/*.cs` | `Assets/Scripts/Track/` |
|
||||
| `UnityScripts/Quiz/*.cs` | `Assets/Scripts/Quiz/` |
|
||||
| `UnityScripts/UI/*.cs` | `Assets/Scripts/UI/` |
|
||||
| `UnityScripts/BreakpilotAPI.cs` | `Assets/Scripts/Network/` |
|
||||
| `UnityScripts/QuizManager.cs` | `Assets/Scripts/Quiz/` |
|
||||
| `UnityScripts/Plugins/*.jslib` | `Assets/Plugins/WebGL/` |
|
||||
| `UnityScripts/WebGLTemplate/Breakpilot/*` | `Assets/WebGLTemplates/Breakpilot/` |
|
||||
|
||||
### 3.2 In Unity
|
||||
|
||||
Nach dem Kopieren:
|
||||
1. Zurueck zu Unity wechseln
|
||||
2. Unity importiert die Dateien automatisch
|
||||
3. Falls Fehler erscheinen: Console pruefen (Window → General → Console)
|
||||
|
||||
---
|
||||
|
||||
## 4. Kenney Assets importieren
|
||||
|
||||
### 4.1 Car Kit
|
||||
|
||||
1. Finden Sie die heruntergeladene ZIP-Datei (`kenney_car-kit.zip`)
|
||||
2. Entpacken Sie sie
|
||||
3. In Unity: **Assets → Import Package → Custom Package**
|
||||
4. Oder: Ziehen Sie die entpackten Dateien in `Assets/ThirdParty/Kenney/CarKit/`
|
||||
|
||||
### 4.2 Nature Kit
|
||||
|
||||
1. Finden Sie `kenney_nature-kit.zip`
|
||||
2. Entpacken Sie sie
|
||||
3. Ziehen Sie die Dateien in `Assets/ThirdParty/Kenney/NatureKit/`
|
||||
|
||||
---
|
||||
|
||||
## 5. TextMeshPro installieren
|
||||
|
||||
1. **Window → TextMeshPro → Import TMP Essential Resources**
|
||||
2. Im Dialog: **"Import"** klicken
|
||||
3. Warten bis der Import abgeschlossen ist
|
||||
|
||||
---
|
||||
|
||||
## 6. Szenen erstellen
|
||||
|
||||
### 6.1 MainMenu Szene
|
||||
|
||||
1. **File → New Scene**
|
||||
2. **File → Save As** → `Assets/Scenes/MainMenu.unity`
|
||||
|
||||
### 6.2 Game_Video Szene
|
||||
|
||||
1. **File → New Scene**
|
||||
2. **File → Save As** → `Assets/Scenes/Game_Video.unity`
|
||||
|
||||
### 6.3 Game_Audio Szene (optional fuer spaeter)
|
||||
|
||||
1. **File → New Scene**
|
||||
2. **File → Save As** → `Assets/Scenes/Game_Audio.unity`
|
||||
|
||||
---
|
||||
|
||||
## 7. Manager-Objekte erstellen
|
||||
|
||||
### 7.1 In Game_Video Szene
|
||||
|
||||
1. Oeffnen Sie `Game_Video.unity`
|
||||
2. Erstellen Sie leere GameObjects (Rechtsklick in Hierarchy → Create Empty):
|
||||
|
||||
| Name | Scripts hinzufuegen |
|
||||
|------|---------------------|
|
||||
| **GameManager** | GameManager.cs |
|
||||
| **DifficultyManager** | DifficultyManager.cs |
|
||||
| **AudioManager** | AudioManager.cs |
|
||||
| **BreakpilotAPI** | BreakpilotAPI.cs |
|
||||
| **QuizManager** | QuizManager.cs |
|
||||
| **TrackGenerator** | TrackGenerator.cs |
|
||||
| **ObstacleSpawner** | ObstacleSpawner.cs |
|
||||
|
||||
### 7.2 Script hinzufuegen
|
||||
|
||||
1. GameObject auswaehlen
|
||||
2. Im Inspector: **Add Component**
|
||||
3. Script-Namen eingeben (z.B. "GameManager")
|
||||
4. Script auswaehlen
|
||||
|
||||
---
|
||||
|
||||
## 8. Spieler erstellen
|
||||
|
||||
### 8.1 Auto-Prefab
|
||||
|
||||
1. In `Assets/ThirdParty/Kenney/CarKit/` ein Auto-Modell finden
|
||||
2. In die Szene ziehen
|
||||
3. Komponenten hinzufuegen:
|
||||
- **Rigidbody** (Add Component → Physics → Rigidbody)
|
||||
- **Box Collider** (Add Component → Physics → Box Collider)
|
||||
- **PlayerController.cs** (Add Component → Scripts → Player)
|
||||
4. Tag setzen: Im Inspector → Tag → **"Player"** (falls nicht vorhanden: Add Tag)
|
||||
5. Als Prefab speichern: Aus Hierarchy in `Assets/Prefabs/Player/` ziehen
|
||||
|
||||
---
|
||||
|
||||
## 9. Tags erstellen
|
||||
|
||||
1. **Edit → Project Settings → Tags and Layers**
|
||||
2. Tags hinzufuegen:
|
||||
- `Player`
|
||||
- `Obstacle`
|
||||
- `Coin`
|
||||
- `Star`
|
||||
- `Shield`
|
||||
- `QuizTrigger`
|
||||
|
||||
---
|
||||
|
||||
## 10. Build Settings
|
||||
|
||||
### 10.1 WebGL Platform
|
||||
|
||||
1. **File → Build Settings**
|
||||
2. **WebGL** auswaehlen
|
||||
3. **"Switch Platform"** klicken (kann einige Minuten dauern)
|
||||
|
||||
### 10.2 Szenen hinzufuegen
|
||||
|
||||
1. Im Build Settings Fenster
|
||||
2. **"Add Open Scenes"** klicken fuer jede Szene
|
||||
3. Reihenfolge: MainMenu (Index 0), Game_Video (Index 1)
|
||||
|
||||
### 10.3 Player Settings
|
||||
|
||||
1. Im Build Settings: **"Player Settings"** klicken
|
||||
2. Einstellungen:
|
||||
|
||||
```
|
||||
Company Name: Breakpilot
|
||||
Product Name: Breakpilot Drive
|
||||
Version: 0.1.0
|
||||
|
||||
Resolution and Presentation:
|
||||
WebGL Template: Breakpilot (falls verfuegbar)
|
||||
Run in Background: true
|
||||
|
||||
Publishing Settings:
|
||||
Compression Format: Gzip
|
||||
Decompression Fallback: true
|
||||
Data Caching: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Erster Test
|
||||
|
||||
### 11.1 Im Editor
|
||||
|
||||
1. Game_Video Szene oeffnen
|
||||
2. **Play-Button** druecken
|
||||
3. Console auf Fehler pruefen
|
||||
|
||||
### 11.2 Haeufige Fehler
|
||||
|
||||
| Fehler | Loesung |
|
||||
|--------|---------|
|
||||
| "Script can't be loaded" | Script-Datei pruefen, Namespace korrekt? |
|
||||
| "Missing Reference" | Inspector pruefen, Referenzen zuweisen |
|
||||
| "Tag not found" | Tags in Project Settings erstellen |
|
||||
| "TMP not found" | TextMeshPro importieren |
|
||||
|
||||
---
|
||||
|
||||
## 12. WebGL Build
|
||||
|
||||
Erst nach erfolgreichem Editor-Test:
|
||||
|
||||
1. **File → Build Settings**
|
||||
2. **"Build"** klicken
|
||||
3. Zielordner: `breakpilot-pwa/breakpilot-drive/Build/`
|
||||
4. Warten (kann 5-10 Minuten dauern)
|
||||
|
||||
Nach dem Build:
|
||||
```bash
|
||||
cd breakpilot-pwa
|
||||
docker-compose --profile game build breakpilot-drive
|
||||
docker-compose --profile game up -d
|
||||
open http://localhost:3001
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naechste Schritte
|
||||
|
||||
Nach dem Setup:
|
||||
|
||||
1. [ ] Strecken-Prefabs erstellen (aus Kenney Assets)
|
||||
2. [ ] Hindernis-Prefabs erstellen
|
||||
3. [ ] Quiz-Trigger-Prefabs erstellen
|
||||
4. [ ] UI Canvas einrichten
|
||||
5. [ ] Audio-Clips zuweisen
|
||||
6. [ ] API-URL konfigurieren
|
||||
|
||||
---
|
||||
|
||||
## Hilfe
|
||||
|
||||
Bei Problemen:
|
||||
- Unity Console pruefen (Window → General → Console)
|
||||
- Dokumentation lesen: `docs/breakpilot-drive/`
|
||||
- API testen: `curl http://localhost:8000/api/game/health`
|
||||
599
breakpilot-drive/UnityScripts/BreakpilotAPI.cs
Normal file
599
breakpilot-drive/UnityScripts/BreakpilotAPI.cs
Normal file
@@ -0,0 +1,599 @@
|
||||
// ==============================================
|
||||
// BreakpilotAPI.cs - Unity API Client
|
||||
// ==============================================
|
||||
// Kopiere diese Datei nach Assets/Scripts/Network/
|
||||
// um mit dem Breakpilot Backend zu kommunizieren.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
[Serializable]
|
||||
public class LearningLevel
|
||||
{
|
||||
public string user_id;
|
||||
public int overall_level;
|
||||
public float math_level;
|
||||
public float german_level;
|
||||
public float english_level;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GameDifficulty
|
||||
{
|
||||
public float lane_speed;
|
||||
public float obstacle_frequency;
|
||||
public float power_up_chance;
|
||||
public int question_complexity;
|
||||
public int answer_time;
|
||||
public bool hints_enabled;
|
||||
public float speech_speed;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class QuizQuestion
|
||||
{
|
||||
public string id;
|
||||
public string question_text;
|
||||
public string audio_url;
|
||||
public string[] options;
|
||||
public int correct_index;
|
||||
public int difficulty;
|
||||
public string subject;
|
||||
public int grade_level;
|
||||
public string quiz_mode; // "quick" oder "pause"
|
||||
public string visual_trigger; // z.B. "bridge"
|
||||
public float time_limit_seconds;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class QuizQuestionList
|
||||
{
|
||||
public QuizQuestion[] questions;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class GameSession
|
||||
{
|
||||
public string user_id;
|
||||
public string game_mode; // "video" oder "audio"
|
||||
public int duration_seconds;
|
||||
public float distance_traveled;
|
||||
public int score;
|
||||
public int questions_answered;
|
||||
public int questions_correct;
|
||||
public int difficulty_level;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class SessionResponse
|
||||
{
|
||||
public string session_id;
|
||||
public string status;
|
||||
public int? new_level;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class VisualTrigger
|
||||
{
|
||||
public string trigger;
|
||||
public int question_count;
|
||||
public int[] difficulties;
|
||||
public string[] subjects;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ProgressData
|
||||
{
|
||||
public string date;
|
||||
public int sessions;
|
||||
public int total_score;
|
||||
public int questions;
|
||||
public int correct;
|
||||
public float accuracy;
|
||||
public float avg_difficulty;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class ProgressResponse
|
||||
{
|
||||
public string user_id;
|
||||
public int days;
|
||||
public int data_points;
|
||||
public ProgressData[] progress;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class LeaderboardEntry
|
||||
{
|
||||
public int rank;
|
||||
public string user_id;
|
||||
public int total_score;
|
||||
public string display_name;
|
||||
}
|
||||
|
||||
public class BreakpilotAPI : MonoBehaviour
|
||||
{
|
||||
// Singleton Pattern
|
||||
public static BreakpilotAPI Instance { get; private set; }
|
||||
|
||||
[Header("API Konfiguration")]
|
||||
[SerializeField] private string baseUrl = "http://localhost:8000/api/game";
|
||||
[SerializeField] private bool useOfflineCache = true;
|
||||
|
||||
// Cached Data
|
||||
private LearningLevel cachedLevel;
|
||||
private GameDifficulty cachedDifficulty;
|
||||
private List<QuizQuestion> cachedQuestions = new List<QuizQuestion>();
|
||||
private List<VisualTrigger> cachedTriggers = new List<VisualTrigger>();
|
||||
|
||||
// API URL property (for other scripts)
|
||||
public string BaseUrl => baseUrl;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Helper: Add Auth Header to Request
|
||||
// ==============================================
|
||||
private void AddAuthHeader(UnityWebRequest request)
|
||||
{
|
||||
if (AuthManager.Instance != null)
|
||||
{
|
||||
string authHeader = AuthManager.Instance.GetAuthHeader();
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
request.SetRequestHeader("Authorization", authHeader);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current user ID (from auth or fallback)
|
||||
public string GetCurrentUserId()
|
||||
{
|
||||
if (AuthManager.Instance != null && AuthManager.Instance.IsAuthenticated)
|
||||
{
|
||||
return AuthManager.Instance.UserId;
|
||||
}
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Lernniveau abrufen
|
||||
// ==============================================
|
||||
public IEnumerator GetLearningLevel(string userId, Action<LearningLevel> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/learning-level/{userId}";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
LearningLevel level = JsonUtility.FromJson<LearningLevel>(request.downloadHandler.text);
|
||||
cachedLevel = level;
|
||||
onSuccess?.Invoke(level);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"API Fehler: {request.error}");
|
||||
|
||||
// Offline-Fallback
|
||||
if (useOfflineCache && cachedLevel != null)
|
||||
{
|
||||
onSuccess?.Invoke(cachedLevel);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard-Level
|
||||
onSuccess?.Invoke(new LearningLevel { user_id = userId, overall_level = 3 });
|
||||
}
|
||||
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Schwierigkeit abrufen
|
||||
// ==============================================
|
||||
public IEnumerator GetDifficulty(int level, Action<GameDifficulty> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/difficulty/{level}";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
GameDifficulty difficulty = JsonUtility.FromJson<GameDifficulty>(request.downloadHandler.text);
|
||||
cachedDifficulty = difficulty;
|
||||
onSuccess?.Invoke(difficulty);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"API Fehler: {request.error}");
|
||||
|
||||
// Fallback: Default-Schwierigkeit
|
||||
if (cachedDifficulty != null)
|
||||
{
|
||||
onSuccess?.Invoke(cachedDifficulty);
|
||||
}
|
||||
else
|
||||
{
|
||||
onSuccess?.Invoke(GetDefaultDifficulty(level));
|
||||
}
|
||||
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Quiz-Fragen abrufen
|
||||
// ==============================================
|
||||
public IEnumerator GetQuizQuestions(
|
||||
int difficulty,
|
||||
int count,
|
||||
string mode = null, // "quick", "pause", oder null fuer beide
|
||||
string subject = null,
|
||||
Action<QuizQuestion[]> onSuccess = null,
|
||||
Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/quiz/questions?difficulty={difficulty}&count={count}";
|
||||
if (!string.IsNullOrEmpty(mode)) url += $"&mode={mode}";
|
||||
if (!string.IsNullOrEmpty(subject)) url += $"&subject={subject}";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
// Unity's JsonUtility braucht Wrapper fuer Arrays
|
||||
string wrappedJson = "{\"questions\":" + request.downloadHandler.text + "}";
|
||||
QuizQuestionList list = JsonUtility.FromJson<QuizQuestionList>(wrappedJson);
|
||||
|
||||
// Cache updaten
|
||||
if (list.questions != null)
|
||||
{
|
||||
cachedQuestions.AddRange(list.questions);
|
||||
}
|
||||
|
||||
onSuccess?.Invoke(list.questions);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"API Fehler: {request.error}");
|
||||
|
||||
// Offline-Fallback
|
||||
if (useOfflineCache && cachedQuestions.Count > 0)
|
||||
{
|
||||
var filtered = cachedQuestions.FindAll(q =>
|
||||
Math.Abs(q.difficulty - difficulty) <= 1 &&
|
||||
(string.IsNullOrEmpty(mode) || q.quiz_mode == mode)
|
||||
);
|
||||
onSuccess?.Invoke(filtered.GetRange(0, Math.Min(count, filtered.Count)).ToArray());
|
||||
}
|
||||
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Visuelle Trigger abrufen
|
||||
// ==============================================
|
||||
public IEnumerator GetVisualTriggers(Action<VisualTrigger[]> onSuccess, Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/quiz/visual-triggers";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
// Parse Array
|
||||
string wrappedJson = "{\"triggers\":" + request.downloadHandler.text + "}";
|
||||
VisualTriggersWrapper wrapper = JsonUtility.FromJson<VisualTriggersWrapper>(wrappedJson);
|
||||
cachedTriggers = new List<VisualTrigger>(wrapper.triggers);
|
||||
onSuccess?.Invoke(wrapper.triggers);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"API Fehler: {request.error}");
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class VisualTriggersWrapper
|
||||
{
|
||||
public VisualTrigger[] triggers;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spielsession speichern
|
||||
// ==============================================
|
||||
public IEnumerator SaveGameSession(GameSession session, Action<SessionResponse> onSuccess = null, Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/session";
|
||||
string json = JsonUtility.ToJson(session);
|
||||
|
||||
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
|
||||
{
|
||||
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
|
||||
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
request.SetRequestHeader("Content-Type", "application/json");
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
SessionResponse response = JsonUtility.FromJson<SessionResponse>(request.downloadHandler.text);
|
||||
onSuccess?.Invoke(response);
|
||||
|
||||
// Bei Level-Aenderung cachedLevel updaten
|
||||
if (response.new_level.HasValue && cachedLevel != null)
|
||||
{
|
||||
cachedLevel.overall_level = response.new_level.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Session speichern fehlgeschlagen: {request.error}");
|
||||
|
||||
// Offline: Spaeter synchronisieren
|
||||
if (useOfflineCache)
|
||||
{
|
||||
QueueOfflineSession(session);
|
||||
}
|
||||
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Offline-Handling
|
||||
// ==============================================
|
||||
private List<GameSession> offlineSessions = new List<GameSession>();
|
||||
|
||||
private void QueueOfflineSession(GameSession session)
|
||||
{
|
||||
offlineSessions.Add(session);
|
||||
// Spaeter: PlayerPrefs oder lokale DB nutzen
|
||||
Debug.Log($"Session zur Offline-Queue hinzugefuegt. Queue-Groesse: {offlineSessions.Count}");
|
||||
}
|
||||
|
||||
public IEnumerator SyncOfflineSessions()
|
||||
{
|
||||
var sessionsToSync = new List<GameSession>(offlineSessions);
|
||||
offlineSessions.Clear();
|
||||
|
||||
foreach (var session in sessionsToSync)
|
||||
{
|
||||
yield return SaveGameSession(session,
|
||||
onSuccess: (response) => Debug.Log($"Offline-Session synchronisiert: {response.session_id}"),
|
||||
onError: (error) => offlineSessions.Add(session) // Zurueck in Queue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Helper
|
||||
// ==============================================
|
||||
private GameDifficulty GetDefaultDifficulty(int level)
|
||||
{
|
||||
// Fallback-Werte wenn API nicht erreichbar
|
||||
return new GameDifficulty
|
||||
{
|
||||
lane_speed = 3f + level,
|
||||
obstacle_frequency = 0.3f + (level * 0.1f),
|
||||
power_up_chance = 0.4f - (level * 0.05f),
|
||||
question_complexity = level,
|
||||
answer_time = 15 - (level * 2),
|
||||
hints_enabled = level <= 3,
|
||||
speech_speed = 0.8f + (level * 0.1f)
|
||||
};
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Frage fuer visuellen Trigger holen
|
||||
// ==============================================
|
||||
public QuizQuestion GetQuestionForTrigger(string triggerType, int difficulty)
|
||||
{
|
||||
var matching = cachedQuestions.FindAll(q =>
|
||||
q.quiz_mode == "quick" &&
|
||||
q.visual_trigger == triggerType &&
|
||||
Math.Abs(q.difficulty - difficulty) <= 1
|
||||
);
|
||||
|
||||
if (matching.Count > 0)
|
||||
{
|
||||
return matching[UnityEngine.Random.Range(0, matching.Count)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Cache-Zugriff
|
||||
// ==============================================
|
||||
public LearningLevel GetCachedLevel()
|
||||
{
|
||||
return cachedLevel;
|
||||
}
|
||||
|
||||
public List<QuizQuestion> GetCachedQuestions()
|
||||
{
|
||||
return cachedQuestions;
|
||||
}
|
||||
|
||||
public GameDifficulty GetCachedDifficulty()
|
||||
{
|
||||
return cachedDifficulty;
|
||||
}
|
||||
|
||||
public int GetCachedQuestionsCount()
|
||||
{
|
||||
return cachedQuestions.Count;
|
||||
}
|
||||
|
||||
public void ClearQuestionCache()
|
||||
{
|
||||
cachedQuestions.Clear();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Progress API (Phase 5)
|
||||
// ==============================================
|
||||
public IEnumerator GetProgress(
|
||||
string userId,
|
||||
int days,
|
||||
Action<ProgressResponse> onSuccess,
|
||||
Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/progress/{userId}?days={days}";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
AddAuthHeader(request);
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
var response = JsonUtility.FromJson<ProgressResponse>(request.downloadHandler.text);
|
||||
onSuccess?.Invoke(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Progress API error: {request.error}");
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Leaderboard API (Phase 5)
|
||||
// ==============================================
|
||||
public IEnumerator GetLeaderboard(
|
||||
string timeframe,
|
||||
int limit,
|
||||
bool anonymize,
|
||||
Action<LeaderboardEntry[]> onSuccess,
|
||||
Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/leaderboard/display?timeframe={timeframe}&limit={limit}&anonymize={anonymize.ToString().ToLower()}";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
string wrappedJson = "{\"entries\":" + request.downloadHandler.text + "}";
|
||||
var wrapper = JsonUtility.FromJson<LeaderboardWrapper>(wrappedJson);
|
||||
onSuccess?.Invoke(wrapper.entries);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Leaderboard API error: {request.error}");
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class LeaderboardWrapper
|
||||
{
|
||||
public LeaderboardEntry[] entries;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Stats API
|
||||
// ==============================================
|
||||
public IEnumerator GetUserStats(
|
||||
string userId,
|
||||
Action<UserStats> onSuccess,
|
||||
Action<string> onError = null)
|
||||
{
|
||||
string url = $"{baseUrl}/stats/{userId}";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
AddAuthHeader(request);
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
var stats = JsonUtility.FromJson<UserStats>(request.downloadHandler.text);
|
||||
onSuccess?.Invoke(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Stats API error: {request.error}");
|
||||
onError?.Invoke(request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class UserStats
|
||||
{
|
||||
public string user_id;
|
||||
public int overall_level;
|
||||
public float math_level;
|
||||
public float german_level;
|
||||
public float english_level;
|
||||
public int total_play_time_minutes;
|
||||
public int total_sessions;
|
||||
public int questions_answered;
|
||||
public int questions_correct;
|
||||
public float accuracy;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Health Check
|
||||
// ==============================================
|
||||
public IEnumerator HealthCheck(Action<bool, string> onComplete)
|
||||
{
|
||||
string url = $"{baseUrl}/health";
|
||||
|
||||
using (UnityWebRequest request = UnityWebRequest.Get(url))
|
||||
{
|
||||
request.timeout = 5;
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityWebRequest.Result.Success)
|
||||
{
|
||||
onComplete?.Invoke(true, request.downloadHandler.text);
|
||||
}
|
||||
else
|
||||
{
|
||||
onComplete?.Invoke(false, request.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
348
breakpilot-drive/UnityScripts/Core/AchievementManager.cs
Normal file
348
breakpilot-drive/UnityScripts/Core/AchievementManager.cs
Normal file
@@ -0,0 +1,348 @@
|
||||
// ==============================================
|
||||
// AchievementManager.cs - Achievement System
|
||||
// ==============================================
|
||||
// Handles achievement display and notifications.
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
[Serializable]
|
||||
public class Achievement
|
||||
{
|
||||
public string id;
|
||||
public string name;
|
||||
public string description;
|
||||
public string icon;
|
||||
public string category;
|
||||
public int threshold;
|
||||
public int progress;
|
||||
public bool unlocked;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class AchievementResponse
|
||||
{
|
||||
public string user_id;
|
||||
public int total;
|
||||
public int unlocked_count;
|
||||
public Achievement[] achievements;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class AchievementArrayWrapper
|
||||
{
|
||||
public Achievement[] achievements;
|
||||
}
|
||||
|
||||
public class AchievementManager : MonoBehaviour
|
||||
{
|
||||
public static AchievementManager Instance { get; private set; }
|
||||
|
||||
[Header("UI References")]
|
||||
[SerializeField] private GameObject achievementPopupPrefab;
|
||||
[SerializeField] private Transform popupContainer;
|
||||
[SerializeField] private float popupDuration = 3f;
|
||||
|
||||
[Header("Audio")]
|
||||
[SerializeField] private AudioClip unlockSound;
|
||||
|
||||
// Events
|
||||
public UnityEvent<Achievement> OnAchievementUnlocked;
|
||||
|
||||
// Cached data
|
||||
private List<Achievement> achievements = new List<Achievement>();
|
||||
private HashSet<string> previouslyUnlocked = new HashSet<string>();
|
||||
private Queue<Achievement> popupQueue = new Queue<Achievement>();
|
||||
private bool isShowingPopup = false;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Load Achievements from API
|
||||
// ==============================================
|
||||
|
||||
public IEnumerator LoadAchievements(string userId, Action<AchievementResponse> onComplete = null)
|
||||
{
|
||||
string url = $"{GetBaseUrl()}/achievements/{userId}";
|
||||
|
||||
using (var request = UnityEngine.Networking.UnityWebRequest.Get(url))
|
||||
{
|
||||
// Add auth header if available
|
||||
string authHeader = AuthManager.Instance?.GetAuthHeader();
|
||||
if (!string.IsNullOrEmpty(authHeader))
|
||||
{
|
||||
request.SetRequestHeader("Authorization", authHeader);
|
||||
}
|
||||
|
||||
yield return request.SendWebRequest();
|
||||
|
||||
if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
|
||||
{
|
||||
var response = JsonUtility.FromJson<AchievementResponse>(request.downloadHandler.text);
|
||||
|
||||
// Store previously unlocked for comparison
|
||||
previouslyUnlocked.Clear();
|
||||
foreach (var a in achievements)
|
||||
{
|
||||
if (a.unlocked)
|
||||
previouslyUnlocked.Add(a.id);
|
||||
}
|
||||
|
||||
// Update achievements
|
||||
achievements.Clear();
|
||||
if (response.achievements != null)
|
||||
{
|
||||
achievements.AddRange(response.achievements);
|
||||
}
|
||||
|
||||
onComplete?.Invoke(response);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Failed to load achievements: {request.error}");
|
||||
onComplete?.Invoke(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Check for New Achievements
|
||||
// ==============================================
|
||||
|
||||
public void CheckForNewUnlocks()
|
||||
{
|
||||
foreach (var achievement in achievements)
|
||||
{
|
||||
if (achievement.unlocked && !previouslyUnlocked.Contains(achievement.id))
|
||||
{
|
||||
// New unlock!
|
||||
ShowAchievementPopup(achievement);
|
||||
OnAchievementUnlocked?.Invoke(achievement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call after each game session to check for new achievements
|
||||
public IEnumerator RefreshAndCheckAchievements(string userId)
|
||||
{
|
||||
yield return LoadAchievements(userId, (response) =>
|
||||
{
|
||||
if (response != null)
|
||||
{
|
||||
CheckForNewUnlocks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Achievement Popup
|
||||
// ==============================================
|
||||
|
||||
public void ShowAchievementPopup(Achievement achievement)
|
||||
{
|
||||
popupQueue.Enqueue(achievement);
|
||||
|
||||
if (!isShowingPopup)
|
||||
{
|
||||
StartCoroutine(ProcessPopupQueue());
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator ProcessPopupQueue()
|
||||
{
|
||||
isShowingPopup = true;
|
||||
|
||||
while (popupQueue.Count > 0)
|
||||
{
|
||||
var achievement = popupQueue.Dequeue();
|
||||
yield return ShowSinglePopup(achievement);
|
||||
yield return new WaitForSeconds(0.3f); // Gap between popups
|
||||
}
|
||||
|
||||
isShowingPopup = false;
|
||||
}
|
||||
|
||||
private IEnumerator ShowSinglePopup(Achievement achievement)
|
||||
{
|
||||
Debug.Log($"[Achievement] Unlocked: {achievement.name}");
|
||||
|
||||
// Play sound
|
||||
if (unlockSound != null && AudioManager.Instance != null)
|
||||
{
|
||||
AudioManager.Instance.PlaySFX(unlockSound);
|
||||
}
|
||||
|
||||
// Create popup UI
|
||||
if (achievementPopupPrefab != null && popupContainer != null)
|
||||
{
|
||||
var popup = Instantiate(achievementPopupPrefab, popupContainer);
|
||||
var popupScript = popup.GetComponent<AchievementPopup>();
|
||||
|
||||
if (popupScript != null)
|
||||
{
|
||||
popupScript.Setup(achievement);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(popupDuration);
|
||||
|
||||
if (popup != null)
|
||||
{
|
||||
Destroy(popup);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: just wait
|
||||
yield return new WaitForSeconds(popupDuration);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Public API
|
||||
// ==============================================
|
||||
|
||||
public List<Achievement> GetAllAchievements()
|
||||
{
|
||||
return new List<Achievement>(achievements);
|
||||
}
|
||||
|
||||
public List<Achievement> GetUnlockedAchievements()
|
||||
{
|
||||
return achievements.FindAll(a => a.unlocked);
|
||||
}
|
||||
|
||||
public List<Achievement> GetLockedAchievements()
|
||||
{
|
||||
return achievements.FindAll(a => !a.unlocked);
|
||||
}
|
||||
|
||||
public List<Achievement> GetAchievementsByCategory(string category)
|
||||
{
|
||||
return achievements.FindAll(a => a.category == category);
|
||||
}
|
||||
|
||||
public int GetUnlockedCount()
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var a in achievements)
|
||||
{
|
||||
if (a.unlocked) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public int GetTotalCount()
|
||||
{
|
||||
return achievements.Count;
|
||||
}
|
||||
|
||||
public float GetCompletionPercentage()
|
||||
{
|
||||
if (achievements.Count == 0) return 0f;
|
||||
return (float)GetUnlockedCount() / achievements.Count * 100f;
|
||||
}
|
||||
|
||||
public Achievement GetAchievement(string id)
|
||||
{
|
||||
return achievements.Find(a => a.id == id);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Icon Mapping
|
||||
// ==============================================
|
||||
|
||||
public Sprite GetIconSprite(string iconName)
|
||||
{
|
||||
// Load from Resources/Icons/Achievements/
|
||||
string path = $"Icons/Achievements/{iconName}";
|
||||
var sprite = Resources.Load<Sprite>(path);
|
||||
|
||||
if (sprite == null)
|
||||
{
|
||||
// Fallback icon
|
||||
sprite = Resources.Load<Sprite>("Icons/Achievements/default");
|
||||
}
|
||||
|
||||
return sprite;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Helpers
|
||||
// ==============================================
|
||||
|
||||
private string GetBaseUrl()
|
||||
{
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
return "http://localhost:8000/api/game"; // TODO: Get from BreakpilotAPI
|
||||
}
|
||||
return "http://localhost:8000/api/game";
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Achievement Popup Component
|
||||
// ==============================================
|
||||
|
||||
public class AchievementPopup : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private UnityEngine.UI.Text titleText;
|
||||
[SerializeField] private UnityEngine.UI.Text descriptionText;
|
||||
[SerializeField] private UnityEngine.UI.Image iconImage;
|
||||
|
||||
public void Setup(Achievement achievement)
|
||||
{
|
||||
if (titleText != null)
|
||||
titleText.text = achievement.name;
|
||||
|
||||
if (descriptionText != null)
|
||||
descriptionText.text = achievement.description;
|
||||
|
||||
if (iconImage != null)
|
||||
{
|
||||
var sprite = AchievementManager.Instance?.GetIconSprite(achievement.icon);
|
||||
if (sprite != null)
|
||||
iconImage.sprite = sprite;
|
||||
}
|
||||
|
||||
// Animate in
|
||||
StartCoroutine(AnimateIn());
|
||||
}
|
||||
|
||||
private IEnumerator AnimateIn()
|
||||
{
|
||||
var canvasGroup = GetComponent<CanvasGroup>();
|
||||
if (canvasGroup != null)
|
||||
{
|
||||
canvasGroup.alpha = 0f;
|
||||
float duration = 0.3f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
canvasGroup.alpha = Mathf.Lerp(0f, 1f, elapsed / duration);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
canvasGroup.alpha = 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
290
breakpilot-drive/UnityScripts/Core/AudioManager.cs
Normal file
290
breakpilot-drive/UnityScripts/Core/AudioManager.cs
Normal file
@@ -0,0 +1,290 @@
|
||||
// ==============================================
|
||||
// AudioManager.cs - Audio-Verwaltung
|
||||
// ==============================================
|
||||
// Verwaltet Musik, Sound-Effekte und
|
||||
// Text-to-Speech fuer das Spiel.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
[System.Serializable]
|
||||
public class SoundEffect
|
||||
{
|
||||
public string name;
|
||||
public AudioClip clip;
|
||||
[Range(0f, 1f)]
|
||||
public float volume = 1f;
|
||||
[Range(0.5f, 1.5f)]
|
||||
public float pitch = 1f;
|
||||
public bool loop = false;
|
||||
}
|
||||
|
||||
public class AudioManager : MonoBehaviour
|
||||
{
|
||||
public static AudioManager Instance { get; private set; }
|
||||
|
||||
[Header("Audio Sources")]
|
||||
[SerializeField] private AudioSource musicSource;
|
||||
[SerializeField] private AudioSource sfxSource;
|
||||
[SerializeField] private AudioSource voiceSource;
|
||||
|
||||
[Header("Musik")]
|
||||
[SerializeField] private AudioClip menuMusic;
|
||||
[SerializeField] private AudioClip gameMusic;
|
||||
|
||||
[Header("Sound-Effekte")]
|
||||
[SerializeField] private SoundEffect[] soundEffects;
|
||||
|
||||
[Header("Lautstaerke")]
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float masterVolume = 1f;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float musicVolume = 0.5f;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float sfxVolume = 1f;
|
||||
[Range(0f, 1f)]
|
||||
[SerializeField] private float voiceVolume = 1f;
|
||||
|
||||
// Sound-Dictionary fuer schnellen Zugriff
|
||||
private Dictionary<string, SoundEffect> soundDict = new Dictionary<string, SoundEffect>();
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
InitializeSounds();
|
||||
LoadVolumeSettings();
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeSounds()
|
||||
{
|
||||
soundDict.Clear();
|
||||
foreach (var sound in soundEffects)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(sound.name) && sound.clip != null)
|
||||
{
|
||||
soundDict[sound.name.ToLower()] = sound;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Musik
|
||||
// ==============================================
|
||||
public void PlayMenuMusic()
|
||||
{
|
||||
PlayMusic(menuMusic);
|
||||
}
|
||||
|
||||
public void PlayGameMusic()
|
||||
{
|
||||
PlayMusic(gameMusic);
|
||||
}
|
||||
|
||||
public void PlayMusic(AudioClip clip)
|
||||
{
|
||||
if (musicSource == null || clip == null) return;
|
||||
|
||||
if (musicSource.clip == clip && musicSource.isPlaying)
|
||||
return;
|
||||
|
||||
musicSource.clip = clip;
|
||||
musicSource.volume = musicVolume * masterVolume;
|
||||
musicSource.loop = true;
|
||||
musicSource.Play();
|
||||
}
|
||||
|
||||
public void StopMusic()
|
||||
{
|
||||
if (musicSource != null)
|
||||
{
|
||||
musicSource.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseMusic()
|
||||
{
|
||||
if (musicSource != null)
|
||||
{
|
||||
musicSource.Pause();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResumeMusic()
|
||||
{
|
||||
if (musicSource != null)
|
||||
{
|
||||
musicSource.UnPause();
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Sound-Effekte
|
||||
// ==============================================
|
||||
public void PlaySound(string soundName)
|
||||
{
|
||||
if (sfxSource == null) return;
|
||||
|
||||
string key = soundName.ToLower();
|
||||
if (soundDict.TryGetValue(key, out SoundEffect sound))
|
||||
{
|
||||
sfxSource.pitch = sound.pitch;
|
||||
sfxSource.PlayOneShot(sound.clip, sound.volume * sfxVolume * masterVolume);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning($"Sound '{soundName}' nicht gefunden!");
|
||||
}
|
||||
}
|
||||
|
||||
public void PlaySound(AudioClip clip, float volume = 1f)
|
||||
{
|
||||
if (sfxSource == null || clip == null) return;
|
||||
sfxSource.PlayOneShot(clip, volume * sfxVolume * masterVolume);
|
||||
}
|
||||
|
||||
// Vordefinierte Sound-Methoden
|
||||
public void PlayCoinSound() => PlaySound("coin");
|
||||
public void PlayCrashSound() => PlaySound("crash");
|
||||
public void PlayCorrectSound() => PlaySound("correct");
|
||||
public void PlayWrongSound() => PlaySound("wrong");
|
||||
public void PlayButtonSound() => PlaySound("button");
|
||||
|
||||
// ==============================================
|
||||
// Text-to-Speech (WebGL)
|
||||
// ==============================================
|
||||
public void Speak(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return;
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
SpeakWebGL(text);
|
||||
#else
|
||||
Debug.Log($"[TTS] {text}");
|
||||
#endif
|
||||
}
|
||||
|
||||
public void SpeakQuestion(QuizQuestion question)
|
||||
{
|
||||
if (question == null) return;
|
||||
|
||||
Speak(question.question_text);
|
||||
|
||||
// Optionen vorlesen (mit Verzoegerung)
|
||||
for (int i = 0; i < question.options.Length; i++)
|
||||
{
|
||||
string optionText = $"Option {i + 1}: {question.options[i]}";
|
||||
// In WebGL: Verzoegerung ueber JavaScript
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
SpeakDelayedWebGL(optionText, 1.5f + (i * 1.5f));
|
||||
#else
|
||||
Debug.Log($"[TTS] {optionText}");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public void SpeakFeedback(bool correct)
|
||||
{
|
||||
string message = correct ?
|
||||
"Richtig! Gut gemacht!" :
|
||||
"Nicht ganz. Versuch es nochmal!";
|
||||
Speak(message);
|
||||
}
|
||||
|
||||
public void StopSpeaking()
|
||||
{
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
StopSpeakingWebGL();
|
||||
#endif
|
||||
}
|
||||
|
||||
// WebGL JavaScript Interop
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
[System.Runtime.InteropServices.DllImport("__Internal")]
|
||||
private static extern void SpeakWebGL(string text);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("__Internal")]
|
||||
private static extern void SpeakDelayedWebGL(string text, float delaySeconds);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("__Internal")]
|
||||
private static extern void StopSpeakingWebGL();
|
||||
#endif
|
||||
|
||||
// ==============================================
|
||||
// Lautstaerke-Einstellungen
|
||||
// ==============================================
|
||||
public void SetMasterVolume(float volume)
|
||||
{
|
||||
masterVolume = Mathf.Clamp01(volume);
|
||||
UpdateAllVolumes();
|
||||
SaveVolumeSettings();
|
||||
}
|
||||
|
||||
public void SetMusicVolume(float volume)
|
||||
{
|
||||
musicVolume = Mathf.Clamp01(volume);
|
||||
if (musicSource != null)
|
||||
{
|
||||
musicSource.volume = musicVolume * masterVolume;
|
||||
}
|
||||
SaveVolumeSettings();
|
||||
}
|
||||
|
||||
public void SetSFXVolume(float volume)
|
||||
{
|
||||
sfxVolume = Mathf.Clamp01(volume);
|
||||
SaveVolumeSettings();
|
||||
}
|
||||
|
||||
public void SetVoiceVolume(float volume)
|
||||
{
|
||||
voiceVolume = Mathf.Clamp01(volume);
|
||||
if (voiceSource != null)
|
||||
{
|
||||
voiceSource.volume = voiceVolume * masterVolume;
|
||||
}
|
||||
SaveVolumeSettings();
|
||||
}
|
||||
|
||||
private void UpdateAllVolumes()
|
||||
{
|
||||
if (musicSource != null)
|
||||
musicSource.volume = musicVolume * masterVolume;
|
||||
if (voiceSource != null)
|
||||
voiceSource.volume = voiceVolume * masterVolume;
|
||||
}
|
||||
|
||||
private void SaveVolumeSettings()
|
||||
{
|
||||
PlayerPrefs.SetFloat("MasterVolume", masterVolume);
|
||||
PlayerPrefs.SetFloat("MusicVolume", musicVolume);
|
||||
PlayerPrefs.SetFloat("SFXVolume", sfxVolume);
|
||||
PlayerPrefs.SetFloat("VoiceVolume", voiceVolume);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
|
||||
private void LoadVolumeSettings()
|
||||
{
|
||||
masterVolume = PlayerPrefs.GetFloat("MasterVolume", 1f);
|
||||
musicVolume = PlayerPrefs.GetFloat("MusicVolume", 0.5f);
|
||||
sfxVolume = PlayerPrefs.GetFloat("SFXVolume", 1f);
|
||||
voiceVolume = PlayerPrefs.GetFloat("VoiceVolume", 1f);
|
||||
UpdateAllVolumes();
|
||||
}
|
||||
|
||||
// Properties fuer UI-Sliders
|
||||
public float MasterVolume => masterVolume;
|
||||
public float MusicVolume => musicVolume;
|
||||
public float SFXVolume => sfxVolume;
|
||||
public float VoiceVolume => voiceVolume;
|
||||
}
|
||||
}
|
||||
247
breakpilot-drive/UnityScripts/Core/AuthManager.cs
Normal file
247
breakpilot-drive/UnityScripts/Core/AuthManager.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
// ==============================================
|
||||
// AuthManager.cs - Unity Auth Handler
|
||||
// ==============================================
|
||||
// Handles JWT token for Keycloak authentication.
|
||||
// Supports token via URL parameter or PostMessage from parent frame.
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class AuthManager : MonoBehaviour
|
||||
{
|
||||
public static AuthManager Instance { get; private set; }
|
||||
|
||||
[Header("Auth Configuration")]
|
||||
[SerializeField] private bool requireAuth = false;
|
||||
[SerializeField] private string devUserId = "dev-user-123";
|
||||
|
||||
// Current auth state
|
||||
private string jwtToken;
|
||||
private string userId;
|
||||
private bool isAuthenticated;
|
||||
|
||||
// Events
|
||||
public event Action<string> OnAuthenticated;
|
||||
public event Action<string> OnAuthFailed;
|
||||
|
||||
// JavaScript interface for WebGL
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
[DllImport("__Internal")]
|
||||
private static extern string GetTokenFromURL();
|
||||
|
||||
[DllImport("__Internal")]
|
||||
private static extern string GetTokenFromParent();
|
||||
|
||||
[DllImport("__Internal")]
|
||||
private static extern void RequestTokenFromParent();
|
||||
#endif
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
InitializeAuth();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Initialization
|
||||
// ==============================================
|
||||
|
||||
private void InitializeAuth()
|
||||
{
|
||||
// Development mode - use dev user
|
||||
if (!requireAuth || Application.isEditor)
|
||||
{
|
||||
SetDevUser();
|
||||
return;
|
||||
}
|
||||
|
||||
#if UNITY_WEBGL && !UNITY_EDITOR
|
||||
// Try URL parameter first
|
||||
string urlToken = GetTokenFromURL();
|
||||
if (!string.IsNullOrEmpty(urlToken))
|
||||
{
|
||||
SetToken(urlToken);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try parent frame (iframe scenario)
|
||||
RequestTokenFromParent();
|
||||
#else
|
||||
// Non-WebGL builds use dev user
|
||||
SetDevUser();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void SetDevUser()
|
||||
{
|
||||
userId = devUserId;
|
||||
isAuthenticated = true;
|
||||
jwtToken = null;
|
||||
|
||||
Debug.Log($"[Auth] Development mode - User: {userId}");
|
||||
OnAuthenticated?.Invoke(userId);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Token Management
|
||||
// ==============================================
|
||||
|
||||
public void SetToken(string token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
OnAuthFailed?.Invoke("Empty token");
|
||||
return;
|
||||
}
|
||||
|
||||
jwtToken = token;
|
||||
userId = ExtractUserIdFromToken(token);
|
||||
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
isAuthenticated = true;
|
||||
Debug.Log($"[Auth] Authenticated - User: {userId}");
|
||||
OnAuthenticated?.Invoke(userId);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnAuthFailed?.Invoke("Could not extract user ID from token");
|
||||
}
|
||||
}
|
||||
|
||||
// Called from JavaScript via SendMessage
|
||||
public void ReceiveTokenFromJS(string token)
|
||||
{
|
||||
Debug.Log("[Auth] Received token from JavaScript");
|
||||
SetToken(token);
|
||||
}
|
||||
|
||||
// Called from JavaScript if auth fails
|
||||
public void AuthFailedFromJS(string error)
|
||||
{
|
||||
Debug.LogWarning($"[Auth] JavaScript auth failed: {error}");
|
||||
|
||||
// Fall back to dev user in development
|
||||
if (!requireAuth)
|
||||
{
|
||||
SetDevUser();
|
||||
}
|
||||
else
|
||||
{
|
||||
OnAuthFailed?.Invoke(error);
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractUserIdFromToken(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
// JWT has 3 parts: header.payload.signature
|
||||
string[] parts = token.Split('.');
|
||||
if (parts.Length != 3) return null;
|
||||
|
||||
// Decode payload (base64)
|
||||
string payload = parts[1];
|
||||
|
||||
// Fix base64 padding
|
||||
int padding = 4 - (payload.Length % 4);
|
||||
if (padding < 4) payload += new string('=', padding);
|
||||
|
||||
// Replace URL-safe chars
|
||||
payload = payload.Replace('-', '+').Replace('_', '/');
|
||||
|
||||
byte[] bytes = Convert.FromBase64String(payload);
|
||||
string json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
|
||||
// Simple JSON parsing for "sub" claim
|
||||
var claims = JsonUtility.FromJson<JWTPayload>(json);
|
||||
return claims?.sub;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[Auth] Token parsing failed: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
private class JWTPayload
|
||||
{
|
||||
public string sub;
|
||||
public string email;
|
||||
public string name;
|
||||
public long exp;
|
||||
public long iat;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Public API
|
||||
// ==============================================
|
||||
|
||||
public bool IsAuthenticated => isAuthenticated;
|
||||
public string UserId => userId;
|
||||
public string Token => jwtToken;
|
||||
public bool RequiresAuth => requireAuth;
|
||||
|
||||
public string GetAuthHeader()
|
||||
{
|
||||
if (string.IsNullOrEmpty(jwtToken))
|
||||
return null;
|
||||
return $"Bearer {jwtToken}";
|
||||
}
|
||||
|
||||
public void Logout()
|
||||
{
|
||||
jwtToken = null;
|
||||
userId = null;
|
||||
isAuthenticated = false;
|
||||
Debug.Log("[Auth] Logged out");
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
public bool IsTokenExpired()
|
||||
{
|
||||
if (string.IsNullOrEmpty(jwtToken))
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
string[] parts = jwtToken.Split('.');
|
||||
if (parts.Length != 3) return true;
|
||||
|
||||
string payload = parts[1];
|
||||
int padding = 4 - (payload.Length % 4);
|
||||
if (padding < 4) payload += new string('=', padding);
|
||||
payload = payload.Replace('-', '+').Replace('_', '/');
|
||||
|
||||
byte[] bytes = Convert.FromBase64String(payload);
|
||||
string json = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
var claims = JsonUtility.FromJson<JWTPayload>(json);
|
||||
|
||||
if (claims?.exp > 0)
|
||||
{
|
||||
var expTime = DateTimeOffset.FromUnixTimeSeconds(claims.exp).UtcDateTime;
|
||||
return DateTime.UtcNow > expTime;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
292
breakpilot-drive/UnityScripts/Core/DifficultyManager.cs
Normal file
292
breakpilot-drive/UnityScripts/Core/DifficultyManager.cs
Normal file
@@ -0,0 +1,292 @@
|
||||
// ==============================================
|
||||
// DifficultyManager.cs - Schwierigkeits-Steuerung
|
||||
// ==============================================
|
||||
// Passt die Spielschwierigkeit dynamisch an
|
||||
// basierend auf Lernniveau und Spielerleistung.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class DifficultyManager : MonoBehaviour
|
||||
{
|
||||
public static DifficultyManager Instance { get; private set; }
|
||||
|
||||
[Header("Schwierigkeits-Bereich")]
|
||||
[SerializeField] private int minDifficulty = 1;
|
||||
[SerializeField] private int maxDifficulty = 5;
|
||||
|
||||
[Header("Dynamische Anpassung")]
|
||||
[SerializeField] private bool enableDynamicDifficulty = true;
|
||||
[SerializeField] private int questionsForEvaluation = 5;
|
||||
[SerializeField] private float accuracyToIncrease = 0.8f; // 80% richtig = schwerer
|
||||
[SerializeField] private float accuracyToDecrease = 0.4f; // 40% richtig = leichter
|
||||
|
||||
[Header("Geschwindigkeits-Steigerung")]
|
||||
[SerializeField] private bool enableSpeedIncrease = true;
|
||||
[SerializeField] private float speedIncreaseInterval = 30f; // Alle X Sekunden
|
||||
[SerializeField] private float speedIncreaseAmount = 0.5f; // Um X erhoehen
|
||||
|
||||
// Aktueller Zustand
|
||||
private int currentDifficulty = 3;
|
||||
private GameDifficulty currentSettings;
|
||||
private float timeSinceLastSpeedIncrease = 0f;
|
||||
|
||||
// Statistik fuer dynamische Anpassung
|
||||
private int recentQuestionsAnswered = 0;
|
||||
private int recentQuestionsCorrect = 0;
|
||||
|
||||
// Events
|
||||
public event Action<int> OnDifficultyChanged;
|
||||
public event Action<GameDifficulty> OnSettingsUpdated;
|
||||
|
||||
// Properties
|
||||
public int CurrentDifficulty => currentDifficulty;
|
||||
public GameDifficulty CurrentSettings => currentSettings;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Quiz-Events abonnieren
|
||||
if (QuizManager.Instance != null)
|
||||
{
|
||||
QuizManager.Instance.OnQuestionAnswered += OnQuestionAnswered;
|
||||
}
|
||||
|
||||
// Initiale Schwierigkeit laden
|
||||
LoadDifficulty(currentDifficulty);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (QuizManager.Instance != null)
|
||||
{
|
||||
QuizManager.Instance.OnQuestionAnswered -= OnQuestionAnswered;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (GameManager.Instance?.CurrentState != GameState.Playing)
|
||||
return;
|
||||
|
||||
// Geschwindigkeit graduell erhoehen
|
||||
if (enableSpeedIncrease)
|
||||
{
|
||||
timeSinceLastSpeedIncrease += Time.deltaTime;
|
||||
|
||||
if (timeSinceLastSpeedIncrease >= speedIncreaseInterval)
|
||||
{
|
||||
timeSinceLastSpeedIncrease = 0f;
|
||||
IncreaseSpeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Schwierigkeit laden
|
||||
// ==============================================
|
||||
public void LoadDifficulty(int level)
|
||||
{
|
||||
currentDifficulty = Mathf.Clamp(level, minDifficulty, maxDifficulty);
|
||||
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
StartCoroutine(BreakpilotAPI.Instance.GetDifficulty(currentDifficulty,
|
||||
onSuccess: (settings) =>
|
||||
{
|
||||
currentSettings = settings;
|
||||
ApplySettings(settings);
|
||||
OnSettingsUpdated?.Invoke(settings);
|
||||
Debug.Log($"Schwierigkeit {currentDifficulty} geladen");
|
||||
},
|
||||
onError: (error) =>
|
||||
{
|
||||
Debug.LogWarning($"Konnte Schwierigkeit nicht laden: {error}");
|
||||
// Fallback zu Default-Werten
|
||||
currentSettings = GetDefaultSettings(currentDifficulty);
|
||||
ApplySettings(currentSettings);
|
||||
}
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
currentSettings = GetDefaultSettings(currentDifficulty);
|
||||
ApplySettings(currentSettings);
|
||||
}
|
||||
|
||||
OnDifficultyChanged?.Invoke(currentDifficulty);
|
||||
}
|
||||
|
||||
private void ApplySettings(GameDifficulty settings)
|
||||
{
|
||||
// Geschwindigkeit anwenden
|
||||
if (TrackGenerator.Instance != null)
|
||||
{
|
||||
TrackGenerator.Instance.SetSpeed(settings.lane_speed);
|
||||
}
|
||||
|
||||
if (PlayerController.Instance != null)
|
||||
{
|
||||
PlayerController.Instance.SetSpeed(settings.lane_speed);
|
||||
}
|
||||
|
||||
// Hindernis-Frequenz anwenden
|
||||
// ObstacleSpawner liest die Settings direkt aus GameManager
|
||||
|
||||
Debug.Log($"Settings angewendet: Speed={settings.lane_speed}, " +
|
||||
$"Obstacles={settings.obstacle_frequency}, " +
|
||||
$"Hints={settings.hints_enabled}");
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Dynamische Anpassung
|
||||
// ==============================================
|
||||
private void OnQuestionAnswered(bool correct, int points)
|
||||
{
|
||||
if (!enableDynamicDifficulty) return;
|
||||
|
||||
recentQuestionsAnswered++;
|
||||
if (correct) recentQuestionsCorrect++;
|
||||
|
||||
// Evaluation nach X Fragen
|
||||
if (recentQuestionsAnswered >= questionsForEvaluation)
|
||||
{
|
||||
EvaluateAndAdjust();
|
||||
}
|
||||
}
|
||||
|
||||
private void EvaluateAndAdjust()
|
||||
{
|
||||
float accuracy = (float)recentQuestionsCorrect / recentQuestionsAnswered;
|
||||
|
||||
Debug.Log($"Quiz-Evaluation: {recentQuestionsCorrect}/{recentQuestionsAnswered} " +
|
||||
$"= {accuracy:P0}");
|
||||
|
||||
if (accuracy >= accuracyToIncrease && currentDifficulty < maxDifficulty)
|
||||
{
|
||||
// Spieler ist gut - Schwierigkeit erhoehen
|
||||
IncreaseDifficulty();
|
||||
}
|
||||
else if (accuracy <= accuracyToDecrease && currentDifficulty > minDifficulty)
|
||||
{
|
||||
// Spieler hat Probleme - Schwierigkeit verringern
|
||||
DecreaseDifficulty();
|
||||
}
|
||||
|
||||
// Reset fuer naechste Evaluation
|
||||
recentQuestionsAnswered = 0;
|
||||
recentQuestionsCorrect = 0;
|
||||
}
|
||||
|
||||
public void IncreaseDifficulty()
|
||||
{
|
||||
if (currentDifficulty < maxDifficulty)
|
||||
{
|
||||
LoadDifficulty(currentDifficulty + 1);
|
||||
Debug.Log($"Schwierigkeit erhoeht auf {currentDifficulty}");
|
||||
}
|
||||
}
|
||||
|
||||
public void DecreaseDifficulty()
|
||||
{
|
||||
if (currentDifficulty > minDifficulty)
|
||||
{
|
||||
LoadDifficulty(currentDifficulty - 1);
|
||||
Debug.Log($"Schwierigkeit verringert auf {currentDifficulty}");
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Geschwindigkeits-Steigerung
|
||||
// ==============================================
|
||||
private void IncreaseSpeed()
|
||||
{
|
||||
if (currentSettings == null) return;
|
||||
|
||||
float newSpeed = currentSettings.lane_speed + speedIncreaseAmount;
|
||||
|
||||
// Maximal-Geschwindigkeit basierend auf Difficulty
|
||||
float maxSpeed = 5f + (currentDifficulty * 2f);
|
||||
newSpeed = Mathf.Min(newSpeed, maxSpeed);
|
||||
|
||||
currentSettings.lane_speed = newSpeed;
|
||||
|
||||
if (TrackGenerator.Instance != null)
|
||||
{
|
||||
TrackGenerator.Instance.SetSpeed(newSpeed);
|
||||
}
|
||||
|
||||
Debug.Log($"Geschwindigkeit erhoeht auf {newSpeed:F1}");
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Default-Werte (Fallback)
|
||||
// ==============================================
|
||||
private GameDifficulty GetDefaultSettings(int level)
|
||||
{
|
||||
return new GameDifficulty
|
||||
{
|
||||
lane_speed = 4f + level,
|
||||
obstacle_frequency = 0.3f + (level * 0.1f),
|
||||
power_up_chance = 0.4f - (level * 0.05f),
|
||||
question_complexity = level,
|
||||
answer_time = 15 - (level * 2),
|
||||
hints_enabled = level <= 2,
|
||||
speech_speed = 0.8f + (level * 0.1f)
|
||||
};
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Oeffentliche Methoden
|
||||
// ==============================================
|
||||
public void SetDifficulty(int level)
|
||||
{
|
||||
LoadDifficulty(level);
|
||||
}
|
||||
|
||||
public void ResetForNewGame()
|
||||
{
|
||||
recentQuestionsAnswered = 0;
|
||||
recentQuestionsCorrect = 0;
|
||||
timeSinceLastSpeedIncrease = 0f;
|
||||
|
||||
// Schwierigkeit von User-Level laden
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
var cachedLevel = BreakpilotAPI.Instance.GetCachedLevel();
|
||||
if (cachedLevel != null)
|
||||
{
|
||||
LoadDifficulty(cachedLevel.overall_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float GetCurrentSpeed()
|
||||
{
|
||||
return currentSettings?.lane_speed ?? 5f;
|
||||
}
|
||||
|
||||
public bool AreHintsEnabled()
|
||||
{
|
||||
return currentSettings?.hints_enabled ?? false;
|
||||
}
|
||||
|
||||
public int GetAnswerTime()
|
||||
{
|
||||
return currentSettings?.answer_time ?? 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
317
breakpilot-drive/UnityScripts/Core/GameManager.cs
Normal file
317
breakpilot-drive/UnityScripts/Core/GameManager.cs
Normal file
@@ -0,0 +1,317 @@
|
||||
// ==============================================
|
||||
// GameManager.cs - Zentrale Spielsteuerung
|
||||
// ==============================================
|
||||
// Verwaltet Spielzustand, Score, Leben und
|
||||
// koordiniert alle anderen Manager.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public enum GameState
|
||||
{
|
||||
MainMenu,
|
||||
Playing,
|
||||
Paused,
|
||||
QuizActive,
|
||||
GameOver
|
||||
}
|
||||
|
||||
public class GameManager : MonoBehaviour
|
||||
{
|
||||
public static GameManager Instance { get; private set; }
|
||||
|
||||
[Header("Spieleinstellungen")]
|
||||
[SerializeField] private int startLives = 3;
|
||||
[SerializeField] private float pauseQuestionInterval = 500f; // Alle X Meter
|
||||
|
||||
[Header("UI Referenzen")]
|
||||
[SerializeField] private GameObject gameOverPanel;
|
||||
[SerializeField] private GameObject pausePanel;
|
||||
|
||||
// Spielzustand
|
||||
private GameState currentState = GameState.MainMenu;
|
||||
private int score = 0;
|
||||
private int lives;
|
||||
private float distanceTraveled = 0f;
|
||||
private float playTime = 0f;
|
||||
private float nextPauseQuestionDistance;
|
||||
|
||||
// Schwierigkeit (von API geladen)
|
||||
private int currentDifficulty = 3;
|
||||
private GameDifficulty difficultySettings;
|
||||
|
||||
// Events
|
||||
public event Action<int> OnScoreChanged;
|
||||
public event Action<int> OnLivesChanged;
|
||||
public event Action<float> OnDistanceChanged;
|
||||
public event Action<GameState> OnStateChanged;
|
||||
|
||||
// Properties
|
||||
public GameState CurrentState => currentState;
|
||||
public int Score => score;
|
||||
public int Lives => lives;
|
||||
public float DistanceTraveled => distanceTraveled;
|
||||
public float PlayTime => playTime;
|
||||
public int CurrentDifficulty => currentDifficulty;
|
||||
public GameDifficulty DifficultySettings => difficultySettings;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// UI initial verstecken
|
||||
if (gameOverPanel) gameOverPanel.SetActive(false);
|
||||
if (pausePanel) pausePanel.SetActive(false);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (currentState == GameState.Playing)
|
||||
{
|
||||
// Spielzeit zaehlen
|
||||
playTime += Time.deltaTime;
|
||||
|
||||
// Pause-Fragen pruefen
|
||||
if (distanceTraveled >= nextPauseQuestionDistance)
|
||||
{
|
||||
TriggerPauseQuestion();
|
||||
}
|
||||
|
||||
// Escape fuer Pause
|
||||
if (Input.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
PauseGame();
|
||||
}
|
||||
}
|
||||
else if (currentState == GameState.Paused)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Escape))
|
||||
{
|
||||
ResumeGame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spiel starten
|
||||
// ==============================================
|
||||
public void StartGame(string userId = "guest")
|
||||
{
|
||||
// Werte zuruecksetzen
|
||||
score = 0;
|
||||
lives = startLives;
|
||||
distanceTraveled = 0f;
|
||||
playTime = 0f;
|
||||
nextPauseQuestionDistance = pauseQuestionInterval;
|
||||
|
||||
// Events ausloesen
|
||||
OnScoreChanged?.Invoke(score);
|
||||
OnLivesChanged?.Invoke(lives);
|
||||
|
||||
// Schwierigkeit laden
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
StartCoroutine(BreakpilotAPI.Instance.GetLearningLevel(userId,
|
||||
onSuccess: (level) =>
|
||||
{
|
||||
currentDifficulty = level.overall_level;
|
||||
LoadDifficultySettings();
|
||||
}
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadDifficultySettings();
|
||||
}
|
||||
|
||||
// Quiz-Statistik zuruecksetzen
|
||||
if (QuizManager.Instance != null)
|
||||
{
|
||||
QuizManager.Instance.ResetStatistics();
|
||||
}
|
||||
|
||||
SetState(GameState.Playing);
|
||||
}
|
||||
|
||||
private void LoadDifficultySettings()
|
||||
{
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
StartCoroutine(BreakpilotAPI.Instance.GetDifficulty(currentDifficulty,
|
||||
onSuccess: (settings) =>
|
||||
{
|
||||
difficultySettings = settings;
|
||||
Debug.Log($"Schwierigkeit {currentDifficulty} geladen: Speed={settings.lane_speed}");
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Score-System
|
||||
// ==============================================
|
||||
public void AddScore(int points)
|
||||
{
|
||||
score = Mathf.Max(0, score + points);
|
||||
OnScoreChanged?.Invoke(score);
|
||||
}
|
||||
|
||||
public void AddDistance(float distance)
|
||||
{
|
||||
distanceTraveled += distance;
|
||||
OnDistanceChanged?.Invoke(distanceTraveled);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Leben-System
|
||||
// ==============================================
|
||||
public void LoseLife()
|
||||
{
|
||||
lives--;
|
||||
OnLivesChanged?.Invoke(lives);
|
||||
|
||||
if (lives <= 0)
|
||||
{
|
||||
GameOver();
|
||||
}
|
||||
}
|
||||
|
||||
public void GainLife()
|
||||
{
|
||||
lives++;
|
||||
OnLivesChanged?.Invoke(lives);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Pause-Fragen
|
||||
// ==============================================
|
||||
private void TriggerPauseQuestion()
|
||||
{
|
||||
nextPauseQuestionDistance += pauseQuestionInterval;
|
||||
|
||||
if (BreakpilotAPI.Instance != null && QuizManager.Instance != null)
|
||||
{
|
||||
// Pause-Frage aus Cache holen
|
||||
var questions = BreakpilotAPI.Instance.GetCachedQuestions();
|
||||
var pauseQuestions = questions.FindAll(q => q.quiz_mode == "pause");
|
||||
|
||||
if (pauseQuestions.Count > 0)
|
||||
{
|
||||
int index = UnityEngine.Random.Range(0, pauseQuestions.Count);
|
||||
QuizManager.Instance.ShowPauseQuestion(pauseQuestions[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spielzustand
|
||||
// ==============================================
|
||||
public void SetState(GameState newState)
|
||||
{
|
||||
currentState = newState;
|
||||
OnStateChanged?.Invoke(newState);
|
||||
|
||||
switch (newState)
|
||||
{
|
||||
case GameState.Playing:
|
||||
Time.timeScale = 1f;
|
||||
break;
|
||||
case GameState.Paused:
|
||||
case GameState.QuizActive:
|
||||
Time.timeScale = 0f;
|
||||
break;
|
||||
case GameState.GameOver:
|
||||
Time.timeScale = 0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void PauseGame()
|
||||
{
|
||||
if (currentState == GameState.Playing)
|
||||
{
|
||||
SetState(GameState.Paused);
|
||||
if (pausePanel) pausePanel.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResumeGame()
|
||||
{
|
||||
if (currentState == GameState.Paused)
|
||||
{
|
||||
SetState(GameState.Playing);
|
||||
if (pausePanel) pausePanel.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spiel beenden
|
||||
// ==============================================
|
||||
public void GameOver()
|
||||
{
|
||||
SetState(GameState.GameOver);
|
||||
|
||||
if (gameOverPanel) gameOverPanel.SetActive(true);
|
||||
|
||||
// Session speichern
|
||||
SaveSession();
|
||||
}
|
||||
|
||||
private void SaveSession()
|
||||
{
|
||||
if (BreakpilotAPI.Instance == null) return;
|
||||
|
||||
GameSession session = new GameSession
|
||||
{
|
||||
user_id = "guest", // TODO: Echte User-ID
|
||||
game_mode = "video",
|
||||
duration_seconds = Mathf.RoundToInt(playTime),
|
||||
distance_traveled = distanceTraveled,
|
||||
score = score,
|
||||
questions_answered = QuizManager.Instance?.GetQuestionsAnswered() ?? 0,
|
||||
questions_correct = QuizManager.Instance?.GetQuestionsCorrect() ?? 0,
|
||||
difficulty_level = currentDifficulty
|
||||
};
|
||||
|
||||
StartCoroutine(BreakpilotAPI.Instance.SaveGameSession(session,
|
||||
onSuccess: (response) =>
|
||||
{
|
||||
Debug.Log($"Session gespeichert: {response.session_id}");
|
||||
if (response.new_level.HasValue)
|
||||
{
|
||||
Debug.Log($"Level Up! Neues Level: {response.new_level}");
|
||||
}
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Neustart
|
||||
// ==============================================
|
||||
public void RestartGame()
|
||||
{
|
||||
if (gameOverPanel) gameOverPanel.SetActive(false);
|
||||
StartGame();
|
||||
}
|
||||
|
||||
public void LoadMainMenu()
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
SceneManager.LoadScene("MainMenu");
|
||||
}
|
||||
}
|
||||
}
|
||||
373
breakpilot-drive/UnityScripts/Player/PlayerController.cs
Normal file
373
breakpilot-drive/UnityScripts/Player/PlayerController.cs
Normal file
@@ -0,0 +1,373 @@
|
||||
// ==============================================
|
||||
// PlayerController.cs - Spieler-Steuerung
|
||||
// ==============================================
|
||||
// 3-Spur-System mit Tastatur und Touch-Eingabe.
|
||||
// Kollisionserkennung fuer Hindernisse und Items.
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class PlayerController : MonoBehaviour
|
||||
{
|
||||
public static PlayerController Instance { get; private set; }
|
||||
|
||||
[Header("Bewegung")]
|
||||
[SerializeField] private float laneDistance = 3f; // Abstand zwischen Spuren
|
||||
[SerializeField] private float laneSwitchSpeed = 10f; // Geschwindigkeit beim Spurwechsel
|
||||
[SerializeField] private float forwardSpeed = 10f; // Vorwaertsgeschwindigkeit
|
||||
|
||||
[Header("Spuren")]
|
||||
[SerializeField] private int currentLane = 1; // 0=Links, 1=Mitte, 2=Rechts
|
||||
private int targetLane = 1;
|
||||
|
||||
[Header("Touch-Steuerung")]
|
||||
[SerializeField] private float swipeThreshold = 50f; // Pixel fuer Swipe-Erkennung
|
||||
private Vector2 touchStartPos;
|
||||
private bool isSwiping = false;
|
||||
|
||||
[Header("Effekte")]
|
||||
[SerializeField] private ParticleSystem crashEffect;
|
||||
[SerializeField] private ParticleSystem collectEffect;
|
||||
|
||||
// Zustand
|
||||
private bool isAlive = true;
|
||||
private bool isInvincible = false;
|
||||
private float invincibleTimer = 0f;
|
||||
|
||||
// Events
|
||||
public event Action OnCrash;
|
||||
public event Action<string> OnItemCollected;
|
||||
|
||||
// Components
|
||||
private Rigidbody rb;
|
||||
private Animator animator;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
rb = GetComponent<Rigidbody>();
|
||||
animator = GetComponent<Animator>();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Startposition (mittlere Spur)
|
||||
currentLane = 1;
|
||||
targetLane = 1;
|
||||
UpdateTargetPosition();
|
||||
|
||||
// Geschwindigkeit von Difficulty laden
|
||||
if (GameManager.Instance?.DifficultySettings != null)
|
||||
{
|
||||
forwardSpeed = GameManager.Instance.DifficultySettings.lane_speed;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (!isAlive || GameManager.Instance?.CurrentState != GameState.Playing)
|
||||
return;
|
||||
|
||||
// Eingabe verarbeiten
|
||||
HandleInput();
|
||||
|
||||
// Zur Zielspur bewegen
|
||||
MoveToTargetLane();
|
||||
|
||||
// Vorwaerts bewegen
|
||||
MoveForward();
|
||||
|
||||
// Unverwundbarkeit Timer
|
||||
if (isInvincible)
|
||||
{
|
||||
invincibleTimer -= Time.deltaTime;
|
||||
if (invincibleTimer <= 0)
|
||||
{
|
||||
isInvincible = false;
|
||||
// Blink-Effekt beenden
|
||||
SetVisibility(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Distanz zum GameManager melden
|
||||
GameManager.Instance?.AddDistance(forwardSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Eingabe-Verarbeitung
|
||||
// ==============================================
|
||||
private void HandleInput()
|
||||
{
|
||||
// Tastatur-Eingabe
|
||||
if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
|
||||
{
|
||||
MoveLane(-1);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow))
|
||||
{
|
||||
MoveLane(1);
|
||||
}
|
||||
|
||||
// Direkte Spurwahl mit Zahlen (auch fuer Quiz)
|
||||
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
|
||||
{
|
||||
SetLane(0);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
|
||||
{
|
||||
SetLane(1);
|
||||
}
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
|
||||
{
|
||||
SetLane(2);
|
||||
}
|
||||
|
||||
// Touch-Eingabe
|
||||
HandleTouchInput();
|
||||
}
|
||||
|
||||
private void HandleTouchInput()
|
||||
{
|
||||
if (Input.touchCount > 0)
|
||||
{
|
||||
Touch touch = Input.GetTouch(0);
|
||||
|
||||
switch (touch.phase)
|
||||
{
|
||||
case TouchPhase.Began:
|
||||
touchStartPos = touch.position;
|
||||
isSwiping = true;
|
||||
break;
|
||||
|
||||
case TouchPhase.Moved:
|
||||
case TouchPhase.Ended:
|
||||
if (isSwiping)
|
||||
{
|
||||
Vector2 swipeDelta = touch.position - touchStartPos;
|
||||
|
||||
if (Mathf.Abs(swipeDelta.x) > swipeThreshold)
|
||||
{
|
||||
if (swipeDelta.x > 0)
|
||||
{
|
||||
MoveLane(1); // Rechts
|
||||
}
|
||||
else
|
||||
{
|
||||
MoveLane(-1); // Links
|
||||
}
|
||||
isSwiping = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Maus-Simulation (fuer Editor-Tests)
|
||||
if (Input.GetMouseButtonDown(0))
|
||||
{
|
||||
touchStartPos = Input.mousePosition;
|
||||
isSwiping = true;
|
||||
}
|
||||
else if (Input.GetMouseButtonUp(0) && isSwiping)
|
||||
{
|
||||
Vector2 swipeDelta = (Vector2)Input.mousePosition - touchStartPos;
|
||||
|
||||
if (Mathf.Abs(swipeDelta.x) > swipeThreshold)
|
||||
{
|
||||
MoveLane(swipeDelta.x > 0 ? 1 : -1);
|
||||
}
|
||||
isSwiping = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spur-Bewegung
|
||||
// ==============================================
|
||||
private void MoveLane(int direction)
|
||||
{
|
||||
targetLane = Mathf.Clamp(targetLane + direction, 0, 2);
|
||||
}
|
||||
|
||||
private void SetLane(int lane)
|
||||
{
|
||||
targetLane = Mathf.Clamp(lane, 0, 2);
|
||||
}
|
||||
|
||||
private void MoveToTargetLane()
|
||||
{
|
||||
float targetX = (targetLane - 1) * laneDistance;
|
||||
Vector3 targetPos = new Vector3(targetX, transform.position.y, transform.position.z);
|
||||
|
||||
transform.position = Vector3.Lerp(
|
||||
transform.position,
|
||||
targetPos,
|
||||
laneSwitchSpeed * Time.deltaTime
|
||||
);
|
||||
|
||||
// Aktuelle Spur aktualisieren
|
||||
currentLane = targetLane;
|
||||
}
|
||||
|
||||
private void MoveForward()
|
||||
{
|
||||
// Spieler bleibt stationaer, Welt bewegt sich
|
||||
// Alternativ: transform.Translate(Vector3.forward * forwardSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
private void UpdateTargetPosition()
|
||||
{
|
||||
float targetX = (targetLane - 1) * laneDistance;
|
||||
transform.position = new Vector3(targetX, transform.position.y, transform.position.z);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Kollisionen
|
||||
// ==============================================
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!isAlive) return;
|
||||
|
||||
// Hindernis
|
||||
if (other.CompareTag("Obstacle"))
|
||||
{
|
||||
if (!isInvincible)
|
||||
{
|
||||
Crash();
|
||||
}
|
||||
}
|
||||
// Sammelbare Items
|
||||
else if (other.CompareTag("Coin"))
|
||||
{
|
||||
CollectItem("coin", 100);
|
||||
Destroy(other.gameObject);
|
||||
}
|
||||
else if (other.CompareTag("Star"))
|
||||
{
|
||||
CollectItem("star", 500);
|
||||
Destroy(other.gameObject);
|
||||
}
|
||||
else if (other.CompareTag("Shield"))
|
||||
{
|
||||
CollectItem("shield", 0);
|
||||
ActivateShield(5f);
|
||||
Destroy(other.gameObject);
|
||||
}
|
||||
// Quiz-Trigger
|
||||
else if (other.CompareTag("QuizTrigger"))
|
||||
{
|
||||
VisualTrigger trigger = other.GetComponent<VisualTrigger>();
|
||||
if (trigger != null)
|
||||
{
|
||||
QuizManager.Instance?.ShowQuickQuestion(trigger.TriggerType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Crash-Handling
|
||||
// ==============================================
|
||||
private void Crash()
|
||||
{
|
||||
// Effekt abspielen
|
||||
if (crashEffect != null)
|
||||
{
|
||||
crashEffect.Play();
|
||||
}
|
||||
|
||||
// Animation
|
||||
if (animator != null)
|
||||
{
|
||||
animator.SetTrigger("Crash");
|
||||
}
|
||||
|
||||
// Leben verlieren
|
||||
GameManager.Instance?.LoseLife();
|
||||
|
||||
// Kurze Unverwundbarkeit
|
||||
isInvincible = true;
|
||||
invincibleTimer = 2f;
|
||||
StartCoroutine(BlinkEffect());
|
||||
|
||||
OnCrash?.Invoke();
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator BlinkEffect()
|
||||
{
|
||||
while (isInvincible)
|
||||
{
|
||||
SetVisibility(false);
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
SetVisibility(true);
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void SetVisibility(bool visible)
|
||||
{
|
||||
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
||||
foreach (var r in renderers)
|
||||
{
|
||||
r.enabled = visible;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Item-Collection
|
||||
// ==============================================
|
||||
private void CollectItem(string itemType, int points)
|
||||
{
|
||||
// Effekt abspielen
|
||||
if (collectEffect != null)
|
||||
{
|
||||
collectEffect.Play();
|
||||
}
|
||||
|
||||
// Punkte hinzufuegen
|
||||
if (points > 0)
|
||||
{
|
||||
GameManager.Instance?.AddScore(points);
|
||||
}
|
||||
|
||||
OnItemCollected?.Invoke(itemType);
|
||||
}
|
||||
|
||||
private void ActivateShield(float duration)
|
||||
{
|
||||
isInvincible = true;
|
||||
invincibleTimer = duration;
|
||||
// TODO: Schild-Visualisierung
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Oeffentliche Methoden
|
||||
// ==============================================
|
||||
public void SetSpeed(float speed)
|
||||
{
|
||||
forwardSpeed = speed;
|
||||
}
|
||||
|
||||
public int GetCurrentLane()
|
||||
{
|
||||
return currentLane;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
isAlive = true;
|
||||
isInvincible = false;
|
||||
currentLane = 1;
|
||||
targetLane = 1;
|
||||
UpdateTargetPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
54
breakpilot-drive/UnityScripts/Plugins/WebGL/AuthPlugin.jslib
Normal file
54
breakpilot-drive/UnityScripts/Plugins/WebGL/AuthPlugin.jslib
Normal file
@@ -0,0 +1,54 @@
|
||||
// ==============================================
|
||||
// AuthPlugin.jslib - WebGL JavaScript Bridge
|
||||
// ==============================================
|
||||
// Place in Assets/Plugins/WebGL/
|
||||
|
||||
mergeInto(LibraryManager.library, {
|
||||
|
||||
// Get token from URL parameter (?token=xxx)
|
||||
GetTokenFromURL: function() {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var token = urlParams.get('token');
|
||||
if (token) {
|
||||
var bufferSize = lengthBytesUTF8(token) + 1;
|
||||
var buffer = _malloc(bufferSize);
|
||||
stringToUTF8(token, buffer, bufferSize);
|
||||
return buffer;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Get token from parent frame (for iframe embedding)
|
||||
GetTokenFromParent: function() {
|
||||
if (window.breakpilotToken) {
|
||||
var token = window.breakpilotToken;
|
||||
var bufferSize = lengthBytesUTF8(token) + 1;
|
||||
var buffer = _malloc(bufferSize);
|
||||
stringToUTF8(token, buffer, bufferSize);
|
||||
return buffer;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
// Request token from parent frame via postMessage
|
||||
RequestTokenFromParent: function() {
|
||||
// Listen for token from parent
|
||||
window.addEventListener('message', function(event) {
|
||||
// Verify origin in production!
|
||||
if (event.data && event.data.type === 'breakpilot_token') {
|
||||
window.breakpilotToken = event.data.token;
|
||||
|
||||
// Send to Unity
|
||||
if (window.unityInstance) {
|
||||
window.unityInstance.SendMessage('AuthManager', 'ReceiveTokenFromJS', event.data.token);
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
|
||||
// Request token from parent
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: 'breakpilot_request_token' }, '*');
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
98
breakpilot-drive/UnityScripts/Plugins/WebSpeech.jslib
Normal file
98
breakpilot-drive/UnityScripts/Plugins/WebSpeech.jslib
Normal file
@@ -0,0 +1,98 @@
|
||||
// ==============================================
|
||||
// WebSpeech.jslib - Text-to-Speech fuer WebGL
|
||||
// ==============================================
|
||||
// Kopiere diese Datei nach Assets/Plugins/WebGL/
|
||||
// um TTS im Browser zu aktivieren.
|
||||
|
||||
mergeInto(LibraryManager.library, {
|
||||
|
||||
// Spricht den Text sofort
|
||||
SpeakWebGL: function(textPtr) {
|
||||
var text = UTF8ToString(textPtr);
|
||||
|
||||
if ('speechSynthesis' in window) {
|
||||
// Vorherige Sprache stoppen
|
||||
window.speechSynthesis.cancel();
|
||||
|
||||
var utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = 'de-DE';
|
||||
utterance.rate = 0.9;
|
||||
utterance.pitch = 1.0;
|
||||
utterance.volume = 1.0;
|
||||
|
||||
// Deutsche Stimme suchen
|
||||
var voices = window.speechSynthesis.getVoices();
|
||||
var germanVoice = voices.find(function(voice) {
|
||||
return voice.lang.startsWith('de');
|
||||
});
|
||||
if (germanVoice) {
|
||||
utterance.voice = germanVoice;
|
||||
}
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
} else {
|
||||
console.warn('Text-to-Speech wird von diesem Browser nicht unterstuetzt.');
|
||||
}
|
||||
},
|
||||
|
||||
// Spricht den Text nach einer Verzoegerung
|
||||
SpeakDelayedWebGL: function(textPtr, delaySeconds) {
|
||||
var text = UTF8ToString(textPtr);
|
||||
|
||||
setTimeout(function() {
|
||||
if ('speechSynthesis' in window) {
|
||||
var utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = 'de-DE';
|
||||
utterance.rate = 0.9;
|
||||
utterance.pitch = 1.0;
|
||||
utterance.volume = 1.0;
|
||||
|
||||
var voices = window.speechSynthesis.getVoices();
|
||||
var germanVoice = voices.find(function(voice) {
|
||||
return voice.lang.startsWith('de');
|
||||
});
|
||||
if (germanVoice) {
|
||||
utterance.voice = germanVoice;
|
||||
}
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
}, delaySeconds * 1000);
|
||||
},
|
||||
|
||||
// Stoppt alle laufenden Sprach-Ausgaben
|
||||
StopSpeakingWebGL: function() {
|
||||
if ('speechSynthesis' in window) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
},
|
||||
|
||||
// Prueft ob TTS verfuegbar ist
|
||||
IsTTSAvailableWebGL: function() {
|
||||
return 'speechSynthesis' in window;
|
||||
},
|
||||
|
||||
// Gibt verfuegbare Stimmen zurueck (als JSON)
|
||||
GetAvailableVoicesWebGL: function() {
|
||||
if ('speechSynthesis' in window) {
|
||||
var voices = window.speechSynthesis.getVoices();
|
||||
var germanVoices = voices.filter(function(voice) {
|
||||
return voice.lang.startsWith('de');
|
||||
}).map(function(voice) {
|
||||
return {
|
||||
name: voice.name,
|
||||
lang: voice.lang,
|
||||
local: voice.localService
|
||||
};
|
||||
});
|
||||
|
||||
var json = JSON.stringify(germanVoices);
|
||||
var bufferSize = lengthBytesUTF8(json) + 1;
|
||||
var buffer = _malloc(bufferSize);
|
||||
stringToUTF8(json, buffer, bufferSize);
|
||||
return buffer;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
});
|
||||
318
breakpilot-drive/UnityScripts/QuizManager.cs
Normal file
318
breakpilot-drive/UnityScripts/QuizManager.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
// ==============================================
|
||||
// QuizManager.cs - Quiz-Steuerung
|
||||
// ==============================================
|
||||
// Verwaltet Quick-Fragen (waehrend Fahrt) und
|
||||
// Pause-Fragen (Spiel haelt an).
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class QuizManager : MonoBehaviour
|
||||
{
|
||||
public static QuizManager Instance { get; private set; }
|
||||
|
||||
[Header("UI Referenzen")]
|
||||
[SerializeField] private GameObject quickQuizPanel;
|
||||
[SerializeField] private GameObject pauseQuizPanel;
|
||||
[SerializeField] private TextMeshProUGUI questionText;
|
||||
[SerializeField] private Button[] answerButtons;
|
||||
[SerializeField] private TextMeshProUGUI timerText;
|
||||
[SerializeField] private Slider timerSlider;
|
||||
|
||||
[Header("Audio (fuer Audio-Version)")]
|
||||
[SerializeField] private AudioSource audioSource;
|
||||
[SerializeField] private bool isAudioMode = false;
|
||||
|
||||
[Header("Einstellungen")]
|
||||
[SerializeField] private int pointsCorrect = 500;
|
||||
[SerializeField] private int pointsWrong = -100;
|
||||
[SerializeField] private float pauseQuestionInterval = 500f; // Alle X Meter
|
||||
|
||||
// Aktueller Zustand
|
||||
private QuizQuestion currentQuestion;
|
||||
private bool isQuizActive = false;
|
||||
private float timeRemaining;
|
||||
private Coroutine timerCoroutine;
|
||||
|
||||
// Events
|
||||
public event Action<bool, int> OnQuestionAnswered; // (correct, points)
|
||||
public event Action OnQuizStarted;
|
||||
public event Action OnQuizEnded;
|
||||
|
||||
// Statistik
|
||||
private int questionsAnswered = 0;
|
||||
private int questionsCorrect = 0;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// UI initial verstecken
|
||||
if (quickQuizPanel) quickQuizPanel.SetActive(false);
|
||||
if (pauseQuizPanel) pauseQuizPanel.SetActive(false);
|
||||
|
||||
// Answer Buttons einrichten
|
||||
for (int i = 0; i < answerButtons.Length; i++)
|
||||
{
|
||||
int index = i; // Capture fuer Lambda
|
||||
answerButtons[i].onClick.AddListener(() => OnAnswerSelected(index));
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Keyboard-Eingabe fuer Antworten (1-4)
|
||||
if (isQuizActive && currentQuestion != null)
|
||||
{
|
||||
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
|
||||
OnAnswerSelected(0);
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
|
||||
OnAnswerSelected(1);
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
|
||||
OnAnswerSelected(2);
|
||||
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
|
||||
OnAnswerSelected(3);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Quick Quiz (waehrend der Fahrt)
|
||||
// ==============================================
|
||||
public void ShowQuickQuestion(string visualTrigger)
|
||||
{
|
||||
if (isQuizActive) return;
|
||||
|
||||
// Frage aus Cache holen
|
||||
int difficulty = BreakpilotAPI.Instance != null ?
|
||||
(int)GetCurrentDifficulty() : 3;
|
||||
|
||||
QuizQuestion question = BreakpilotAPI.Instance?.GetQuestionForTrigger(visualTrigger, difficulty);
|
||||
|
||||
if (question == null)
|
||||
{
|
||||
Debug.LogWarning($"Keine Frage fuer Trigger '{visualTrigger}' gefunden");
|
||||
return;
|
||||
}
|
||||
|
||||
currentQuestion = question;
|
||||
isQuizActive = true;
|
||||
|
||||
// UI anzeigen
|
||||
if (quickQuizPanel) quickQuizPanel.SetActive(true);
|
||||
DisplayQuestion(question);
|
||||
|
||||
// Timer starten
|
||||
float timeLimit = question.time_limit_seconds > 0 ? question.time_limit_seconds : 5f;
|
||||
timerCoroutine = StartCoroutine(QuickQuizTimer(timeLimit));
|
||||
|
||||
// Audio-Version: Frage vorlesen
|
||||
if (isAudioMode)
|
||||
{
|
||||
SpeakQuestion(question);
|
||||
}
|
||||
|
||||
OnQuizStarted?.Invoke();
|
||||
}
|
||||
|
||||
private IEnumerator QuickQuizTimer(float duration)
|
||||
{
|
||||
timeRemaining = duration;
|
||||
|
||||
while (timeRemaining > 0)
|
||||
{
|
||||
timeRemaining -= Time.deltaTime;
|
||||
|
||||
// UI updaten
|
||||
if (timerText) timerText.text = $"{timeRemaining:F1}s";
|
||||
if (timerSlider) timerSlider.value = timeRemaining / duration;
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Zeit abgelaufen = falsche Antwort
|
||||
OnAnswerSelected(-1);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Pause Quiz (Spiel haelt an)
|
||||
// ==============================================
|
||||
public void ShowPauseQuestion(QuizQuestion question)
|
||||
{
|
||||
if (isQuizActive) return;
|
||||
|
||||
currentQuestion = question;
|
||||
isQuizActive = true;
|
||||
|
||||
// Spiel pausieren
|
||||
Time.timeScale = 0;
|
||||
|
||||
// UI anzeigen
|
||||
if (pauseQuizPanel) pauseQuizPanel.SetActive(true);
|
||||
DisplayQuestion(question);
|
||||
|
||||
// Audio-Version: Frage vorlesen
|
||||
if (isAudioMode)
|
||||
{
|
||||
SpeakQuestion(question);
|
||||
}
|
||||
|
||||
OnQuizStarted?.Invoke();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Antwort verarbeiten
|
||||
// ==============================================
|
||||
private void OnAnswerSelected(int index)
|
||||
{
|
||||
if (!isQuizActive || currentQuestion == null) return;
|
||||
|
||||
// Timer stoppen
|
||||
if (timerCoroutine != null)
|
||||
{
|
||||
StopCoroutine(timerCoroutine);
|
||||
timerCoroutine = null;
|
||||
}
|
||||
|
||||
bool isCorrect = index == currentQuestion.correct_index;
|
||||
int points = isCorrect ? pointsCorrect : pointsWrong;
|
||||
|
||||
questionsAnswered++;
|
||||
if (isCorrect) questionsCorrect++;
|
||||
|
||||
// Visuelles Feedback
|
||||
ShowAnswerFeedback(isCorrect, currentQuestion.correct_index);
|
||||
|
||||
// Audio Feedback
|
||||
if (isAudioMode)
|
||||
{
|
||||
SpeakFeedback(isCorrect);
|
||||
}
|
||||
|
||||
// Event ausloesen
|
||||
OnQuestionAnswered?.Invoke(isCorrect, points);
|
||||
|
||||
// Quiz beenden (mit kurzer Verzoegerung fuer Feedback)
|
||||
StartCoroutine(EndQuizAfterDelay(isCorrect ? 0.5f : 1f));
|
||||
}
|
||||
|
||||
private IEnumerator EndQuizAfterDelay(float delay)
|
||||
{
|
||||
// Bei pausiertem Spiel: unscaled Zeit nutzen
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
|
||||
EndQuiz();
|
||||
}
|
||||
|
||||
private void EndQuiz()
|
||||
{
|
||||
isQuizActive = false;
|
||||
currentQuestion = null;
|
||||
|
||||
// UI verstecken
|
||||
if (quickQuizPanel) quickQuizPanel.SetActive(false);
|
||||
if (pauseQuizPanel) pauseQuizPanel.SetActive(false);
|
||||
|
||||
// Spiel fortsetzen (falls pausiert)
|
||||
Time.timeScale = 1;
|
||||
|
||||
OnQuizEnded?.Invoke();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// UI Helper
|
||||
// ==============================================
|
||||
private void DisplayQuestion(QuizQuestion question)
|
||||
{
|
||||
if (questionText) questionText.text = question.question_text;
|
||||
|
||||
for (int i = 0; i < answerButtons.Length; i++)
|
||||
{
|
||||
if (i < question.options.Length)
|
||||
{
|
||||
answerButtons[i].gameObject.SetActive(true);
|
||||
var buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (buttonText) buttonText.text = $"[{i + 1}] {question.options[i]}";
|
||||
}
|
||||
else
|
||||
{
|
||||
answerButtons[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowAnswerFeedback(bool correct, int correctIndex)
|
||||
{
|
||||
// Richtige Antwort gruen markieren
|
||||
if (correctIndex >= 0 && correctIndex < answerButtons.Length)
|
||||
{
|
||||
var colors = answerButtons[correctIndex].colors;
|
||||
colors.normalColor = Color.green;
|
||||
answerButtons[correctIndex].colors = colors;
|
||||
}
|
||||
|
||||
// TODO: Animation, Sound, Partikel
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Audio-Version
|
||||
// ==============================================
|
||||
private void SpeakQuestion(QuizQuestion question)
|
||||
{
|
||||
// TODO: Text-to-Speech Integration
|
||||
// Option 1: Voraufgenommene Audio-Dateien
|
||||
// Option 2: Web-TTS API
|
||||
// Option 3: Unity Speech Synthesis (platform-abhaengig)
|
||||
|
||||
Debug.Log($"[TTS] {question.question_text}");
|
||||
for (int i = 0; i < question.options.Length; i++)
|
||||
{
|
||||
Debug.Log($"[TTS] {i + 1} fuer {question.options[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
private void SpeakFeedback(bool correct)
|
||||
{
|
||||
string message = correct ? "Richtig! Gut gemacht!" : "Nicht ganz. Versuch es nochmal!";
|
||||
Debug.Log($"[TTS] {message}");
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Statistik
|
||||
// ==============================================
|
||||
public int GetQuestionsAnswered() => questionsAnswered;
|
||||
public int GetQuestionsCorrect() => questionsCorrect;
|
||||
|
||||
public float GetAccuracy()
|
||||
{
|
||||
if (questionsAnswered == 0) return 0;
|
||||
return (float)questionsCorrect / questionsAnswered;
|
||||
}
|
||||
|
||||
public void ResetStatistics()
|
||||
{
|
||||
questionsAnswered = 0;
|
||||
questionsCorrect = 0;
|
||||
}
|
||||
|
||||
private float GetCurrentDifficulty()
|
||||
{
|
||||
// TODO: Aus GameManager holen
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
248
breakpilot-drive/UnityScripts/Track/ObstacleSpawner.cs
Normal file
248
breakpilot-drive/UnityScripts/Track/ObstacleSpawner.cs
Normal file
@@ -0,0 +1,248 @@
|
||||
// ==============================================
|
||||
// ObstacleSpawner.cs - Hindernis-Generator
|
||||
// ==============================================
|
||||
// Spawnt Hindernisse, Items und Quiz-Trigger
|
||||
// basierend auf Schwierigkeitsgrad.
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class ObstacleSpawner : MonoBehaviour
|
||||
{
|
||||
public static ObstacleSpawner Instance { get; private set; }
|
||||
|
||||
[Header("Hindernis-Prefabs")]
|
||||
[SerializeField] private GameObject[] obstaclePrefabs; // Verschiedene Hindernisse
|
||||
[SerializeField] private GameObject[] largObstaclePrefabs; // Grosse Hindernisse (2 Spuren)
|
||||
|
||||
[Header("Item-Prefabs")]
|
||||
[SerializeField] private GameObject coinPrefab;
|
||||
[SerializeField] private GameObject starPrefab;
|
||||
[SerializeField] private GameObject shieldPrefab;
|
||||
|
||||
[Header("Quiz-Trigger-Prefabs")]
|
||||
[SerializeField] private GameObject bridgePrefab;
|
||||
[SerializeField] private GameObject treePrefab;
|
||||
[SerializeField] private GameObject housePrefab;
|
||||
|
||||
[Header("Spawn-Einstellungen")]
|
||||
[SerializeField] private float laneDistance = 3f;
|
||||
[SerializeField] private float minObstacleSpacing = 10f;
|
||||
[SerializeField] private float maxObstacleSpacing = 30f;
|
||||
|
||||
[Header("Wahrscheinlichkeiten (0-1)")]
|
||||
[SerializeField] private float obstacleChance = 0.6f;
|
||||
[SerializeField] private float coinChance = 0.3f;
|
||||
[SerializeField] private float starChance = 0.05f;
|
||||
[SerializeField] private float shieldChance = 0.02f;
|
||||
[SerializeField] private float quizTriggerChance = 0.1f;
|
||||
|
||||
// Interner Zustand
|
||||
private float lastObstacleZ = 0f;
|
||||
private int quizTriggerCount = 0;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Wahrscheinlichkeiten von Difficulty laden
|
||||
UpdateFromDifficulty();
|
||||
}
|
||||
|
||||
private void UpdateFromDifficulty()
|
||||
{
|
||||
if (GameManager.Instance?.DifficultySettings != null)
|
||||
{
|
||||
var settings = GameManager.Instance.DifficultySettings;
|
||||
obstacleChance = settings.obstacle_frequency;
|
||||
// Power-Up Chance beeinflusst Items
|
||||
float powerUpChance = settings.power_up_chance;
|
||||
coinChance = powerUpChance * 0.8f;
|
||||
starChance = powerUpChance * 0.15f;
|
||||
shieldChance = powerUpChance * 0.05f;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spawning auf Segment
|
||||
// ==============================================
|
||||
public void SpawnOnSegment(GameObject segment, float segmentStartZ)
|
||||
{
|
||||
float segmentLength = 50f; // Sollte mit TrackGenerator uebereinstimmen
|
||||
float currentZ = segmentStartZ + minObstacleSpacing;
|
||||
|
||||
while (currentZ < segmentStartZ + segmentLength - minObstacleSpacing)
|
||||
{
|
||||
// Zufaelliger Abstand zum naechsten Objekt
|
||||
float spacing = Random.Range(minObstacleSpacing, maxObstacleSpacing);
|
||||
|
||||
// Was spawnen?
|
||||
SpawnAtPosition(segment.transform, currentZ);
|
||||
|
||||
currentZ += spacing;
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnAtPosition(Transform parent, float zPosition)
|
||||
{
|
||||
float roll = Random.value;
|
||||
|
||||
// Quiz-Trigger (selten, aber wichtig)
|
||||
if (roll < quizTriggerChance && quizTriggerCount < 3)
|
||||
{
|
||||
SpawnQuizTrigger(parent, zPosition);
|
||||
quizTriggerCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
// Hindernis
|
||||
roll = Random.value;
|
||||
if (roll < obstacleChance)
|
||||
{
|
||||
SpawnObstacle(parent, zPosition);
|
||||
}
|
||||
|
||||
// Items (koennen neben Hindernissen sein)
|
||||
roll = Random.value;
|
||||
if (roll < coinChance)
|
||||
{
|
||||
SpawnCoinRow(parent, zPosition);
|
||||
}
|
||||
else if (roll < coinChance + starChance)
|
||||
{
|
||||
SpawnItem(starPrefab, parent, zPosition, GetRandomLane());
|
||||
}
|
||||
else if (roll < coinChance + starChance + shieldChance)
|
||||
{
|
||||
SpawnItem(shieldPrefab, parent, zPosition, GetRandomLane());
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Hindernisse
|
||||
// ==============================================
|
||||
private void SpawnObstacle(Transform parent, float zPosition)
|
||||
{
|
||||
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0) return;
|
||||
|
||||
// Zufaelliges Hindernis und Spur waehlen
|
||||
int prefabIndex = Random.Range(0, obstaclePrefabs.Length);
|
||||
int lane = GetRandomLane();
|
||||
|
||||
// Manchmal grosses Hindernis (blockiert 2 Spuren)
|
||||
if (largObstaclePrefabs != null && largObstaclePrefabs.Length > 0 && Random.value < 0.2f)
|
||||
{
|
||||
prefabIndex = Random.Range(0, largObstaclePrefabs.Length);
|
||||
SpawnObstacleAtLane(largObstaclePrefabs[prefabIndex], parent, zPosition, lane);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpawnObstacleAtLane(obstaclePrefabs[prefabIndex], parent, zPosition, lane);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnObstacleAtLane(GameObject prefab, Transform parent, float zPosition, int lane)
|
||||
{
|
||||
float xPos = (lane - 1) * laneDistance;
|
||||
Vector3 position = new Vector3(xPos, 0, zPosition);
|
||||
|
||||
GameObject obstacle = Instantiate(prefab, parent);
|
||||
obstacle.transform.localPosition = position;
|
||||
obstacle.tag = "Obstacle";
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Items
|
||||
// ==============================================
|
||||
private void SpawnItem(GameObject prefab, Transform parent, float zPosition, int lane)
|
||||
{
|
||||
if (prefab == null) return;
|
||||
|
||||
float xPos = (lane - 1) * laneDistance;
|
||||
Vector3 position = new Vector3(xPos, 0.5f, zPosition); // Leicht ueber dem Boden
|
||||
|
||||
GameObject item = Instantiate(prefab, parent);
|
||||
item.transform.localPosition = position;
|
||||
}
|
||||
|
||||
private void SpawnCoinRow(Transform parent, float zPosition)
|
||||
{
|
||||
if (coinPrefab == null) return;
|
||||
|
||||
int lane = GetRandomLane();
|
||||
int coinCount = Random.Range(3, 6);
|
||||
|
||||
for (int i = 0; i < coinCount; i++)
|
||||
{
|
||||
SpawnItem(coinPrefab, parent, zPosition + (i * 2f), lane);
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Quiz-Trigger
|
||||
// ==============================================
|
||||
private void SpawnQuizTrigger(Transform parent, float zPosition)
|
||||
{
|
||||
GameObject prefab = GetRandomTriggerPrefab();
|
||||
if (prefab == null) return;
|
||||
|
||||
// Trigger sind meist in der Mitte oder ueber der Strasse
|
||||
Vector3 position = new Vector3(0, 0, zPosition);
|
||||
|
||||
GameObject trigger = Instantiate(prefab, parent);
|
||||
trigger.transform.localPosition = position;
|
||||
trigger.tag = "QuizTrigger";
|
||||
|
||||
// VisualTrigger-Komponente hinzufuegen falls nicht vorhanden
|
||||
if (trigger.GetComponent<VisualTrigger>() == null)
|
||||
{
|
||||
VisualTrigger vt = trigger.AddComponent<VisualTrigger>();
|
||||
vt.TriggerType = GetTriggerTypeFromPrefab(prefab);
|
||||
}
|
||||
}
|
||||
|
||||
private GameObject GetRandomTriggerPrefab()
|
||||
{
|
||||
GameObject[] triggers = new GameObject[] { bridgePrefab, treePrefab, housePrefab };
|
||||
var validTriggers = System.Array.FindAll(triggers, t => t != null);
|
||||
|
||||
if (validTriggers.Length == 0) return null;
|
||||
|
||||
return validTriggers[Random.Range(0, validTriggers.Length)];
|
||||
}
|
||||
|
||||
private string GetTriggerTypeFromPrefab(GameObject prefab)
|
||||
{
|
||||
if (prefab == bridgePrefab) return "bridge";
|
||||
if (prefab == treePrefab) return "tree";
|
||||
if (prefab == housePrefab) return "house";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Helper
|
||||
// ==============================================
|
||||
private int GetRandomLane()
|
||||
{
|
||||
return Random.Range(0, 3); // 0, 1, oder 2
|
||||
}
|
||||
|
||||
public void ResetSpawner()
|
||||
{
|
||||
lastObstacleZ = 0f;
|
||||
quizTriggerCount = 0;
|
||||
UpdateFromDifficulty();
|
||||
}
|
||||
}
|
||||
}
|
||||
233
breakpilot-drive/UnityScripts/Track/TrackGenerator.cs
Normal file
233
breakpilot-drive/UnityScripts/Track/TrackGenerator.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
// ==============================================
|
||||
// TrackGenerator.cs - Endlose Streckengenerierung
|
||||
// ==============================================
|
||||
// Erzeugt endlose Streckenabschnitte durch
|
||||
// Object-Pooling und Recycling.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class TrackGenerator : MonoBehaviour
|
||||
{
|
||||
public static TrackGenerator Instance { get; private set; }
|
||||
|
||||
[Header("Track Prefabs")]
|
||||
[SerializeField] private GameObject[] trackSegmentPrefabs; // Verschiedene Streckenabschnitte
|
||||
[SerializeField] private GameObject startSegmentPrefab; // Start-Segment
|
||||
|
||||
[Header("Generierung")]
|
||||
[SerializeField] private float segmentLength = 50f; // Laenge eines Segments
|
||||
[SerializeField] private int visibleSegments = 5; // Anzahl sichtbarer Segmente
|
||||
[SerializeField] private float despawnDistance = -20f; // Wann Segment recycelt wird
|
||||
|
||||
[Header("Bewegung")]
|
||||
[SerializeField] private float baseSpeed = 10f; // Basis-Geschwindigkeit
|
||||
private float currentSpeed;
|
||||
|
||||
// Object Pool
|
||||
private List<GameObject> activeSegments = new List<GameObject>();
|
||||
private Queue<GameObject> segmentPool = new Queue<GameObject>();
|
||||
|
||||
// Position
|
||||
private float nextSpawnZ = 0f;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
currentSpeed = baseSpeed;
|
||||
|
||||
// Geschwindigkeit von Difficulty laden
|
||||
if (GameManager.Instance?.DifficultySettings != null)
|
||||
{
|
||||
currentSpeed = GameManager.Instance.DifficultySettings.lane_speed;
|
||||
}
|
||||
|
||||
// Initiale Segmente erstellen
|
||||
InitializeTrack();
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (GameManager.Instance?.CurrentState != GameState.Playing)
|
||||
return;
|
||||
|
||||
// Segmente bewegen
|
||||
MoveSegments();
|
||||
|
||||
// Alte Segmente recyceln
|
||||
RecycleSegments();
|
||||
|
||||
// Neue Segmente spawnen
|
||||
SpawnSegmentsIfNeeded();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Initialisierung
|
||||
// ==============================================
|
||||
private void InitializeTrack()
|
||||
{
|
||||
nextSpawnZ = 0f;
|
||||
|
||||
// Start-Segment
|
||||
if (startSegmentPrefab != null)
|
||||
{
|
||||
SpawnSegment(startSegmentPrefab);
|
||||
}
|
||||
|
||||
// Weitere Segmente fuer sichtbaren Bereich
|
||||
for (int i = 0; i < visibleSegments; i++)
|
||||
{
|
||||
SpawnRandomSegment();
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Segment-Spawning
|
||||
// ==============================================
|
||||
private void SpawnRandomSegment()
|
||||
{
|
||||
if (trackSegmentPrefabs == null || trackSegmentPrefabs.Length == 0)
|
||||
{
|
||||
Debug.LogWarning("Keine Track-Segment-Prefabs zugewiesen!");
|
||||
return;
|
||||
}
|
||||
|
||||
int index = Random.Range(0, trackSegmentPrefabs.Length);
|
||||
SpawnSegment(trackSegmentPrefabs[index]);
|
||||
}
|
||||
|
||||
private void SpawnSegment(GameObject prefab)
|
||||
{
|
||||
GameObject segment;
|
||||
|
||||
// Aus Pool holen oder neu erstellen
|
||||
if (segmentPool.Count > 0)
|
||||
{
|
||||
segment = segmentPool.Dequeue();
|
||||
segment.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
segment = Instantiate(prefab, transform);
|
||||
}
|
||||
|
||||
// Position setzen
|
||||
segment.transform.position = new Vector3(0, 0, nextSpawnZ);
|
||||
nextSpawnZ += segmentLength;
|
||||
|
||||
activeSegments.Add(segment);
|
||||
|
||||
// Hindernisse und Items auf Segment spawnen
|
||||
ObstacleSpawner.Instance?.SpawnOnSegment(segment, nextSpawnZ - segmentLength);
|
||||
}
|
||||
|
||||
private void SpawnSegmentsIfNeeded()
|
||||
{
|
||||
// Pruefe ob wir mehr Segmente brauchen
|
||||
float playerZ = 0; // Spieler ist bei Z=0, Welt bewegt sich
|
||||
float maxVisibleZ = playerZ + (visibleSegments * segmentLength);
|
||||
|
||||
while (nextSpawnZ < maxVisibleZ)
|
||||
{
|
||||
SpawnRandomSegment();
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Bewegung
|
||||
// ==============================================
|
||||
private void MoveSegments()
|
||||
{
|
||||
Vector3 movement = Vector3.back * currentSpeed * Time.deltaTime;
|
||||
|
||||
foreach (var segment in activeSegments)
|
||||
{
|
||||
segment.transform.position += movement;
|
||||
}
|
||||
|
||||
// Auch nextSpawnZ anpassen
|
||||
nextSpawnZ -= currentSpeed * Time.deltaTime;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Recycling
|
||||
// ==============================================
|
||||
private void RecycleSegments()
|
||||
{
|
||||
for (int i = activeSegments.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (activeSegments[i].transform.position.z < despawnDistance)
|
||||
{
|
||||
RecycleSegment(activeSegments[i]);
|
||||
activeSegments.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RecycleSegment(GameObject segment)
|
||||
{
|
||||
// Kinder-Objekte (Hindernisse, Items) entfernen
|
||||
foreach (Transform child in segment.transform)
|
||||
{
|
||||
if (child.CompareTag("Obstacle") || child.CompareTag("Coin") ||
|
||||
child.CompareTag("Star") || child.CompareTag("QuizTrigger"))
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
// Segment deaktivieren und zurueck in Pool
|
||||
segment.SetActive(false);
|
||||
segmentPool.Enqueue(segment);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Geschwindigkeit
|
||||
// ==============================================
|
||||
public void SetSpeed(float speed)
|
||||
{
|
||||
currentSpeed = speed;
|
||||
}
|
||||
|
||||
public float GetSpeed()
|
||||
{
|
||||
return currentSpeed;
|
||||
}
|
||||
|
||||
public void IncreaseSpeed(float amount)
|
||||
{
|
||||
currentSpeed += amount;
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Reset
|
||||
// ==============================================
|
||||
public void ResetTrack()
|
||||
{
|
||||
// Alle aktiven Segmente recyceln
|
||||
foreach (var segment in activeSegments)
|
||||
{
|
||||
RecycleSegment(segment);
|
||||
}
|
||||
activeSegments.Clear();
|
||||
|
||||
// Neu initialisieren
|
||||
nextSpawnZ = 0f;
|
||||
currentSpeed = baseSpeed;
|
||||
InitializeTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
57
breakpilot-drive/UnityScripts/Track/VisualTrigger.cs
Normal file
57
breakpilot-drive/UnityScripts/Track/VisualTrigger.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
// ==============================================
|
||||
// VisualTrigger.cs - Quiz-Trigger Objekt
|
||||
// ==============================================
|
||||
// Loest Quick-Quiz aus wenn Spieler durchfaehrt.
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class VisualTrigger : MonoBehaviour
|
||||
{
|
||||
[Header("Trigger-Konfiguration")]
|
||||
[SerializeField] private string triggerType = "bridge"; // "bridge", "tree", "house"
|
||||
|
||||
public string TriggerType
|
||||
{
|
||||
get => triggerType;
|
||||
set => triggerType = value;
|
||||
}
|
||||
|
||||
private bool hasTriggered = false;
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (hasTriggered) return;
|
||||
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
hasTriggered = true;
|
||||
|
||||
// Quick-Quiz ausloesen
|
||||
if (QuizManager.Instance != null)
|
||||
{
|
||||
QuizManager.Instance.ShowQuickQuestion(triggerType);
|
||||
}
|
||||
|
||||
Debug.Log($"Quiz-Trigger ausgeloest: {triggerType}");
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerExit(Collider other)
|
||||
{
|
||||
// Reset fuer naechsten Durchlauf (falls Objekt recycelt wird)
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
// Optional: Trigger nach kurzer Zeit wieder aktivieren
|
||||
// hasTriggered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wird aufgerufen wenn Objekt recycelt wird
|
||||
void OnDisable()
|
||||
{
|
||||
hasTriggered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
144
breakpilot-drive/UnityScripts/UI/GameHUD.cs
Normal file
144
breakpilot-drive/UnityScripts/UI/GameHUD.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
// ==============================================
|
||||
// GameHUD.cs - Spiel-UI Anzeige
|
||||
// ==============================================
|
||||
// Zeigt Score, Leben, Distanz und andere
|
||||
// Spielinformationen an.
|
||||
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class GameHUD : MonoBehaviour
|
||||
{
|
||||
[Header("Score")]
|
||||
[SerializeField] private TextMeshProUGUI scoreText;
|
||||
[SerializeField] private TextMeshProUGUI highScoreText;
|
||||
|
||||
[Header("Leben")]
|
||||
[SerializeField] private TextMeshProUGUI livesText;
|
||||
[SerializeField] private Image[] heartImages;
|
||||
|
||||
[Header("Distanz")]
|
||||
[SerializeField] private TextMeshProUGUI distanceText;
|
||||
|
||||
[Header("Quiz-Statistik")]
|
||||
[SerializeField] private TextMeshProUGUI quizStatsText;
|
||||
|
||||
[Header("Animationen")]
|
||||
[SerializeField] private Animator scoreAnimator;
|
||||
|
||||
private int displayedScore = 0;
|
||||
private int targetScore = 0;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Events abonnieren
|
||||
if (GameManager.Instance != null)
|
||||
{
|
||||
GameManager.Instance.OnScoreChanged += UpdateScore;
|
||||
GameManager.Instance.OnLivesChanged += UpdateLives;
|
||||
GameManager.Instance.OnDistanceChanged += UpdateDistance;
|
||||
}
|
||||
|
||||
// Initial-Werte setzen
|
||||
UpdateScore(0);
|
||||
UpdateLives(3);
|
||||
UpdateDistance(0);
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
// Events abbestellen
|
||||
if (GameManager.Instance != null)
|
||||
{
|
||||
GameManager.Instance.OnScoreChanged -= UpdateScore;
|
||||
GameManager.Instance.OnLivesChanged -= UpdateLives;
|
||||
GameManager.Instance.OnDistanceChanged -= UpdateDistance;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Score-Animation (zaehlt hoch)
|
||||
if (displayedScore != targetScore)
|
||||
{
|
||||
displayedScore = (int)Mathf.MoveTowards(displayedScore, targetScore, Time.deltaTime * 1000);
|
||||
if (scoreText != null)
|
||||
{
|
||||
scoreText.text = displayedScore.ToString("N0");
|
||||
}
|
||||
}
|
||||
|
||||
// Quiz-Statistik aktualisieren
|
||||
UpdateQuizStats();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Score
|
||||
// ==============================================
|
||||
private void UpdateScore(int newScore)
|
||||
{
|
||||
targetScore = newScore;
|
||||
|
||||
// Animation ausloesen bei Punkte-Gewinn
|
||||
if (newScore > displayedScore && scoreAnimator != null)
|
||||
{
|
||||
scoreAnimator.SetTrigger("ScoreUp");
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Leben
|
||||
// ==============================================
|
||||
private void UpdateLives(int lives)
|
||||
{
|
||||
if (livesText != null)
|
||||
{
|
||||
livesText.text = $"x{lives}";
|
||||
}
|
||||
|
||||
// Herz-Icons aktualisieren
|
||||
if (heartImages != null)
|
||||
{
|
||||
for (int i = 0; i < heartImages.Length; i++)
|
||||
{
|
||||
heartImages[i].enabled = i < lives;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Distanz
|
||||
// ==============================================
|
||||
private void UpdateDistance(float distance)
|
||||
{
|
||||
if (distanceText != null)
|
||||
{
|
||||
distanceText.text = $"{distance:F0}m";
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Quiz-Statistik
|
||||
// ==============================================
|
||||
private void UpdateQuizStats()
|
||||
{
|
||||
if (quizStatsText == null || QuizManager.Instance == null) return;
|
||||
|
||||
int answered = QuizManager.Instance.GetQuestionsAnswered();
|
||||
int correct = QuizManager.Instance.GetQuestionsCorrect();
|
||||
|
||||
if (answered > 0)
|
||||
{
|
||||
float accuracy = QuizManager.Instance.GetAccuracy() * 100;
|
||||
quizStatsText.text = $"Quiz: {correct}/{answered} ({accuracy:F0}%)";
|
||||
}
|
||||
else
|
||||
{
|
||||
quizStatsText.text = "Quiz: 0/0";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
180
breakpilot-drive/UnityScripts/UI/MainMenu.cs
Normal file
180
breakpilot-drive/UnityScripts/UI/MainMenu.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
// ==============================================
|
||||
// MainMenu.cs - Hauptmenue Steuerung
|
||||
// ==============================================
|
||||
// Verwaltet das Startmenue mit Spielmodus-Auswahl
|
||||
// und Einstellungen.
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using TMPro;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class MainMenu : MonoBehaviour
|
||||
{
|
||||
[Header("UI Elemente")]
|
||||
[SerializeField] private GameObject mainPanel;
|
||||
[SerializeField] private GameObject settingsPanel;
|
||||
[SerializeField] private GameObject loadingPanel;
|
||||
|
||||
[Header("Benutzer-Info")]
|
||||
[SerializeField] private TMP_InputField userIdInput;
|
||||
[SerializeField] private TextMeshProUGUI levelText;
|
||||
[SerializeField] private TextMeshProUGUI welcomeText;
|
||||
|
||||
[Header("Scene-Namen")]
|
||||
[SerializeField] private string videoGameScene = "Game_Video";
|
||||
[SerializeField] private string audioGameScene = "Game_Audio";
|
||||
|
||||
// Benutzer-Daten
|
||||
private string currentUserId = "guest";
|
||||
private LearningLevel currentLevel;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Panels initialisieren
|
||||
if (mainPanel) mainPanel.SetActive(true);
|
||||
if (settingsPanel) settingsPanel.SetActive(false);
|
||||
if (loadingPanel) loadingPanel.SetActive(false);
|
||||
|
||||
// Gespeicherte User-ID laden
|
||||
currentUserId = PlayerPrefs.GetString("UserId", "guest");
|
||||
if (userIdInput != null)
|
||||
{
|
||||
userIdInput.text = currentUserId;
|
||||
}
|
||||
|
||||
// Lernniveau laden
|
||||
LoadUserLevel();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Benutzer-Management
|
||||
// ==============================================
|
||||
public void OnUserIdChanged(string newUserId)
|
||||
{
|
||||
currentUserId = string.IsNullOrEmpty(newUserId) ? "guest" : newUserId;
|
||||
PlayerPrefs.SetString("UserId", currentUserId);
|
||||
PlayerPrefs.Save();
|
||||
|
||||
LoadUserLevel();
|
||||
}
|
||||
|
||||
private void LoadUserLevel()
|
||||
{
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
StartCoroutine(BreakpilotAPI.Instance.GetLearningLevel(currentUserId,
|
||||
onSuccess: (level) =>
|
||||
{
|
||||
currentLevel = level;
|
||||
UpdateLevelDisplay();
|
||||
},
|
||||
onError: (error) =>
|
||||
{
|
||||
Debug.LogWarning($"Lernniveau konnte nicht geladen werden: {error}");
|
||||
// Fallback-Level
|
||||
currentLevel = new LearningLevel { overall_level = 3 };
|
||||
UpdateLevelDisplay();
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateLevelDisplay()
|
||||
{
|
||||
if (levelText != null && currentLevel != null)
|
||||
{
|
||||
levelText.text = $"Level {currentLevel.overall_level}";
|
||||
}
|
||||
|
||||
if (welcomeText != null)
|
||||
{
|
||||
string name = currentUserId == "guest" ? "Gast" : currentUserId;
|
||||
welcomeText.text = $"Hallo, {name}!";
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Spielstart
|
||||
// ==============================================
|
||||
public void PlayVideoMode()
|
||||
{
|
||||
StartGame(videoGameScene);
|
||||
}
|
||||
|
||||
public void PlayAudioMode()
|
||||
{
|
||||
StartGame(audioGameScene);
|
||||
}
|
||||
|
||||
private void StartGame(string sceneName)
|
||||
{
|
||||
// Loading anzeigen
|
||||
if (loadingPanel) loadingPanel.SetActive(true);
|
||||
if (mainPanel) mainPanel.SetActive(false);
|
||||
|
||||
// Fragen vorladen
|
||||
if (BreakpilotAPI.Instance != null)
|
||||
{
|
||||
int difficulty = currentLevel?.overall_level ?? 3;
|
||||
StartCoroutine(BreakpilotAPI.Instance.GetQuizQuestions(
|
||||
difficulty: difficulty,
|
||||
count: 20,
|
||||
onSuccess: (questions) =>
|
||||
{
|
||||
Debug.Log($"{questions.Length} Fragen vorgeladen");
|
||||
LoadScene(sceneName);
|
||||
},
|
||||
onError: (error) =>
|
||||
{
|
||||
Debug.LogWarning($"Fragen konnten nicht geladen werden: {error}");
|
||||
// Trotzdem starten (Offline-Modus)
|
||||
LoadScene(sceneName);
|
||||
}
|
||||
));
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadScene(sceneName);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadScene(string sceneName)
|
||||
{
|
||||
SceneManager.LoadScene(sceneName);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Einstellungen
|
||||
// ==============================================
|
||||
public void OpenSettings()
|
||||
{
|
||||
if (mainPanel) mainPanel.SetActive(false);
|
||||
if (settingsPanel) settingsPanel.SetActive(true);
|
||||
}
|
||||
|
||||
public void CloseSettings()
|
||||
{
|
||||
if (settingsPanel) settingsPanel.SetActive(false);
|
||||
if (mainPanel) mainPanel.SetActive(true);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Sonstiges
|
||||
// ==============================================
|
||||
public void QuitGame()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
UnityEditor.EditorApplication.isPlaying = false;
|
||||
#else
|
||||
Application.Quit();
|
||||
#endif
|
||||
}
|
||||
|
||||
public void OpenWebsite()
|
||||
{
|
||||
Application.OpenURL("https://breakpilot.app");
|
||||
}
|
||||
}
|
||||
}
|
||||
340
breakpilot-drive/UnityScripts/UI/QuizOverlay.cs
Normal file
340
breakpilot-drive/UnityScripts/UI/QuizOverlay.cs
Normal file
@@ -0,0 +1,340 @@
|
||||
// ==============================================
|
||||
// QuizOverlay.cs - Quiz-UI Anzeige
|
||||
// ==============================================
|
||||
// Zeigt Quiz-Fragen an (Quick und Pause Mode)
|
||||
// mit Timer, Antwort-Buttons und Feedback.
|
||||
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
namespace BreakpilotDrive
|
||||
{
|
||||
public class QuizOverlay : MonoBehaviour
|
||||
{
|
||||
[Header("Panels")]
|
||||
[SerializeField] private GameObject quickQuizPanel;
|
||||
[SerializeField] private GameObject pauseQuizPanel;
|
||||
[SerializeField] private GameObject feedbackPanel;
|
||||
|
||||
[Header("Quick Quiz UI")]
|
||||
[SerializeField] private TextMeshProUGUI quickQuestionText;
|
||||
[SerializeField] private Button[] quickAnswerButtons;
|
||||
[SerializeField] private Slider timerSlider;
|
||||
[SerializeField] private TextMeshProUGUI timerText;
|
||||
|
||||
[Header("Pause Quiz UI")]
|
||||
[SerializeField] private TextMeshProUGUI pauseQuestionText;
|
||||
[SerializeField] private Button[] pauseAnswerButtons;
|
||||
[SerializeField] private TextMeshProUGUI pauseHintText;
|
||||
|
||||
[Header("Feedback UI")]
|
||||
[SerializeField] private TextMeshProUGUI feedbackText;
|
||||
[SerializeField] private Image feedbackIcon;
|
||||
[SerializeField] private Sprite correctIcon;
|
||||
[SerializeField] private Sprite wrongIcon;
|
||||
|
||||
[Header("Farben")]
|
||||
[SerializeField] private Color normalColor = Color.white;
|
||||
[SerializeField] private Color correctColor = Color.green;
|
||||
[SerializeField] private Color wrongColor = Color.red;
|
||||
[SerializeField] private Color selectedColor = Color.yellow;
|
||||
|
||||
[Header("Animation")]
|
||||
[SerializeField] private Animator quizAnimator;
|
||||
[SerializeField] private float feedbackDuration = 1.5f;
|
||||
|
||||
// Aktuelle Frage
|
||||
private QuizQuestion currentQuestion;
|
||||
private int selectedAnswer = -1;
|
||||
private bool isAnswered = false;
|
||||
|
||||
void Start()
|
||||
{
|
||||
// Panels verstecken
|
||||
HideAll();
|
||||
|
||||
// QuizManager Events abonnieren
|
||||
if (QuizManager.Instance != null)
|
||||
{
|
||||
QuizManager.Instance.OnQuizStarted += OnQuizStarted;
|
||||
QuizManager.Instance.OnQuizEnded += OnQuizEnded;
|
||||
QuizManager.Instance.OnQuestionAnswered += OnQuestionAnswered;
|
||||
}
|
||||
|
||||
// Button-Listener einrichten
|
||||
SetupButtons();
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (QuizManager.Instance != null)
|
||||
{
|
||||
QuizManager.Instance.OnQuizStarted -= OnQuizStarted;
|
||||
QuizManager.Instance.OnQuizEnded -= OnQuizEnded;
|
||||
QuizManager.Instance.OnQuestionAnswered -= OnQuestionAnswered;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Button Setup
|
||||
// ==============================================
|
||||
private void SetupButtons()
|
||||
{
|
||||
// Quick Answer Buttons
|
||||
for (int i = 0; i < quickAnswerButtons.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
quickAnswerButtons[i].onClick.AddListener(() => OnAnswerClicked(index));
|
||||
}
|
||||
|
||||
// Pause Answer Buttons
|
||||
for (int i = 0; i < pauseAnswerButtons.Length; i++)
|
||||
{
|
||||
int index = i;
|
||||
pauseAnswerButtons[i].onClick.AddListener(() => OnAnswerClicked(index));
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Quiz Events
|
||||
// ==============================================
|
||||
private void OnQuizStarted()
|
||||
{
|
||||
isAnswered = false;
|
||||
selectedAnswer = -1;
|
||||
}
|
||||
|
||||
private void OnQuizEnded()
|
||||
{
|
||||
StartCoroutine(HideAfterDelay(feedbackDuration));
|
||||
}
|
||||
|
||||
private void OnQuestionAnswered(bool correct, int points)
|
||||
{
|
||||
isAnswered = true;
|
||||
ShowFeedback(correct, points);
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Quick Quiz anzeigen
|
||||
// ==============================================
|
||||
public void ShowQuickQuiz(QuizQuestion question, float timeLimit)
|
||||
{
|
||||
currentQuestion = question;
|
||||
isAnswered = false;
|
||||
|
||||
// Panel aktivieren
|
||||
HideAll();
|
||||
if (quickQuizPanel) quickQuizPanel.SetActive(true);
|
||||
|
||||
// Frage anzeigen
|
||||
if (quickQuestionText) quickQuestionText.text = question.question_text;
|
||||
|
||||
// Antworten anzeigen
|
||||
for (int i = 0; i < quickAnswerButtons.Length; i++)
|
||||
{
|
||||
if (i < question.options.Length)
|
||||
{
|
||||
quickAnswerButtons[i].gameObject.SetActive(true);
|
||||
var text = quickAnswerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (text) text.text = $"[{i + 1}] {question.options[i]}";
|
||||
ResetButtonColor(quickAnswerButtons[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
quickAnswerButtons[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Timer starten
|
||||
StartCoroutine(UpdateTimer(timeLimit));
|
||||
|
||||
// Animation
|
||||
if (quizAnimator) quizAnimator.SetTrigger("ShowQuick");
|
||||
}
|
||||
|
||||
private IEnumerator UpdateTimer(float duration)
|
||||
{
|
||||
float timeRemaining = duration;
|
||||
|
||||
while (timeRemaining > 0 && !isAnswered)
|
||||
{
|
||||
timeRemaining -= Time.deltaTime;
|
||||
|
||||
// UI aktualisieren
|
||||
if (timerSlider) timerSlider.value = timeRemaining / duration;
|
||||
if (timerText) timerText.text = $"{timeRemaining:F1}s";
|
||||
|
||||
// Farbe aendern wenn Zeit knapp
|
||||
if (timerSlider && timeRemaining < duration * 0.3f)
|
||||
{
|
||||
timerSlider.fillRect.GetComponent<Image>().color = Color.red;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Pause Quiz anzeigen
|
||||
// ==============================================
|
||||
public void ShowPauseQuiz(QuizQuestion question)
|
||||
{
|
||||
currentQuestion = question;
|
||||
isAnswered = false;
|
||||
|
||||
// Panel aktivieren
|
||||
HideAll();
|
||||
if (pauseQuizPanel) pauseQuizPanel.SetActive(true);
|
||||
|
||||
// Frage anzeigen
|
||||
if (pauseQuestionText) pauseQuestionText.text = question.question_text;
|
||||
|
||||
// Hint anzeigen (falls aktiviert)
|
||||
if (pauseHintText)
|
||||
{
|
||||
bool hintsEnabled = GameManager.Instance?.DifficultySettings?.hints_enabled ?? false;
|
||||
pauseHintText.gameObject.SetActive(hintsEnabled);
|
||||
// TODO: Hint-Text generieren
|
||||
}
|
||||
|
||||
// Antworten anzeigen
|
||||
for (int i = 0; i < pauseAnswerButtons.Length; i++)
|
||||
{
|
||||
if (i < question.options.Length)
|
||||
{
|
||||
pauseAnswerButtons[i].gameObject.SetActive(true);
|
||||
var text = pauseAnswerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (text) text.text = $"[{i + 1}] {question.options[i]}";
|
||||
ResetButtonColor(pauseAnswerButtons[i]);
|
||||
}
|
||||
else
|
||||
{
|
||||
pauseAnswerButtons[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Animation
|
||||
if (quizAnimator) quizAnimator.SetTrigger("ShowPause");
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Antwort-Handling
|
||||
// ==============================================
|
||||
private void OnAnswerClicked(int index)
|
||||
{
|
||||
if (isAnswered || currentQuestion == null) return;
|
||||
|
||||
selectedAnswer = index;
|
||||
|
||||
// Button hervorheben
|
||||
Button[] buttons = currentQuestion.quiz_mode == "quick" ?
|
||||
quickAnswerButtons : pauseAnswerButtons;
|
||||
|
||||
if (index < buttons.Length)
|
||||
{
|
||||
HighlightButton(buttons[index]);
|
||||
}
|
||||
|
||||
// An QuizManager melden (wird dort verarbeitet)
|
||||
// QuizManager ruft dann OnQuestionAnswered auf
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Feedback anzeigen
|
||||
// ==============================================
|
||||
private void ShowFeedback(bool correct, int points)
|
||||
{
|
||||
// Richtige Antwort markieren
|
||||
Button[] buttons = currentQuestion?.quiz_mode == "quick" ?
|
||||
quickAnswerButtons : pauseAnswerButtons;
|
||||
|
||||
if (currentQuestion != null && buttons != null)
|
||||
{
|
||||
int correctIndex = currentQuestion.correct_index;
|
||||
if (correctIndex >= 0 && correctIndex < buttons.Length)
|
||||
{
|
||||
SetButtonColor(buttons[correctIndex], correctColor);
|
||||
}
|
||||
|
||||
// Falsche Antwort rot markieren
|
||||
if (!correct && selectedAnswer >= 0 && selectedAnswer < buttons.Length)
|
||||
{
|
||||
SetButtonColor(buttons[selectedAnswer], wrongColor);
|
||||
}
|
||||
}
|
||||
|
||||
// Feedback Panel
|
||||
if (feedbackPanel)
|
||||
{
|
||||
feedbackPanel.SetActive(true);
|
||||
|
||||
if (feedbackText)
|
||||
{
|
||||
string pointsStr = points >= 0 ? $"+{points}" : $"{points}";
|
||||
feedbackText.text = correct ?
|
||||
$"Richtig! {pointsStr} Punkte" :
|
||||
$"Leider falsch. {pointsStr} Punkte";
|
||||
feedbackText.color = correct ? correctColor : wrongColor;
|
||||
}
|
||||
|
||||
if (feedbackIcon)
|
||||
{
|
||||
feedbackIcon.sprite = correct ? correctIcon : wrongIcon;
|
||||
}
|
||||
}
|
||||
|
||||
// Sound abspielen
|
||||
if (correct)
|
||||
{
|
||||
AudioManager.Instance?.PlayCorrectSound();
|
||||
}
|
||||
else
|
||||
{
|
||||
AudioManager.Instance?.PlayWrongSound();
|
||||
}
|
||||
|
||||
// Animation
|
||||
if (quizAnimator)
|
||||
{
|
||||
quizAnimator.SetTrigger(correct ? "Correct" : "Wrong");
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Helper
|
||||
// ==============================================
|
||||
private void HideAll()
|
||||
{
|
||||
if (quickQuizPanel) quickQuizPanel.SetActive(false);
|
||||
if (pauseQuizPanel) pauseQuizPanel.SetActive(false);
|
||||
if (feedbackPanel) feedbackPanel.SetActive(false);
|
||||
}
|
||||
|
||||
private IEnumerator HideAfterDelay(float delay)
|
||||
{
|
||||
yield return new WaitForSecondsRealtime(delay);
|
||||
HideAll();
|
||||
}
|
||||
|
||||
private void ResetButtonColor(Button button)
|
||||
{
|
||||
SetButtonColor(button, normalColor);
|
||||
}
|
||||
|
||||
private void HighlightButton(Button button)
|
||||
{
|
||||
SetButtonColor(button, selectedColor);
|
||||
}
|
||||
|
||||
private void SetButtonColor(Button button, Color color)
|
||||
{
|
||||
var colors = button.colors;
|
||||
colors.normalColor = color;
|
||||
colors.highlightedColor = color;
|
||||
button.colors = colors;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>{{{ PRODUCT_NAME }}}</title>
|
||||
|
||||
<!-- PWA Meta Tags -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="shortcut icon" href="TemplateData/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="TemplateData/icon-192.png">
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
#unity-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#unity-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1a1a2e;
|
||||
}
|
||||
|
||||
#unity-loading-bar {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 80%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
#unity-logo {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#unity-logo img {
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
#unity-title {
|
||||
color: #fff;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
font-size: 24px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#unity-progress-bar-empty {
|
||||
width: 100%;
|
||||
height: 18px;
|
||||
background: #333;
|
||||
border-radius: 9px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#unity-progress-bar-full {
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #8BC34A);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
#unity-loading-text {
|
||||
color: #aaa;
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#unity-warning {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#unity-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#unity-fullscreen-button {
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#unity-fullscreen-button img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
#unity-fullscreen-button:hover img {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Mobile Optimierungen */
|
||||
@media (max-width: 768px) {
|
||||
#unity-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
#unity-loading-bar {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="unity-container">
|
||||
<!-- Loading Screen -->
|
||||
<div id="unity-loading-bar">
|
||||
<div id="unity-logo">
|
||||
<img src="TemplateData/logo.png" alt="Breakpilot Drive">
|
||||
</div>
|
||||
<div id="unity-title">{{{ PRODUCT_NAME }}}</div>
|
||||
<div id="unity-progress-bar-empty">
|
||||
<div id="unity-progress-bar-full"></div>
|
||||
</div>
|
||||
<div id="unity-loading-text">Lade...</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Canvas -->
|
||||
<canvas id="unity-canvas" tabindex="-1"></canvas>
|
||||
|
||||
<!-- Warning Message -->
|
||||
<div id="unity-warning"></div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div id="unity-footer">
|
||||
<button id="unity-fullscreen-button" title="Vollbild">
|
||||
<img src="TemplateData/fullscreen-button.png" alt="Vollbild">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var container = document.querySelector("#unity-container");
|
||||
var canvas = document.querySelector("#unity-canvas");
|
||||
var loadingBar = document.querySelector("#unity-loading-bar");
|
||||
var progressBarFull = document.querySelector("#unity-progress-bar-full");
|
||||
var loadingText = document.querySelector("#unity-loading-text");
|
||||
var fullscreenButton = document.querySelector("#unity-fullscreen-button");
|
||||
var warningBanner = document.querySelector("#unity-warning");
|
||||
|
||||
// Zeigt Warnungen/Fehler an
|
||||
function unityShowBanner(msg, type) {
|
||||
function updateBannerVisibility() {
|
||||
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
|
||||
}
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = msg;
|
||||
warningBanner.appendChild(div);
|
||||
if (type == 'error') div.style = 'color: #ff0000;';
|
||||
else if (type == 'warning') div.style = 'color: #ff9900;';
|
||||
setTimeout(function() {
|
||||
warningBanner.removeChild(div);
|
||||
updateBannerVisibility();
|
||||
}, 5000);
|
||||
updateBannerVisibility();
|
||||
}
|
||||
|
||||
// Build-Konfiguration
|
||||
var buildUrl = "Build";
|
||||
var loaderUrl = buildUrl + "/{{{ LOADER_FILENAME }}}";
|
||||
var config = {
|
||||
dataUrl: buildUrl + "/{{{ DATA_FILENAME }}}",
|
||||
frameworkUrl: buildUrl + "/{{{ FRAMEWORK_FILENAME }}}",
|
||||
#if USE_WASM
|
||||
codeUrl: buildUrl + "/{{{ CODE_FILENAME }}}",
|
||||
#endif
|
||||
#if MEMORY_FILENAME
|
||||
memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}",
|
||||
#endif
|
||||
#if SYMBOLS_FILENAME
|
||||
symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}",
|
||||
#endif
|
||||
streamingAssetsUrl: "StreamingAssets",
|
||||
companyName: "{{{ COMPANY_NAME }}}",
|
||||
productName: "{{{ PRODUCT_NAME }}}",
|
||||
productVersion: "{{{ PRODUCT_VERSION }}}",
|
||||
showBanner: unityShowBanner,
|
||||
};
|
||||
|
||||
// Mobile Check
|
||||
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
|
||||
var meta = document.createElement('meta');
|
||||
meta.name = 'viewport';
|
||||
meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
|
||||
document.getElementsByTagName('head')[0].appendChild(meta);
|
||||
container.className = "unity-mobile";
|
||||
}
|
||||
|
||||
// Canvas-Groesse anpassen
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
|
||||
// Loader-Script laden
|
||||
var script = document.createElement("script");
|
||||
script.src = loaderUrl;
|
||||
script.onload = () => {
|
||||
createUnityInstance(canvas, config, (progress) => {
|
||||
// Progress-Bar aktualisieren
|
||||
var percent = Math.round(progress * 100);
|
||||
progressBarFull.style.width = percent + "%";
|
||||
loadingText.textContent = "Lade... " + percent + "%";
|
||||
}).then((unityInstance) => {
|
||||
// Loading fertig
|
||||
loadingBar.classList.add("hidden");
|
||||
|
||||
// Fullscreen-Button
|
||||
fullscreenButton.onclick = () => {
|
||||
unityInstance.SetFullscreen(1);
|
||||
};
|
||||
|
||||
// Unity-Instanz global verfuegbar machen
|
||||
window.unityInstance = unityInstance;
|
||||
|
||||
}).catch((message) => {
|
||||
alert(message);
|
||||
});
|
||||
};
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Text-to-Speech Funktionen (fuer Unity)
|
||||
window.SpeakWebGL = function(text) {
|
||||
if ('speechSynthesis' in window) {
|
||||
window.speechSynthesis.cancel();
|
||||
var utterance = new SpeechSynthesisUtterance(text);
|
||||
utterance.lang = 'de-DE';
|
||||
utterance.rate = 0.9;
|
||||
|
||||
var voices = window.speechSynthesis.getVoices();
|
||||
var germanVoice = voices.find(v => v.lang.startsWith('de'));
|
||||
if (germanVoice) utterance.voice = germanVoice;
|
||||
|
||||
window.speechSynthesis.speak(utterance);
|
||||
}
|
||||
};
|
||||
|
||||
window.SpeakDelayedWebGL = function(text, delay) {
|
||||
setTimeout(function() {
|
||||
window.SpeakWebGL(text);
|
||||
}, delay * 1000);
|
||||
};
|
||||
|
||||
window.StopSpeakingWebGL = function() {
|
||||
if ('speechSynthesis' in window) {
|
||||
window.speechSynthesis.cancel();
|
||||
}
|
||||
};
|
||||
|
||||
// Voice-Liste laden (manche Browser laden sie async)
|
||||
if ('speechSynthesis' in window) {
|
||||
window.speechSynthesis.onvoiceschanged = function() {
|
||||
console.log('Voices geladen:', window.speechSynthesis.getVoices().length);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
239
breakpilot-drive/index.html
Normal file
239
breakpilot-drive/index.html
Normal file
@@ -0,0 +1,239 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title>Breakpilot Drive - Lernspiel</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
background: linear-gradient(45deg, #e94560, #ff6b6b);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 1.5rem;
|
||||
color: #8892b0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.status-box {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
padding: 2rem;
|
||||
margin: 2rem 0;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.2rem;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.mode-btn.video {
|
||||
background: linear-gradient(45deg, #e94560, #ff6b6b);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.mode-btn.audio {
|
||||
background: linear-gradient(45deg, #00d9ff, #00ff88);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
.mode-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.info-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info-card h3 {
|
||||
color: #e94560;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.info-card p {
|
||||
color: #8892b0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.api-status {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: rgba(0, 255, 136, 0.1);
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.api-status.error {
|
||||
background: rgba(233, 69, 96, 0.1);
|
||||
}
|
||||
|
||||
.loading {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3rem;
|
||||
color: #8892b0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Breakpilot Drive</h1>
|
||||
<p class="subtitle">Lernspiel fuer Klasse 2-6</p>
|
||||
|
||||
<div class="status-box">
|
||||
<div class="status-icon">🚧</div>
|
||||
<h2>Unity Build in Entwicklung</h2>
|
||||
<p style="color: #8892b0; margin-top: 1rem;">
|
||||
Diese Seite wird durch den Unity WebGL Build ersetzt,<br>
|
||||
sobald das Spiel fertig ist.
|
||||
</p>
|
||||
|
||||
<div class="mode-buttons">
|
||||
<button class="mode-btn video" disabled>
|
||||
🎮 Video-Version
|
||||
</button>
|
||||
<button class="mode-btn audio" disabled>
|
||||
🔊 Audio-Version
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-cards">
|
||||
<div class="info-card">
|
||||
<h3>🚗 Endless Runner</h3>
|
||||
<p>Steuere dein Auto durch 3 Spuren, weiche Hindernissen aus und sammle Punkte!</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>📚 Quiz-Integration</h3>
|
||||
<p>Beantworte Fragen waehrend der Fahrt oder nimm dir Zeit bei Denkaufgaben.</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>📈 Adaptiv</h3>
|
||||
<p>Die Schwierigkeit passt sich automatisch an dein Breakpilot-Lernniveau an.</p>
|
||||
</div>
|
||||
<div class="info-card">
|
||||
<h3>🔊 Barrierefrei</h3>
|
||||
<p>Audio-Version fuer Hoerspiel-Modus mit raeumlichen Sound-Cues.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="api-status" class="api-status loading">
|
||||
Pruefe Backend-Verbindung...
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>Breakpilot Drive © 2026 - Lernen mit Spass</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// API Health-Check
|
||||
async function checkApi() {
|
||||
const statusEl = document.getElementById('api-status');
|
||||
try {
|
||||
// Versuche Backend zu erreichen
|
||||
const response = await fetch('/api/game/health');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'healthy') {
|
||||
statusEl.innerHTML = `✅ Backend verbunden - ${data.questions_available} Quiz-Fragen verfuegbar`;
|
||||
statusEl.classList.remove('loading', 'error');
|
||||
} else {
|
||||
throw new Error('Unhealthy');
|
||||
}
|
||||
} catch (e) {
|
||||
// Fallback: Versuche direkten Backend-Aufruf
|
||||
try {
|
||||
const response = await fetch('http://localhost:8000/api/game/health');
|
||||
const data = await response.json();
|
||||
statusEl.innerHTML = `✅ Backend (localhost:8000) - ${data.questions_available} Quiz-Fragen`;
|
||||
statusEl.classList.remove('loading', 'error');
|
||||
} catch (e2) {
|
||||
statusEl.innerHTML = '⚠️ Backend nicht erreichbar - Starte docker-compose up';
|
||||
statusEl.classList.remove('loading');
|
||||
statusEl.classList.add('error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Beim Laden pruefen
|
||||
checkApi();
|
||||
|
||||
// Alle 30 Sekunden erneut pruefen
|
||||
setInterval(checkApi, 30000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
76
breakpilot-drive/nginx.conf
Normal file
76
breakpilot-drive/nginx.conf
Normal file
@@ -0,0 +1,76 @@
|
||||
# ==============================================
|
||||
# Nginx Konfiguration fuer Unity WebGL
|
||||
# ==============================================
|
||||
# Optimiert fuer grosse WASM-Dateien und
|
||||
# korrektes CORS fuer API-Aufrufe
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Standard MIME-Types einbinden + WebGL-spezifische hinzufuegen
|
||||
include /etc/nginx/mime.types;
|
||||
|
||||
# WebGL-spezifische MIME-Types (zusaetzlich)
|
||||
types {
|
||||
application/wasm wasm;
|
||||
}
|
||||
|
||||
# Gzip Kompression fuer grosse Dateien
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types
|
||||
application/javascript
|
||||
application/wasm
|
||||
application/json
|
||||
text/plain
|
||||
text/css
|
||||
text/html;
|
||||
gzip_min_length 1000;
|
||||
|
||||
# Brotli-komprimierte Unity-Dateien (falls vorhanden)
|
||||
location ~ \.br$ {
|
||||
gzip off;
|
||||
add_header Content-Encoding br;
|
||||
default_type application/octet-stream;
|
||||
}
|
||||
|
||||
# Gzip-komprimierte Unity-Dateien
|
||||
location ~ \.gz$ {
|
||||
gzip off;
|
||||
add_header Content-Encoding gzip;
|
||||
default_type application/octet-stream;
|
||||
}
|
||||
|
||||
# Caching fuer statische Assets (1 Jahr)
|
||||
location ~* \.(data|wasm|js|css|png|jpg|gif|ico|svg|woff|woff2)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
}
|
||||
|
||||
# Health-Check Endpunkt
|
||||
location = /health {
|
||||
default_type application/json;
|
||||
alias /usr/share/nginx/html/health.json;
|
||||
}
|
||||
|
||||
location = /health.json {
|
||||
access_log off;
|
||||
default_type application/json;
|
||||
}
|
||||
|
||||
# SPA Routing - alle Requests auf index.html
|
||||
location / {
|
||||
# CORS Headers
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user