Files
bamort/backend/bmrt/character/lerncost_handler.go
T
Bardioc26 042a1d4773 Learncost frontend (#42)
* introduced central package  registry by package init function
* dynamic registration of routes, model, migrations and initializers.
* setting a docker compose project name to prevent shutdown of other containers with the same (composer)name
* ai documentation
* app template
* Create tests for ALL API entpoints in ALL packages Based on current data. Ensure that all API endpoints used in frontend are tested. These tests are crucial for the next refactoring tasks.
* adopting agent instructions for a more consistent coding style
* added desired module layout and debugging information
* Fix All Failing tests All failing tests are fixed now that makes the refactoring more easy since all tests must pass
* restored routes for maintenance
* added common translations
* added new tests for API Endpoint
* Merge branch 'separate_business_logic'
* added lern and skill improvement cost editing
* Set Docker image tag when building to prevent rebuild when nothing has changed
* add and remove PP for Weaponskill fixed
* add and remove PP for same named skills fixed
* add new task
2026-05-01 18:15:31 +02:00

645 lines
22 KiB
Go

package character
import (
"bamort/bmrt/gsmaster"
"bamort/bmrt/models"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type SkillCostRequest struct {
Name string `json:"name" binding:"required"` // Name der Fertigkeit
CurrentLevel int `json:"current_level,omitempty"` // Aktueller Wert (nur für Verbesserung)
Type string `json:"type" binding:"required,oneof=skill spell weapon"` // 'skill', 'spell' oder 'weapon'
Action string `json:"action" binding:"required,oneof=learn improve"` // 'learn' oder 'improve'
TargetLevel int `json:"target_level,omitempty"` // Zielwert (optional, für Kostenberechnung bis zu einem bestimmten Level)
UsePP int `json:"use_pp,omitempty"` // Anzahl der zu verwendenden Praxispunkte
// Belohnungsoptionen
Reward *RewardOptions `json:"reward,omitempty"` // Belohnungsoptionen
}
// RewardOptions definiert die verschiedenen Belohnungsmöglichkeiten
type RewardOptions struct {
Type string `json:"type,omitempty" binding:"omitempty,oneof=free_learning free_spell_learning half_ep_improvement gold_for_ep"` // Art der Belohnung
UseGoldForEP bool `json:"use_gold_for_ep,omitempty"` // 10 GS statt 1 EP verwenden
MaxGoldEP int `json:"max_gold_ep,omitempty"` // Maximale EP die durch Gold ersetzt werden (automatisch: Hälfte der Kosten)
}
type SkillCostResponse struct {
*models.LearnCost
SkillName string `json:"skill_name"`
SkillType string `json:"skill_type"`
Action string `json:"action"`
CharacterID uint `json:"character_id"`
CurrentLevel int `json:"current_level,omitempty"`
TargetLevel int `json:"target_level,omitempty"`
Category string `json:"category,omitempty"`
Difficulty string `json:"difficulty,omitempty"`
CanAfford bool `json:"can_afford"`
Notes string `json:"notes,omitempty"`
PPUsed int `json:"pp_used,omitempty"` // Anzahl der verwendeten Praxispunkte
PPAvailable int `json:"pp_available,omitempty"` // Verfügbare Praxispunkte für diese Kategorie
// Belohnungsdetails
RewardApplied string `json:"reward_applied,omitempty"` // Art der angewendeten Belohnung
OriginalCostStruct *models.LearnCost `json:"original_cost_struct,omitempty"` // Ursprüngliche Kosten ohne Belohnung
Savings *models.LearnCost `json:"savings,omitempty"` // Ersparnisse durch Belohnung
GoldUsedForEP int `json:"gold_used_for_ep,omitempty"` // Gold das für EP verwendet wurde
PPReduction int `json:"pp_reduction,omitempty"` // Reduktion der Kosten durch PP
OriginalCost int `json:"original_cost,omitempty"` // Ursprüngliche Kosten (vor PP-Reduktion)
FinalCost int `json:"final_cost,omitempty"` // Endgültige Kosten (nach PP-Reduktion)
}
type MultiLevelCostResponse struct {
SkillName string `json:"skill_name"`
SkillType string `json:"skill_type"`
CharacterID uint `json:"character_id"`
CurrentLevel int `json:"current_level"`
TargetLevel int `json:"target_level"`
LevelCosts []SkillCostResponse `json:"level_costs"`
TotalCost *models.LearnCost `json:"total_cost"`
CanAffordTotal bool `json:"can_afford_total"`
}
// GetLernCostNewSystem verwendet das neue Datenbank-Lernkosten-System
// und produziert die gleichen Ergebnisse wie GetLernCost.
//
// Wie es funktionert:
// - Für "learn" Aktion: Nur eine Berechnung, da Lernkosten einmalig sind
// - Für "improve" Aktion: Berechne für jedes Level von current+1 bis 18
// - Berücksichtigt Praxispunkte (PP) und Gold-für-EP Konvertierung
// - Wendet Belohnungen an (kostenloses Lernen, halbe EP, etc.)
// - Gibt eine Liste von Kosten pro Level zurück
// Schritt für Schritt:
// 1. Hole Charakter und Klassenabkürzung
// 2. Normalisiere Fertigkeits-/Zaubername
// 3. Initialisiere einzusetzende/verbleibende PP und Gold
// 4. Je nach Aktion:
// 4.1 "learn": Hole Lerninformationen und berechne Kosten
// 4.1.1 "spell": Hole Zauber-Lerninformationen und berechne Kosten
// 4.1.2 "skill": Hole Fertigkeits-Lerninformationen und berechne Kosten
// 4.2 "improve": Für jedes Level, hole Verbesserungsinformationen und berechne Kosten
// (nur Fertigkeiten, keine Zauber)
//
// 5. Wende Belohnungen an
// 6. Wende PP und Gold-für-EP an
// 7. Sammle Ergebnisse und sende als JSON-Antwort
func GetLernCostNewSystem(c *gin.Context) {
// Request-Parameter abrufen
var request gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&request); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
// 1. Hole Charakter
charID := fmt.Sprintf("%d", request.CharId)
var character models.Char
if err := character.FirstID(charID); err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist
var characterClass string
if len(character.Typ) > 3 {
characterClass = gsmaster.GetClassAbbreviationNewSystem(character.Typ)
} else {
characterClass = character.Typ
}
//2. Normalize skill/spell name (trim whitespace, proper case)
skillName := strings.TrimSpace(request.Name)
var response []gsmaster.SkillCostResultNew
remainingPP := request.UsePP
remainingGold := request.UseGold
// Für "learn" Aktion: nur eine Berechnung, da Lernkosten einmalig sind
if request.Action == "learn" {
// 4.1 "learn": Hole Lerninformationen und berechne Kosten
if request.Type == "spell" {
// 4.1.1 "spell": Hole Zauber-Lerninformationen und berechne Kosten
// Spell learning logic
spellInfo, err := models.GetSpellLearningInfoNewSystem(skillName, characterClass)
if err != nil {
respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Zauber '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", skillName, characterClass, err))
return
}
levelResult := gsmaster.SkillCostResultNew{
CharacterID: charID,
CharacterClass: characterClass,
SkillName: skillName,
Category: spellInfo.SchoolName,
Difficulty: fmt.Sprintf("Stufe %d", spellInfo.SpellLevel),
TargetLevel: 1, // Lernkosten sind für das Erlernen des Zaubers (Level 1)
}
err = calculateSpellLearnCostNewSystem(&request, &levelResult, &remainingPP, &remainingGold, spellInfo)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
response = append(response, levelResult)
} else {
// 4.1.2 "skill": Hole Fertigkeits-Lerninformationen und berechne Kosten
skillInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, characterClass)
if err != nil {
respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Fertigkeit '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", skillName, characterClass, err))
return
}
levelResult := gsmaster.SkillCostResultNew{
CharacterID: charID,
CharacterClass: characterClass,
SkillName: skillName,
Category: skillInfo.CategoryName,
Difficulty: skillInfo.DifficultyName,
TargetLevel: 1, // Lernkosten sind für das Erlernen der Fertigkeit (Level 1)
}
err = calculateSkillLearnCostNewSystem(&request, &levelResult, &remainingPP, &remainingGold, skillInfo)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
response = append(response, levelResult)
}
} else {
// Für "improve" Aktion: berechne für jedes Level von current+1 bis 18
// Improvement only works on skills, not spells
skillInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, characterClass)
if err != nil {
respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Fertigkeit '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", skillName, characterClass, err))
return
}
for i := request.CurrentLevel + 1; i <= 18; i++ {
levelResult := gsmaster.SkillCostResultNew{
CharacterID: charID,
CharacterClass: characterClass,
SkillName: skillName,
Category: skillInfo.CategoryName,
Difficulty: skillInfo.DifficultyName,
TargetLevel: i,
}
err := CalculateSkillImproveCostNewSystem(&request, &levelResult, i, &remainingPP, &remainingGold, skillInfo)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
// für die nächste Runde die PP und Gold reduzieren die zum Lernen genutzt werden sollen
if levelResult.PPUsed > 0 {
request.UsePP -= levelResult.PPUsed
// Sicherstellen, dass PP nicht unter 0 fallen
if request.UsePP < 0 {
request.UsePP = 0
}
}
if levelResult.GoldUsed > 0 {
request.UseGold -= levelResult.GoldUsed
// Sicherstellen, dass Gold nicht unter 0 fällt
if request.UseGold < 0 {
request.UseGold = 0
}
}
response = append(response, levelResult)
}
}
c.JSON(http.StatusOK, response)
}
// calculateCostNewSystem berechnet die Kosten für ein Level mit dem neuen Datenbank-System
func CalculateSkillImproveCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.SkillCostResultNew, targetLevel int, remainingPP *int, remainingGold *int, skillInfo *models.SkillLearningInfo) error {
// 1. Hole die TE-Kosten für die Verbesserung vom aktuellen Level
teRequired, err := models.GetImprovementCost(skillInfo.SkillName, skillInfo.CategoryName, skillInfo.DifficultyName, targetLevel)
if err != nil {
return fmt.Errorf("verbesserungskosten nicht gefunden für %s (Level %d): %v", skillInfo.SkillName, targetLevel, err)
}
// 2. Hole die EP-Kosten pro TE für diese Klasse und Kategorie
if skillInfo.EPPerTE == 0 {
epPerTE, err := models.GetEPPerTEForClassAndCategory(result.CharacterClass, skillInfo.CategoryName)
if err != nil {
return fmt.Errorf("EP-Kosten pro TE nicht gefunden für Klasse %s, Kategorie %s: %v", result.CharacterClass, skillInfo.CategoryName, err)
}
skillInfo.EPPerTE = epPerTE
}
// 3. Setze die ursprünglichen TE-Kosten
trainCost := teRequired
// 4. Anwenden von Praxispunkten (PP) - Exakt wie im alten System
ppUsed := 0
if *remainingPP > 0 {
if trainCost < *remainingPP {
ppUsed = trainCost // Maximal so viele PP verwenden wie TE benötigt werden
trainCost = 0 // Wenn PP alle TE abdecken, setze trainCost auf 0
} else if *remainingPP > 0 {
ppUsed = *remainingPP // Verwende alle verfügbaren PP
trainCost -= ppUsed // Reduziere TE um verwendete PP
}
result.PPUsed = ppUsed
*remainingPP -= ppUsed
if *remainingPP < 0 {
*remainingPP = 0
}
}
// 5. Berechne Kosten nach PP-Anwendung (wie im alten System)
result.LE = trainCost
result.EP = skillInfo.EPPerTE * trainCost
result.GoldCost = trainCost * 20 // Wie im alten System: 20 Gold pro TE
// 6. Anwenden von Belohnungen
if request.Reward != nil {
applyRewardNewSystem(result, request.Reward, result.EP)
}
// 7. Anwenden von Gold für EP (falls verfügbar) - Beschränkt auf EP/2
goldUsed := 0
if *remainingGold > 0 {
// 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden
maxEPFromGold := result.EP / 2
epFromGold := *remainingGold / 10
if epFromGold > maxEPFromGold {
// Beschränke auf maximal EP/2
epFromGold = maxEPFromGold
goldUsed = epFromGold * 10
} else {
// Verwende das verfügbare Gold
goldUsed = *remainingGold
}
// Reduziere EP um die durch Gold ersetzte Menge
result.EP -= epFromGold
result.GoldUsed = goldUsed
*remainingGold -= goldUsed
if *remainingGold < 0 {
*remainingGold = 0
}
}
return nil
}
// calculateSkillLearnCostNewSystem berechnet die Kosten für das Erlernen einer Fertigkeit (Action: "learn", Type: "skill")
func calculateSkillLearnCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.SkillCostResultNew, remainingPP *int, remainingGold *int, skillInfo *models.SkillLearningInfo) error {
// 1. Hole die EP-Kosten pro TE für diese Klasse und Kategorie
epPerTE, err := models.GetEPPerTEForClassAndCategory(result.CharacterClass, skillInfo.CategoryName)
if err != nil {
return fmt.Errorf("EP-Kosten pro TE nicht gefunden für Klasse %s, Kategorie %s: %v", result.CharacterClass, skillInfo.CategoryName, err)
}
// 2. Verwende die Lernkosten (LE) direkt aus der skillInfo - diese enthält bereits alle benötigten Informationen
learnCost := skillInfo.LearnCost
// 3. Berechne Kosten nach Lernregeln (wie im alten System)
result.LE = learnCost
result.EP = epPerTE * result.LE * 3 // Faktor 3 beim Lernen!
result.GoldCost = result.LE * 200 // 200 Gold pro LE (nicht 20 Gold pro TE)
// 4. Anwenden von Belohnungen
if request.Reward != nil {
applyRewardNewSystem(result, request.Reward, result.EP)
}
// 5. Für Skill-Lernen: Keine PP oder Gold-für-EP Anwendung erlaubt (im Gegensatz zu Spell-Lernen)
// PP und Gold bleiben unverändert, da sie bei Skill-Lernen nicht verwendet werden
return nil
}
// applyRewardNewSystem wendet Belohnungen auf die Kosten an (neues System)
func applyRewardNewSystem(result *gsmaster.SkillCostResultNew, reward *string, originalEP int) {
if reward == nil || *reward == "" {
return
}
switch *reward {
case "noGold":
// Kostenlose Fertigkeiten: Nur Geld ist 0, EP bleiben
result.GoldCost = 0
case "halveep":
// Halbe EP für Verbesserungen
result.EP = result.EP / 2
case "halveepnoGold":
// Halbe EP und kein Gold
result.EP = result.EP / 2
result.GoldCost = 0
case "default":
// Keine Änderungen
break
default:
// Unbekannte Belohnung - ignorieren
break
}
}
// calculateSpellLearnCostNewSystem berechnet die Kosten für das Erlernen eines Zaubers (Action: "learn", Type: "spell")
func calculateSpellLearnCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.SkillCostResultNew, remainingPP *int, remainingGold *int, spellInfo *models.SpellLearningInfo) error {
// 1. Setze die grundlegenden Zauber-Informationen
result.Category = spellInfo.SchoolName
result.Difficulty = fmt.Sprintf("Stufe %d", spellInfo.SpellLevel)
// 2. Berechne die LE-Kosten basierend auf der Zaubergrad
leRequired := spellInfo.LERequired
// 3. Anwenden von PP (Practice Points): 1 PP = 1 LE Reduktion (bei Zauber-Lernen erlaubt)
ppUsed := 0
if *remainingPP > 0 {
if leRequired <= *remainingPP {
ppUsed = leRequired // Maximal so viele PP verwenden wie LE benötigt werden
leRequired = 0 // Wenn PP alle LE abdecken
} else {
ppUsed = *remainingPP // Verwende alle verfügbaren PP
leRequired -= ppUsed // Reduziere LE um verwendete PP
}
result.PPUsed = ppUsed
*remainingPP -= ppUsed
if *remainingPP < 0 {
*remainingPP = 0
}
}
// 4. Setze die finalen LE-Kosten
result.LE = leRequired
// 5. Berechne EP-Kosten basierend auf LE und EP-pro-LE für diese Klasse/Schule
result.EP = result.LE * spellInfo.EPPerLE
// 6. Berechne Gold-Kosten (Beispiel: 100 Gold pro LE wie im alten System)
result.GoldCost = result.LE * 100
// 7. Anwenden von Belohnungen (spruchrolle, halveep, etc.)
if request.Reward != nil {
applySpellRewardNewSystem(result, request.Reward)
}
// 8. Gold-für-EP Konvertierung für Zauber-Lernen (erlaubt)
goldUsed := 0
if *remainingGold > 0 {
// 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden
maxEPFromGold := result.EP / 2
epFromGold := *remainingGold / 10
if epFromGold > maxEPFromGold {
// Beschränke auf maximal EP/2
epFromGold = maxEPFromGold
goldUsed = epFromGold * 10
} else {
// Verwende das verfügbare Gold
goldUsed = *remainingGold
}
// Reduziere EP um die durch Gold ersetzte Menge
result.EP -= epFromGold
result.GoldUsed = goldUsed
*remainingGold -= goldUsed
if *remainingGold < 0 {
*remainingGold = 0
}
}
return nil
}
// applySpellRewardNewSystem wendet zauber-spezifische Belohnungen an
func applySpellRewardNewSystem(result *gsmaster.SkillCostResultNew, reward *string) {
if reward == nil || *reward == "" {
return
}
switch *reward {
case "spruchrolle":
// Spruchrolle: 20 Gold für jeden Versuch und 1/3 EP-Kosten bei Erfolg
result.GoldCost = 20
result.EP = result.EP / 3
case "halveep":
// Halbe EP für Zauber-Lernen
result.EP = result.EP / 2
case "halveepnoGold":
// Halbe EP und kein Gold
result.EP = result.EP / 2
result.GoldCost = 0
case "noGold":
// Nur Geld ist 0, EP bleiben
result.GoldCost = 0
case "default":
// Keine Änderungen
break
default:
// Unbekannte Belohnung - ignorieren
break
}
}
// Helper function to get current skill level from character
func getCurrentSkillLevel(character *models.Char, skillName, skillType string) int {
switch skillType {
case "skill":
for _, skill := range character.Fertigkeiten {
if skill.Name == skillName {
return skill.Fertigkeitswert
}
}
case "weapon":
for _, skill := range character.Waffenfertigkeiten {
if skill.Name == skillName {
return skill.Fertigkeitswert
}
}
case "spell":
// Spells don't have levels in the same way
return 0
}
return -1
}
// applyReward wendet Belohnungen auf die Kosten an
func applyReward(cost *models.LearnCost, request *SkillCostRequest) *models.LearnCost {
if request.Reward == nil || request.Reward.Type == "" {
return cost
}
newCost := *cost // Kopie der ursprünglichen Kosten
switch request.Reward.Type {
case "free_learning":
// Kostenlose Fertigkeiten: Nur Geld ist 0, EP/LE bleiben
if request.Type == "skill" && request.Action == "learn" {
newCost.Money = 0
}
case "free_spell_learning":
// Kostenlose Zauber: Nur LE ist 0, EP/Geld bleiben
if request.Type == "spell" && request.Action == "learn" {
newCost.LE = 0
}
case "half_ep_improvement":
// Halbe EP für Verbesserungen
if request.Action == "improve" {
newCost.Ep = newCost.Ep / 2
}
case "gold_for_ep":
// Gold statt EP verwenden (10 GS = 1 EP)
if request.Reward.UseGoldForEP && newCost.Ep > 0 {
maxGoldEP := request.Reward.MaxGoldEP
if maxGoldEP == 0 {
// Standard: Maximal die Hälfte der EP durch Gold ersetzen
maxGoldEP = newCost.Ep / 2
}
// Beschränke auf verfügbare EP
if maxGoldEP > newCost.Ep {
maxGoldEP = newCost.Ep
}
// Ersetze EP durch Gold (10 GS pro EP)
newCost.Ep -= maxGoldEP
newCost.Money += maxGoldEP * 10
}
}
return &newCost
}
// Helper structures and functions
type skillInfo struct {
Category string
Difficulty string
}
func getSpellInfo(spellName string) skillInfo {
var spell models.Spell
if err := spell.First(spellName); err != nil {
return skillInfo{Category: "unknown", Difficulty: "unknown"}
}
return skillInfo{Category: spell.Category, Difficulty: strconv.Itoa(spell.Stufe)}
}
func canCharacterAfford(character *models.Char, cost *models.LearnCost) bool {
// Check if character has enough EP
if character.Erfahrungsschatz.EP < cost.Ep {
return false
}
// Check if character has enough money
// Assuming money is stored in Bennies (Gold pieces)
totalMoney := character.Bennies.Gg + character.Bennies.Gp + character.Bennies.Sg
return totalMoney >= cost.Money
}
func generateNotes(character *models.Char, request *SkillCostRequest, cost *models.LearnCost) string {
var notes []string
if request.Action == "learn" {
notes = append(notes, "Neue Fertigkeit erlernen")
} else {
notes = append(notes, fmt.Sprintf("Verbesserung von %d auf %d", request.CurrentLevel, request.CurrentLevel+1))
}
// Add character class specific notes
if character.Typ != "" {
notes = append(notes, fmt.Sprintf("Kosten für %s", character.Typ))
}
// Add PP usage notes
if request.UsePP > 0 {
notes = append(notes, fmt.Sprintf("Verwendung von %d Praxispunkten", request.UsePP))
}
// Add affordability note
if !canCharacterAfford(character, cost) {
notes = append(notes, "Nicht genügend EP oder Gold vorhanden")
}
return strings.Join(notes, ". ")
}
// getPPForSkill ermittelt die verfügbaren Praxispunkte für eine spezifische Fertigkeit
func getPPForSkill(character *models.Char, skillName string) int {
// Ermittle die tatsächliche Fertigkeit (bei Zaubern die Zaubergruppe)
targetSkillName := getSpellCategoryNewSystem(skillName)
for _, fertigkeit := range character.Fertigkeiten {
if fertigkeit.Name == targetSkillName {
return fertigkeit.Pp
}
}
return 0
}
// applyPPReduction reduziert die Kosten entsprechend der verwendeten Praxispunkte
func applyPPReduction(request *SkillCostRequest, cost *models.LearnCost, availablePP int) (int, int, int) {
if request.UsePP <= 0 {
return cost.Ep, cost.LE, 0
}
// Maximal so viele PP verwenden, wie verfügbar sind
ppToUse := request.UsePP
if ppToUse > availablePP {
ppToUse = availablePP
}
originalEP := cost.Ep
originalLE := cost.LE
var finalEP, finalLE int
var reduction int
if request.Action == "improve" {
// Für Verbesserungen: 1 TE für 1 PP
// Jeder PP ersetzt 1 TE, daher wird die entsprechende EP-Menge reduziert
reduction = ppToUse // PP-Punkte direkt als Reduktion verwenden
finalEP = originalEP - reduction
finalLE = originalLE
if finalEP < 0 {
finalEP = 0
}
} else if request.Action == "learn" && request.Type == "spell" {
// Für Zauber lernen: 1 LE für 1 PP
reduction = ppToUse // PP-Punkte direkt als Reduktion verwenden
finalLE = originalLE - reduction
finalEP = originalEP
if finalLE < 0 {
finalLE = 0
}
} else {
// Für andere Lernfälle: keine PP-Reduktion
finalEP = originalEP
finalLE = originalLE
reduction = 0
}
return finalEP, finalLE, reduction
}