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>
374 lines
11 KiB
C#
374 lines
11 KiB
C#
// ==============================================
|
|
// 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();
|
|
}
|
|
}
|
|
}
|