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>
349 lines
10 KiB
C#
349 lines
10 KiB
C#
// ==============================================
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
}
|