From 149f80f0057d173e325836aba4181f64f1475436 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 30 Aug 2025 17:55:20 +0200 Subject: [PATCH] removed almost all function from the Old static SYstem --- backend/character/handlers.go | 817 +---------- backend/character/handlers_test.go | 97 -- backend/character/lerncost_comparison_test.go | 551 -------- backend/character/lerncost_handler.go | 430 ------ backend/character/lerncost_handler_test.go | 1225 ----------------- backend/character/routes.go | 12 +- backend/character/skill_api_test.go | 259 ---- .../character/skill_learning_dialog_test.go | 296 ---- backend/character/skill_update_test.go | 16 +- .../character/system_information_handlers.go | 7 +- backend/cmd/test_learning_costs/main.go | 59 - backend/gsmaster/beidhändiger_kampf_test.go | 89 -- backend/gsmaster/learning_costs.go | 514 ------- backend/gsmaster/lernkosten_maps.go | 390 ------ backend/gsmaster/lernkosten_maps_test.go | 949 ------------- backend/gsmaster/lerntabellen_test.go | 53 - backend/gsmaster/levelup.go | 111 -- backend/gsmaster/levelup_test.go | 263 +--- backend/testdata/prepared_test_data.db.backup | Bin 1540096 -> 1708032 bytes 19 files changed, 29 insertions(+), 6109 deletions(-) delete mode 100644 backend/character/lerncost_comparison_test.go delete mode 100644 backend/character/skill_api_test.go delete mode 100644 backend/character/skill_learning_dialog_test.go delete mode 100644 backend/cmd/test_learning_costs/main.go delete mode 100644 backend/gsmaster/beidhändiger_kampf_test.go delete mode 100644 backend/gsmaster/lerntabellen_test.go diff --git a/backend/character/handlers.go b/backend/character/handlers.go index d1bf102..7fb4361 100644 --- a/backend/character/handlers.go +++ b/backend/character/handlers.go @@ -182,83 +182,6 @@ func splitSkills(object []models.SkFertigkeit) ([]models.SkFertigkeit, []models. return normSkills, innateSkills, categories } -// GetLearnSkillCostOld is deprecated. Use GetLearnSkillCost instead. -// This function uses the old hardcoded learning cost system. -func GetLearnSkillCostOld(c *gin.Context) { - // Get the character ID from the request - charID := c.Param("id") - - // Load the character from the database - var character models.Char - if err := character.FirstID(charID); err != nil { - respondWithError(c, http.StatusInternalServerError, "Failed to retrieve character") - return - } - - // Load the skill from the request - var s models.SkFertigkeit - if err := c.ShouldBindJSON(&s); err != nil { - respondWithError(c, http.StatusBadRequest, err.Error()) - return - } - - var skill models.Skill - if err := skill.First(s.Name); err != nil { - respondWithError(c, http.StatusBadRequest, "can not find speel in gsmaster: "+err.Error()) - return - } - - cost, err := gsmaster.CalculateSkillLearnCostOld(skill.Name, character.Typ) - if err != nil { - respondWithError(c, http.StatusBadRequest, "error getting costs to learn skill: "+err.Error()) - return - } - - // Return the updated character - c.JSON(http.StatusOK, cost) -} - -// GetLearnSpellCostOld is deprecated. Use GetLearnSpellCost instead. -// This function uses the old hardcoded learning cost system. -func GetLearnSpellCostOld(c *gin.Context) { - // Get the character ID from the request - charID := c.Param("id") - - // Load the character from the database - var character models.Char - if err := character.FirstID(charID); err != nil { - respondWithError(c, http.StatusInternalServerError, "Failed to retrieve character") - return - } - - // Load the spell from the request - var s models.SkZauber - if err := c.ShouldBindJSON(&s); err != nil { - respondWithError(c, http.StatusBadRequest, err.Error()) - return - } - var spell models.Spell - if err := spell.First(s.Name); err != nil { - respondWithError(c, http.StatusBadRequest, "can not find speel in gsmaster: "+err.Error()) - return - } - sd := gsmaster.SpellDefinition{ - Name: spell.Name, - Stufe: spell.Stufe, - School: spell.Category, - } - - cost, err := gsmaster.CalculateSpellLearnCostOld(spell.Name, character.Typ) - if err != nil { - respondWithError(c, http.StatusBadRequest, "error getting costs to learn spell: "+err.Error()) - return - } - - sd.CostEP = cost - // Return the updated character - c.JSON(http.StatusOK, sd) -} - // ExperienceAndWealthResponse repräsentiert die Antwort für EP und Vermögen type ExperienceAndWealthResponse struct { ExperiencePoints int `json:"experience_points"` @@ -605,317 +528,16 @@ type LearnSpellRequest struct { Notes string `json:"notes,omitempty"` } -// calculateMultiLevelCostsOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost system. -// calculateMultiLevelCostsOld berechnet die Kosten für mehrere Level-Verbesserungen mit gsmaster.GetLernCostNextLevel -func calculateMultiLevelCostsOld(character *models.Char, skillName string, currentLevel int, levelsToLearn []int, rewardType string, usePP, useGold int) (*models.LearnCost, error) { - if len(levelsToLearn) == 0 { - return nil, fmt.Errorf("keine Level zum Lernen angegeben") - } - - // Sortiere die Level aufsteigend - sortedLevels := make([]int, len(levelsToLearn)) - copy(sortedLevels, levelsToLearn) - for i := 0; i < len(sortedLevels)-1; i++ { - for j := i + 1; j < len(sortedLevels); j++ { - if sortedLevels[i] > sortedLevels[j] { - sortedLevels[i], sortedLevels[j] = sortedLevels[j], sortedLevels[i] - } - } - } - - // Erstelle LernCostRequest - var rewardTypePtr *string - if rewardType != "" { - rewardTypePtr = &rewardType - } - - request := gsmaster.LernCostRequest{ - CharId: uint(character.ID), - Name: skillName, - CurrentLevel: currentLevel, - Type: "skill", - Action: "improve", - TargetLevel: sortedLevels[len(sortedLevels)-1], // Höchstes Level als Ziel - UsePP: usePP, - UseGold: useGold, - Reward: rewardTypePtr, - } - - totalCost := &models.LearnCost{ - Stufe: sortedLevels[len(sortedLevels)-1], - LE: 0, - Ep: 0, - Money: 0, - } - - remainingPP := usePP - remainingGold := useGold - - // Berechne Kosten für jedes Level - for _, targetLevel := range sortedLevels { - classAbr := getCharacterClassOld(character) - cat, difficulty, _ := gsmaster.FindBestCategoryForSkillLearningOld(skillName, classAbr) - levelResult := gsmaster.SkillCostResultNew{ - CharacterID: fmt.Sprintf("%d", character.ID), - CharacterClass: classAbr, - SkillName: skillName, - Category: cat, - Difficulty: gsmaster.GetSkillDifficultyOld(difficulty, skillName), - TargetLevel: targetLevel, - } - - // Temporäre Request für dieses Level - tempRequest := request - tempRequest.CurrentLevel = targetLevel - 1 - tempRequest.UsePP = remainingPP - tempRequest.UseGold = remainingGold - - err := gsmaster.GetLernCostNextLevelOld(&tempRequest, &levelResult, rewardTypePtr, targetLevel, character.Typ) - if err != nil { - return nil, fmt.Errorf("fehler bei Level %d: %v", targetLevel, err) - } - - // Aktualisiere verbleibende Ressourcen - if levelResult.PPUsed > 0 { - remainingPP -= levelResult.PPUsed - if remainingPP < 0 { - remainingPP = 0 - } - } - if levelResult.GoldUsed > 0 { - remainingGold -= levelResult.GoldUsed - if remainingGold < 0 { - remainingGold = 0 - } - } - - totalCost.Ep += levelResult.EP - totalCost.Money += levelResult.GoldCost - totalCost.LE += levelResult.LE - } - - return totalCost, nil -} - -// getCharacterClassOld is deprecated. Use character.Klasse directly or appropriate database lookups. +// getCharacterClass is deprecated. Use character.Klasse directly or appropriate database lookups. // This function provides backwards compatibility for character class access. -// getCharacterClassOld gibt die Charakterklassen-Abkürzung zurück -func getCharacterClassOld(character *models.Char) string { +// getCharacterClass gibt die Charakterklassen-Abkürzung zurück +func getCharacterClass(character *models.Char) string { if len(character.Typ) > 3 { - return gsmaster.GetClassAbbreviationOld(character.Typ) + return gsmaster.GetClassAbbreviationNewSystem(character.Typ) } return character.Typ } -// LearnSkillOld is deprecated. Use LearnSkill instead. -// This function uses the old hardcoded learning cost system. -// LearnSkillOld lernt eine neue Fertigkeit und erstellt Audit-Log-Einträge -func LearnSkillOld(c *gin.Context) { - charID := c.Param("id") - var character models.Char - - if err := character.FirstID(charID); err != nil { - respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden") - return - } - - // Verwende gsmaster.LernCostRequest direkt - var request gsmaster.LernCostRequest - if err := c.ShouldBindJSON(&request); err != nil { - respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error()) - return - } - - // Setze Charakter-ID und Action für learning - request.CharId = character.ID - request.Action = "learn" - if request.Type == "" { - request.Type = "skill" // Default zu skill für Learning - } - // Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist - var characterClass string - if len(character.Typ) > 3 { - characterClass = gsmaster.GetClassAbbreviationOld(character.Typ) - } else { - characterClass = character.Typ - } - - // Bestimme das finale Level - finalLevel := request.TargetLevel - if finalLevel <= 0 { - finalLevel = 1 // Standard für neue Fertigkeit - } - - // Für Learning müssen wir von Level 0 (nicht gelernt) auf finalLevel lernen - var totalEP, totalGold, totalPP int - var err error - - // Loop für jeden Level von 0 bis finalLevel (für neue Fertigkeiten) - for tempLevel := 0; tempLevel < finalLevel; tempLevel++ { - nextLevel := tempLevel + 1 - - // Erstelle temporären Request für diesen Level - tempRequest := request - tempRequest.CurrentLevel = tempLevel - tempRequest.TargetLevel = nextLevel - - // Für das erste Level (0->1) ist es ein "learn", für weitere Level "improve" - if tempLevel == 0 { - tempRequest.Action = "learn" - } else { - tempRequest.Action = "improve" - } - - // Berechne Kosten für diesen einen Level - var costResult gsmaster.SkillCostResultNew - costResult.CharacterID = fmt.Sprintf("%d", character.ID) - costResult.CharacterClass = characterClass - costResult.SkillName = request.Name - - err = gsmaster.GetLernCostNextLevelOld(&tempRequest, &costResult, request.Reward, nextLevel, character.Rasse) - if err != nil { - respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Fehler bei Level %d: %v", nextLevel, err)) - return - } - - // Addiere die Kosten - totalEP += costResult.EP - totalGold += costResult.GoldCost - totalPP += costResult.PPUsed - } - - // Prüfe, ob genügend EP vorhanden sind - currentEP := character.Erfahrungsschatz.EP - if currentEP < totalEP { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Erfahrungspunkte vorhanden") - return - } - - // Prüfe, ob genügend Gold vorhanden ist - currentGold := character.Vermoegen.Goldstücke - if currentGold < totalGold { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Gold vorhanden") - return - } - - // Prüfe, ob genügend PP vorhanden sind (PP der jeweiligen Fertigkeit) - für neue Fertigkeiten normalerweise 0 - currentPP := 0 - for _, skill := range character.Fertigkeiten { - if skill.Name == request.Name { - currentPP = skill.Pp - break - } - } - // Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten - if currentPP == 0 { - for _, skill := range character.Waffenfertigkeiten { - if skill.Name == request.Name { - currentPP = skill.Pp - break - } - } - } - if totalPP > 0 && currentPP < totalPP { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Praxispunkte vorhanden") - return - } - - // EP abziehen und Audit-Log erstellen - newEP := currentEP - totalEP - if totalEP > 0 { - var notes string - if finalLevel > 1 { - notes = fmt.Sprintf("Fertigkeit '%s' bis Level %d gelernt", request.Name, finalLevel) - } else { - notes = fmt.Sprintf("Fertigkeit '%s' gelernt", request.Name) - } - - err = CreateAuditLogEntry(character.ID, "experience_points", currentEP, newEP, ReasonSkillLearning, 0, notes) - if err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Erstellen des Audit-Log-Eintrags") - return - } - character.Erfahrungsschatz.EP = newEP - if err := database.DB.Save(&character.Erfahrungsschatz).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern der Erfahrungspunkte") - return - } - } - - // Gold abziehen und Audit-Log erstellen - newGold := currentGold - totalGold - if totalGold > 0 { - notes := fmt.Sprintf("Gold für Fertigkeit '%s' ausgegeben", request.Name) - - err = CreateAuditLogEntry(character.ID, "gold", currentGold, newGold, ReasonSkillLearning, 0, notes) - if err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Erstellen des Audit-Log-Eintrags") - return - } - character.Vermoegen.Goldstücke = newGold - if err := database.DB.Save(&character.Vermoegen).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Vermögens") - return - } - } - - // PP abziehen (falls vorhanden und erforderlich) - if totalPP > 0 { - // Suche die richtige Fertigkeit und ziehe PP ab - for i, skill := range character.Fertigkeiten { - if skill.Name == request.Name { - character.Fertigkeiten[i].Pp -= totalPP - break - } - } - // Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten - for i, skill := range character.Waffenfertigkeiten { - if skill.Name == request.Name { - character.Waffenfertigkeiten[i].Pp -= totalPP - break - } - } - } - - // Erstelle die neue Fertigkeit mit dem finalen Level - if err := updateOrCreateSkill(&character, request.Name, finalLevel); err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Hinzufügen der Fertigkeit: "+err.Error()) - return - } - - // Charakter speichern - if err := database.DB.Save(&character).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Charakters") - return - } - - // Response für Multi-Level oder Single-Level - response := gin.H{ - "message": "Fertigkeit erfolgreich gelernt", - "skill_name": request.Name, - "final_level": finalLevel, - "ep_cost": totalEP, - "gold_cost": totalGold, - "remaining_ep": newEP, - "remaining_gold": newGold, - } - - // Füge Multi-Level-spezifische Informationen hinzu - if finalLevel > 1 { - // Erstelle Array der gelernten Level für Kompatibilität - var levelsLearned []int - for i := 1; i <= finalLevel; i++ { - levelsLearned = append(levelsLearned, i) - } - response["levels_learned"] = levelsLearned - response["level_count"] = finalLevel - response["multi_level"] = true - } - - c.JSON(http.StatusOK, response) -} - // LearnSkill lernt eine neue Fertigkeit und erstellt Audit-Log-Einträge func LearnSkill(c *gin.Context) { charID := c.Param("id") @@ -1485,305 +1107,6 @@ func ImproveSkill(c *gin.Context) { c.JSON(http.StatusOK, responseData) } -// ImproveSkillOld is deprecated. Use ImproveSkill instead. -// This function uses the old hardcoded learning cost system. -// ImproveSkillOld verbessert eine bestehende Fertigkeit und erstellt Audit-Log-Einträge -func ImproveSkillOld(c *gin.Context) { - // Verwende gsmaster.LernCostRequest direkt - var request gsmaster.LernCostRequest - if err := c.ShouldBindJSON(&request); err != nil { - respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error()) - return - } - - // Hole Charakter über die ID aus dem Request - var char models.Char - err := database.DB. - Preload("Fertigkeiten"). - Preload("Waffenfertigkeiten"). - Preload("Erfahrungsschatz"). - Preload("Vermoegen"). - First(&char, request.CharId).Error - if 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(char.Typ) > 3 { - characterClass = gsmaster.GetClassAbbreviationOld(char.Typ) - } else { - characterClass = char.Typ - } - - // Aktuellen Level ermitteln, falls nicht angegeben - currentLevel := request.CurrentLevel - if currentLevel <= 0 { - currentLevel = getCurrentSkillLevel(&char, request.Name, "skill") - if currentLevel == -1 { - respondWithError(c, http.StatusBadRequest, "Fertigkeit nicht bei diesem Charakter vorhanden") - return - } - request.CurrentLevel = currentLevel - } - - // Bestimme das finale Level - finalLevel := request.TargetLevel - if finalLevel <= 0 { - finalLevel = currentLevel + 1 - } - - // Initialisiere Gesamtkosten - var totalEP, totalGold, totalPP int - - // Loop für jeden Level von currentLevel bis finalLevel - tempLevel := currentLevel - for tempLevel < finalLevel { - nextLevel := tempLevel + 1 - - // Erstelle temporären Request für diesen Level - tempRequest := request - tempRequest.CurrentLevel = tempLevel - tempRequest.TargetLevel = nextLevel - - // Berechne Kosten für diesen einen Level - var costResult gsmaster.SkillCostResultNew - costResult.CharacterID = fmt.Sprintf("%d", char.ID) - costResult.CharacterClass = characterClass - costResult.SkillName = request.Name - - err = gsmaster.GetLernCostNextLevelOld(&tempRequest, &costResult, request.Reward, nextLevel, char.Rasse) - if err != nil { - respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Fehler bei Level %d: %v", nextLevel, err)) - return - } - - // Addiere die Kosten - totalEP += costResult.EP - totalGold += costResult.GoldCost - totalPP += costResult.PPUsed - - tempLevel++ - } - - // Prüfe, ob genügend EP vorhanden sind - currentEP := char.Erfahrungsschatz.EP - if currentEP < totalEP { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Erfahrungspunkte vorhanden") - return - } - - // Prüfe, ob genügend Gold vorhanden ist - currentGold := char.Vermoegen.Goldstücke - if currentGold < totalGold { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Gold vorhanden") - return - } - - // Prüfe, ob genügend PP vorhanden sind (PP der jeweiligen Fertigkeit) - currentPP := 0 - for _, skill := range char.Fertigkeiten { - if skill.Name == request.Name { - currentPP = skill.Pp - break - } - } - // Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten - if currentPP == 0 { - for _, skill := range char.Waffenfertigkeiten { - if skill.Name == request.Name { - currentPP = skill.Pp - break - } - } - } - if totalPP > 0 && currentPP < totalPP { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Praxispunkte vorhanden") - return - } - - // EP abziehen und Audit-Log erstellen - newEP := currentEP - totalEP - if totalEP > 0 { - // Erstelle Notiz für Multi-Level Improvement - levelCount := finalLevel - currentLevel - var notes string - if levelCount > 1 { - notes = fmt.Sprintf("Fertigkeit '%s' von %d auf %d verbessert (%d Level)", request.Name, currentLevel, finalLevel, levelCount) - } else { - notes = fmt.Sprintf("Fertigkeit '%s' von %d auf %d verbessert", request.Name, currentLevel, finalLevel) - } - - err = CreateAuditLogEntry(char.ID, "experience_points", currentEP, newEP, ReasonSkillImprovement, 0, notes) - if err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Erstellen des Audit-Log-Eintrags") - return - } - char.Erfahrungsschatz.EP = newEP - if err := database.DB.Save(&char.Erfahrungsschatz).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern der Erfahrungspunkte") - return - } - } - - // Gold abziehen und Audit-Log erstellen - newGold := currentGold - totalGold - if totalGold > 0 { - notes := fmt.Sprintf("Gold für Verbesserung von '%s' ausgegeben", request.Name) - - err = CreateAuditLogEntry(char.ID, "gold", currentGold, newGold, ReasonSkillImprovement, 0, notes) - if err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Erstellen des Audit-Log-Eintrags") - return - } - char.Vermoegen.Goldstücke = newGold - if err := database.DB.Save(&char.Vermoegen).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Vermögens") - return - } - } - - // PP abziehen wenn verwendet (PP der jeweiligen Fertigkeit) - if totalPP > 0 { - // Finde die richtige Fertigkeit und ziehe PP ab - for i := range char.Fertigkeiten { - if char.Fertigkeiten[i].Name == request.Name { - char.Fertigkeiten[i].Pp -= totalPP - if err := database.DB.Save(&char.Fertigkeiten[i]).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Aktualisieren der Praxispunkte") - return - } - break - } - } - // Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten - for i := range char.Waffenfertigkeiten { - if char.Waffenfertigkeiten[i].Name == request.Name { - char.Waffenfertigkeiten[i].Pp -= totalPP - if err := database.DB.Save(&char.Waffenfertigkeiten[i]).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Aktualisieren der Praxispunkte") - return - } - break - } - } - } - - // Aktualisiere die Fertigkeit mit dem neuen Level - if err := updateOrCreateSkill(&char, request.Name, finalLevel); err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Aktualisieren der Fertigkeit: "+err.Error()) - return - } - - // Charakter speichern - if err := database.DB.Save(&char).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Charakters") - return - } - - // Response für Multi-Level oder Single-Level - response := gin.H{ - "message": "Fertigkeit erfolgreich verbessert", - "skill_name": request.Name, - "from_level": currentLevel, - "to_level": finalLevel, - "ep_cost": totalEP, - "gold_cost": totalGold, - "remaining_ep": newEP, - "remaining_gold": newGold, - } - - // Füge Multi-Level-spezifische Informationen hinzu - levelCount := finalLevel - currentLevel - if levelCount > 1 { - // Erstelle Array der gelernten Level für Kompatibilität - var levelsLearned []int - for i := currentLevel + 1; i <= finalLevel; i++ { - levelsLearned = append(levelsLearned, i) - } - response["levels_learned"] = levelsLearned - response["level_count"] = levelCount - response["multi_level"] = true - } - - c.JSON(http.StatusOK, response) -} - -// LearnSpellOld is deprecated. Use LearnSpell instead. -// This function uses the old hardcoded learning cost system. -// LearnSpellOld lernt einen neuen Zauber und erstellt Audit-Log-Einträge -func LearnSpellOld(c *gin.Context) { - charID := c.Param("id") - var character models.Char - - if err := character.FirstID(charID); err != nil { - respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden") - return - } - - var request LearnSpellRequest - if err := c.ShouldBindJSON(&request); err != nil { - respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error()) - return - } - - // Berechne Kosten mit GetSkillCost - costRequest := SkillCostRequest{ - Name: request.Name, - Type: "spell", - Action: "learn", - } - - cost, _, _, err := calculateSingleCostOld(&character, &costRequest) - if err != nil { - respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error()) - return - } - - // Prüfe, ob genügend EP vorhanden sind - currentEP := character.Erfahrungsschatz.EP - if currentEP < cost.Ep { - respondWithError(c, http.StatusBadRequest, "Nicht genügend Erfahrungspunkte vorhanden") - return - } - - // EP abziehen und Audit-Log erstellen - newEP := currentEP - cost.Ep - if cost.Ep > 0 { - notes := fmt.Sprintf("Zauber '%s' gelernt", request.Name) - if request.Notes != "" { - notes += " - " + request.Notes - } - - err = CreateAuditLogEntry(character.ID, "experience_points", currentEP, newEP, ReasonSpellLearning, 0, notes) - if err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Erstellen des Audit-Log-Eintrags") - return - } - character.Erfahrungsschatz.EP = newEP - } - - // Füge den Zauber zum Charakter hinzu - if err := addSpellToCharacter(&character, request.Name); err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Hinzufügen des Zaubers: "+err.Error()) - return - } - - // Charakter speichern - if err := database.DB.Save(&character).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Charakters") - return - } - - c.JSON(http.StatusOK, gin.H{ - "message": "Zauber erfolgreich gelernt", - "spell_name": request.Name, - "ep_cost": cost.Ep, - "remaining_ep": newEP, - }) -} - // validateSpellForLearning validiert Zauber-Namen für neue Zauber (Learning) func validateSpellForLearning(char *models.Char, request *gsmaster.LernCostRequest) (string, *models.SpellLearningInfo, int, error) { // Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist @@ -1946,10 +1269,10 @@ func LearnSpell(c *gin.Context) { c.JSON(http.StatusOK, responseData) } -// GetRewardTypesOld is deprecated. Use GetRewardTypes instead. +// GetRewardTypesStatic is deprecated. Use GetRewardTypes instead. // This function provides hardcoded reward type mappings. -// GetRewardTypesOld liefert verfügbare Belohnungsarten für ein bestimmtes Lernszenario -func GetRewardTypesOld(c *gin.Context) { +// GetRewardTypesStatic liefert verfügbare Belohnungsarten für ein bestimmtes Lernszenario +func GetRewardTypesStatic(c *gin.Context) { characterID := c.Param("id") learningType := c.Query("learning_type") // 'improve', 'learn', 'spell' skillName := c.Query("skill_name") @@ -2070,7 +1393,7 @@ func GetAvailableSkillsNewSystem(c *gin.Context) { if baseRequest.CharId != 0 { // Use existing character data characterID = fmt.Sprintf("%d", character.ID) - characterClass = getCharacterClassOld(&character) + characterClass = getCharacterClass(&character) } // For character creation, we don't have a character class yet, use empty string @@ -2581,7 +1904,13 @@ func GetAllSkillsWithLearningCosts(characterClass string) (map[string][]gin.H, e } // Try to get the best category and learning cost for this skill and character class - bestCategory, difficulty, err := gsmaster.FindBestCategoryForSkillLearningOld(skill.Name, characterClass) + + skillInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skill.Name, characterClass) + if err != nil { + return nil, err + } + bestCategory := skillInfo.CategoryName + difficulty := skillInfo.DifficultyName var learnCost int if err == nil && bestCategory != "" { @@ -2717,7 +2046,7 @@ func GetAvailableSpellsNewSystem(c *gin.Context) { return } - charakteClass := getCharacterClassOld(&character) + charakteClass := getCharacterClass(&character) // Hole alle verfügbaren Zauber aus der gsmaster Datenbank, aber filtere Placeholder aus var allSpells []models.Spell @@ -2849,120 +2178,6 @@ func GetSpellDetails(c *gin.Context) { }) } -// GetAvailableSkillsOld is deprecated. Use GetAvailableSkillsNewSystem instead. -// This function uses the old hardcoded learning cost system. -// GetAvailableSkillsOld gibt alle verfügbaren Fertigkeiten mit Lernkosten zurück -func GetAvailableSkillsOld(c *gin.Context) { - characterID := c.Param("id") - rewardType := c.Query("reward_type") - - var character models.Char - if err := database.DB.Preload("Fertigkeiten").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, characterID).Error; err != nil { - respondWithError(c, http.StatusNotFound, "Character not found") - return - } - - // Hole alle verfügbaren Fertigkeiten aus der gsmaster Datenbank, aber filtere Placeholder aus - var allSkills []models.Skill - - allSkills, err := models.SelectSkills("", "") - if err != nil { - respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skills from gsmaster") - return - } - /*if err := database.DB.Where("name != ?", "Placeholder").Find(&allSkills).Error; err != nil { - respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skills") - return - } - */ - - // Erstelle eine Map der bereits gelernten Fertigkeiten - learnedSkills := make(map[string]bool) - for _, skill := range character.Fertigkeiten { - learnedSkills[skill.Name] = true - } - - // Organisiere Fertigkeiten nach Kategorien - skillsByCategory := make(map[string][]gin.H) - - for _, skill := range allSkills { - // Überspringe bereits gelernte Fertigkeiten - if learnedSkills[skill.Name] { - continue - } - - // Überspringe Placeholder-Fertigkeiten (zusätzliche Sicherheit) - if skill.Name == "Placeholder" { - continue - } - - // Berechne Lernkosten mit GetLernCostNextLevel - epCost, goldCost := calculateSkillLearningCostsOld(skill, character, rewardType) - - skillInfo := gin.H{ - "name": skill.Name, - "epCost": epCost, - "goldCost": goldCost, - } - - category := skill.Category - if category == "" { - category = "Sonstige" - } - - skillsByCategory[category] = append(skillsByCategory[category], skillInfo) - } - - c.JSON(http.StatusOK, gin.H{ - "skills_by_category": skillsByCategory, - }) -} - -// calculateSkillLearningCostsOld is deprecated. Use calculateSkillLearnCostNewSystem instead. -// This function uses the old hardcoded learning cost system. -// calculateSkillLearningCostsOld berechnet die EP- und Goldkosten für das Lernen einer Fertigkeit mit GetLernCostNextLevel -func calculateSkillLearningCostsOld(skill models.Skill, character models.Char, rewardType string) (int, int) { - // Erstelle LernCostRequest für das Lernen (Level 0 -> 1) - var rewardTypePtr *string - if rewardType != "" && rewardType != "default" { - rewardTypePtr = &rewardType - } - - request := gsmaster.LernCostRequest{ - CharId: character.ID, - Name: skill.Name, - CurrentLevel: 0, // Nicht gelernt - TargetLevel: 1, // Auf Level 1 lernen - Type: "skill", - Action: "learn", - UsePP: 0, - UseGold: 0, - Reward: rewardTypePtr, - } - - // Erstelle SkillCostResultNew - costResult := gsmaster.SkillCostResultNew{ - CharacterID: fmt.Sprintf("%d", character.ID), - CharacterClass: getCharacterClassOld(&character), - SkillName: skill.Name, - Category: skill.Category, - Difficulty: skill.Difficulty, - TargetLevel: 1, - } - - // Berechne Kosten mit GetLernCostNextLevel - err := gsmaster.GetLernCostNextLevelOld(&request, &costResult, rewardTypePtr, 1, character.Typ) - if err != nil { - // Fallback zu Standard-Kosten bei Fehler - epCost := 100 - goldCost := 50 - - return epCost, goldCost - } - - return costResult.EP, costResult.GoldCost -} - // Character Creation Session Management // CreateCharacterSession erstellt eine neue Charakter-Erstellungssession diff --git a/backend/character/handlers_test.go b/backend/character/handlers_test.go index a2e84b2..1c0f013 100644 --- a/backend/character/handlers_test.go +++ b/backend/character/handlers_test.go @@ -161,103 +161,6 @@ func TestImproveSkillHandler(t *testing.T) { t.Logf("Gold: %d -> %d (cost: %.0f)", 390, updatedChar.Vermoegen.Goldstücke, response["gold_cost"]) }) - t.Run("ImproveSkill with insufficient EP", func(t *testing.T) { - // Create character with insufficient EP - poorChar := models.Char{ - BamortBase: models.BamortBase{ - ID: 21, - Name: "Poor Test Character", - }, - Typ: "Krieger", - Rasse: "Mensch", - Grad: 1, - Erfahrungsschatz: models.Erfahrungsschatz{ - BamortCharTrait: models.BamortCharTrait{ - CharacterID: 21, - }, - ES: 5, // Insufficient EP - }, - Vermoegen: models.Vermoegen{ - BamortCharTrait: models.BamortCharTrait{ - CharacterID: 21, - }, - Goldstücke: 100, - }, - } - - // Add skill - skill := models.SkFertigkeit{ - BamortCharTrait: models.BamortCharTrait{ - BamortBase: models.BamortBase{ - Name: "Athletik", - }, - CharacterID: 21, - }, - Fertigkeitswert: 9, - } - poorChar.Fertigkeiten = append(poorChar.Fertigkeiten, skill) - - err = poorChar.Create() - assert.NoError(t, err) - - requestData := map[string]interface{}{ - "char_id": 21, - "name": "Athletik", - "current_level": 9, - "target_level": 10, - "type": "skill", - "action": "improve", - "reward": "default", - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/improve-skill", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - ImproveSkillOld(c) - - assert.Equal(t, http.StatusBadRequest, w.Code, "Should return 400 for insufficient EP") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Contains(t, response, "error") - assert.Contains(t, response["error"], "Nicht genügend Erfahrungspunkte") - }) - - t.Run("ImproveSkill with nonexistent character", func(t *testing.T) { - requestData := map[string]interface{}{ - "char_id": 999, // Non-existent character - "name": "Athletik", - "current_level": 9, - "target_level": 10, - "type": "skill", - "action": "improve", - "reward": "default", - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/improve-skill", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - ImproveSkillOld(c) - - assert.Equal(t, http.StatusNotFound, w.Code, "Should return 404 for non-existent character") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err) - assert.Contains(t, response, "error") - assert.Contains(t, response["error"], "Charakter nicht gefunden") - }) } func TestGetAvailableSkillsNewSystem(t *testing.T) { diff --git a/backend/character/lerncost_comparison_test.go b/backend/character/lerncost_comparison_test.go deleted file mode 100644 index a8c370e..0000000 --- a/backend/character/lerncost_comparison_test.go +++ /dev/null @@ -1,551 +0,0 @@ -package character - -import ( - "bamort/database" - "bamort/gsmaster" - "bamort/models" - "bytes" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -// TestCompareOldVsNewLearningCostSystems vergleicht die Ergebnisse von GetLernCost und GetLernCostNewSystem -func TestCompareOldVsNewLearningCostSystems(t *testing.T) { - // Setup test database - database.SetupTestDB(true, true) - defer database.ResetTestDB() - - // Migrate the schema - err := models.MigrateStructure() - assert.NoError(t, err) - - // Setup Gin in test mode - gin.SetMode(gin.TestMode) - - testCases := []struct { - name string - charId uint - skillName string - currentLevel int - TargetLevel int `json:"target_level,omitempty"` // Zielwert (optional, für Kostenberechnung bis zu einem bestimmten Level) - usePP int - useGold int - description string - Type string `json:"type" binding:"required,oneof=skill spell weapon"` // 'skill', 'spell' oder 'weapon' Waffenfertigkeiten sind normale Fertigkeiten (evtl. kann hier später der Name der Waffe angegeben werden ) - Action string `json:"action" binding:"required,oneof=learn improve"` // 'learn' oder 'improve' - Reward *string `json:"reward" binding:"required,oneof=default noGold halveep halveepnoGold"` // Belohnungsoptionen Lernen als Belohnung - }{ - { - name: "improve Athletik Basic Test", - charId: 20, - skillName: "Athletik", - currentLevel: 9, - usePP: 0, - useGold: 0, - description: "Basic comparison for Athletik skill improvement", - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - //"improve Athletik with PP", - { - name: "improve Athletik with PP", - charId: 20, - skillName: "Athletik", - currentLevel: 9, - usePP: 10, - useGold: 0, - description: "Comparison with Practice Points usage", - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - //"improve Athletik with Gold", - { - name: "improve Athletik with Gold", - charId: 20, - skillName: "Athletik", - currentLevel: 9, - usePP: 0, - useGold: 100, - description: "Comparison with Gold usage", - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - //improve Athletik with 2000 Gold - { - name: "improve Athletik with 2000 Gold", - charId: 20, - skillName: "Athletik", - currentLevel: 9, - usePP: 0, - useGold: 2000, - description: "Comparison with Gold usage", - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // improve Athletik with 15 PP and 150 Gold - { - name: "improve Athletik with PP and Gold", - charId: 20, - skillName: "Athletik", - currentLevel: 9, - usePP: 15, - useGold: 150, - description: "Comparison with both PP and Gold usage", - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Naturkunde Basic Test - { - name: "learn Naturkunde Basic Test", - charId: 20, - skillName: "Naturkunde", - currentLevel: 9, - usePP: 0, - useGold: 0, - description: "Comparison Learn Basic", - Type: "skill", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Naturkunde Test 100 Gold - { - name: "learn Naturkunde Test 100 Gold", - charId: 20, - skillName: "Naturkunde", - currentLevel: 9, - usePP: 0, - useGold: 100, - description: "Comparison Learn 100 Gold", - Type: "skill", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Naturkunde Test 2000 Gold - { - name: "learn Naturkunde Test 2000 Gold", - charId: 20, - skillName: "Naturkunde", - currentLevel: 9, - usePP: 0, - useGold: 2000, - description: "Comparison Learn 2000 Gold", - Type: "skill", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Naturkunde Test 2 PP - { - name: "learn Naturkunde Test 2 PP", - charId: 20, - skillName: "Naturkunde", - currentLevel: 9, - usePP: 2, - useGold: 0, - description: "Comparison Learn 2 PP", - Type: "skill", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Beeinflussen Test Basic - { - name: "learn Beeinflussen Test Basic", - charId: 18, - skillName: "Beeinflussen", - currentLevel: 9, - usePP: 0, - useGold: 0, - description: "Comparison Learn ", - Type: "spell", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - - // learn Beeinflussen Test Basic 2 PP - { - name: "learn Beeinflussen Test Basic 2 PP", - charId: 18, - skillName: "Beeinflussen", - currentLevel: 9, - usePP: 2, - useGold: 0, - description: "Comparison Learn ", - Type: "spell", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Beeinflussen Test Basic 150 Gold - { - name: "learn Beeinflussen Test Basic", - charId: 18, - skillName: "Beeinflussen", - currentLevel: 9, - usePP: 0, - useGold: 150, - description: "Comparison Learn ", - Type: "spell", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Beeinflussen Test Basic 2PP and 150 Gold - { - name: "learn Beeinflussen Test Basic 2PP and 150 Gold", - charId: 18, - skillName: "Beeinflussen", - currentLevel: 9, - usePP: 2, - useGold: 150, - description: "Comparison Learn ", - Type: "spell", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - // learn Naturkunde Test 2 PP - { - name: "learn non existent Bogenbau Test Basic", - charId: 20, - skillName: "Bogenbau", - currentLevel: 0, - usePP: 0, - useGold: 0, - description: "Comparison Learn Bogenbau", - Type: "skill", - Action: "learn", - TargetLevel: 0, // Calculate all levels - Reward: &[]string{"default"}[0], - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - fmt.Printf("\n=== Comparing Old vs New System: %s ===\n", tc.description) - - // Prepare request data - requestData := gsmaster.LernCostRequest{ - CharId: tc.charId, - Name: tc.skillName, - CurrentLevel: tc.currentLevel, - Type: tc.Type, - Action: tc.Action, - TargetLevel: tc.TargetLevel, // Calculate all levels - UsePP: tc.usePP, - UseGold: tc.useGold, - Reward: tc.Reward, - } - requestBody, _ := json.Marshal(requestData) - - // Test Old System (GetLernCost) - reqOld, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - reqOld.Header.Set("Content-Type", "application/json") - - wOld := httptest.NewRecorder() - cOld, _ := gin.CreateTestContext(wOld) - cOld.Request = reqOld - - GetLernCost(cOld) - - // Test New System (GetLernCostNewSystem) - reqNew, _ := http.NewRequest("POST", "/api/characters/lerncost-new", bytes.NewBuffer(requestBody)) - reqNew.Header.Set("Content-Type", "application/json") - - wNew := httptest.NewRecorder() - cNew, _ := gin.CreateTestContext(wNew) - cNew.Request = reqNew - - GetLernCostNewSystem(cNew) - - // Check that both systems returned successful responses - fmt.Printf("Old System Status: %d, New System Status: %d\n", wOld.Code, wNew.Code) - - if wOld.Code != http.StatusOK && wNew.Code != http.StatusOK { - fmt.Printf("Both systems failed - Old: %s, New: %s\n", wOld.Body.String(), wNew.Body.String()) - t.Skip("Both systems failed, skipping comparison") - return - } - - if wOld.Code != http.StatusOK { - fmt.Printf("Old system failed: %s\n", wOld.Body.String()) - fmt.Printf("New system succeeded with status %d\n", wNew.Code) - t.Skip("Old system failed, new system comparison only") - return - } - - if wNew.Code != http.StatusOK { - fmt.Printf("New system failed: %s\n", wNew.Body.String()) - fmt.Printf("Old system succeeded with status %d\n", wOld.Code) - t.Skip("New system failed, old system comparison only") - return - } - - // Parse responses - var oldResponse []gsmaster.SkillCostResultNew - var newResponse []gsmaster.SkillCostResultNew - - err := json.Unmarshal(wOld.Body.Bytes(), &oldResponse) - assert.NoError(t, err, "Old system response should be valid JSON") - - err = json.Unmarshal(wNew.Body.Bytes(), &newResponse) - assert.NoError(t, err, "New system response should be valid JSON") - - // Compare basic structure - fmt.Printf("Old System: %d levels, New System: %d levels\n", len(oldResponse), len(newResponse)) - - // Create comparison table - fmt.Printf("\n=== Detailed Cost Comparison ===\n") - fmt.Printf("Level | Old EP | New EP | Old Gold | New Gold | Old LE | New LE | Old PP used | New PP used | Old Gold used | New Gold used | Match?\n") - fmt.Printf("------|--------|--------|----------|----------|--------|--------|-------------|-------------|---------------|---------------|-------\n") - - // Compare each level that exists in both systems - maxLevels := len(oldResponse) - if len(newResponse) < maxLevels { - maxLevels = len(newResponse) - } - - exactMatches := 0 - totalComparisons := 0 - - for i := 0; i < maxLevels; i++ { - old := oldResponse[i] - new := newResponse[i] - - // Check if target levels match - /*if old.TargetLevel != new.TargetLevel { - fmt.Printf("Level mismatch at index %d: Old=%d, New=%d\n", i, old.TargetLevel, new.TargetLevel) - continue - }*/ - - totalComparisons++ - - // Compare costs - epMatch := old.EP == new.EP - goldMatch := old.GoldCost == new.GoldCost - leMatch := old.LE == new.LE - ppMatch := old.PPUsed == new.PPUsed - goldUsedMatch := old.GoldUsed == new.GoldUsed - - overallMatch := epMatch && goldMatch && leMatch && ppMatch && goldUsedMatch - if overallMatch { - exactMatches++ - } - - matchSymbol := "✓" - if !overallMatch { - matchSymbol = "✗" - } - - fmt.Printf("%5d | %6d | %6d | %8d | %8d | %6d | %6d | %11d | %11d | %13d | %13d | %7s\n", - old.TargetLevel, old.EP, new.EP, old.GoldCost, new.GoldCost, - old.LE, new.LE, old.PPUsed, new.PPUsed, old.GoldUsed, new.GoldUsed, matchSymbol) - - // Individual field assertions for debugging - if !epMatch { - fmt.Printf(" EP mismatch at level %d: Old=%d, New=%d (diff=%d)\n", - old.TargetLevel, old.EP, new.EP, old.EP-new.EP) - } - if !goldMatch { - fmt.Printf(" GoldCost mismatch at level %d: Old=%d, New=%d (diff=%d)\n", - old.TargetLevel, old.GoldCost, new.GoldCost, old.GoldCost-new.GoldCost) - } - if !leMatch { - fmt.Printf(" LE mismatch at level %d: Old=%d, New=%d (diff=%d)\n", - old.TargetLevel, old.LE, new.LE, old.LE-new.LE) - } - if !ppMatch { - fmt.Printf(" PP used mismatch at level %d: Old=%d, New=%d (diff=%d)\n", - old.TargetLevel, old.PPUsed, new.PPUsed, old.PPUsed-new.PPUsed) - } - if !goldUsedMatch { - fmt.Printf(" GoldUsed mismatch at level %d: Old=%d, New=%d (diff=%d)\n", - old.TargetLevel, old.GoldUsed, new.GoldUsed, old.GoldUsed-new.GoldUsed) - } - - // Verify basic consistency within each system - assert.GreaterOrEqual(t, old.EP, 0, "Old system EP should be non-negative for level %d", old.TargetLevel) - assert.GreaterOrEqual(t, new.EP, 0, "New system EP should be non-negative for level %d", new.TargetLevel) - assert.GreaterOrEqual(t, old.LE, 0, "Old system LE should be non-negative for level %d", old.TargetLevel) - assert.GreaterOrEqual(t, new.LE, 0, "New system LE should be non-negative for level %d", new.TargetLevel) - } - - // Summary statistics - matchPercentage := float64(exactMatches) / float64(totalComparisons) * 100 - fmt.Printf("\n=== Comparison Summary ===\n") - fmt.Printf("Total Comparisons: %d\n", totalComparisons) - fmt.Printf("Exact Matches: %d\n", exactMatches) - fmt.Printf("Match Percentage: %.1f%%\n", matchPercentage) - - // Check for levels that exist in one system but not the other - if len(oldResponse) != len(newResponse) { - fmt.Printf("Length Difference: Old=%d levels, New=%d levels\n", len(oldResponse), len(newResponse)) - - if len(oldResponse) > len(newResponse) { - fmt.Printf("Old system has additional levels:\n") - for i := len(newResponse); i < len(oldResponse); i++ { - fmt.Printf(" Level %d: EP=%d, Gold=%d, LE=%d\n", - oldResponse[i].TargetLevel, oldResponse[i].EP, oldResponse[i].GoldCost, oldResponse[i].LE) - } - } else { - fmt.Printf("New system has additional levels:\n") - for i := len(oldResponse); i < len(newResponse); i++ { - fmt.Printf(" Level %d: EP=%d, Gold=%d, LE=%d\n", - newResponse[i].TargetLevel, newResponse[i].EP, newResponse[i].GoldCost, newResponse[i].LE) - } - } - } - - // Basic assertions for test validation - assert.Greater(t, totalComparisons, 0, "Should have at least one level to compare") - - // If systems are supposed to be equivalent, we might want exact matches - // For now, we'll just document the differences and ensure both are reasonable - if matchPercentage < 50.0 { - t.Logf("WARNING: Low match percentage (%.1f%%) between old and new systems", matchPercentage) - } - - // Verify that both systems produce reasonable results - if len(oldResponse) > 0 { - assert.Equal(t, tc.skillName, oldResponse[0].SkillName, "Old system should return correct skill name") - assert.Equal(t, fmt.Sprintf("%d", tc.charId), oldResponse[0].CharacterID, "Old system should return correct character ID") - } - - if len(newResponse) > 0 { - assert.Equal(t, tc.skillName, newResponse[0].SkillName, "New system should return correct skill name") - assert.Equal(t, fmt.Sprintf("%d", tc.charId), newResponse[0].CharacterID, "New system should return correct character ID") - } - }) - } - - // Summary test to compare system performance - t.Run("System Performance Summary", func(t *testing.T) { - fmt.Printf("\n=== Overall System Comparison Summary ===\n") - fmt.Printf("Both systems tested with various configurations:\n") - fmt.Printf("- Basic skill improvement (no resources)\n") - fmt.Printf("- With Practice Points usage\n") - fmt.Printf("- With Gold usage\n") - fmt.Printf("- With combined PP and Gold usage\n") - fmt.Printf("\nKey observations should be documented above.\n") - fmt.Printf("Differences may indicate:\n") - fmt.Printf("1. Different calculation methods\n") - fmt.Printf("2. Different data sources (old vs new tables)\n") - fmt.Printf("3. Implementation bugs in either system\n") - fmt.Printf("4. Different business rules or assumptions\n") - }) -} - -// TestPerformanceComparison vergleicht die Performance der beiden Systeme -func TestPerformanceComparison(t *testing.T) { - // Setup test database - database.SetupTestDB(true, true) - defer database.ResetTestDB() - - // Migrate the schema - err := models.MigrateStructure() - assert.NoError(t, err) - - // Setup Gin in test mode - gin.SetMode(gin.TestMode) - - fmt.Printf("\n=== Performance Comparison ===\n") - - // Prepare test data - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 5, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - // Test old system multiple times - oldSystemTimes := make([]int64, 10) - for i := 0; i < 10; i++ { - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - start := getCurrentTime() - GetLernCost(c) - oldSystemTimes[i] = getCurrentTime() - start - - if w.Code != http.StatusOK { - t.Logf("Old system failed on iteration %d: %s", i, w.Body.String()) - } - } - - // Test new system multiple times - newSystemTimes := make([]int64, 10) - for i := 0; i < 10; i++ { - req, _ := http.NewRequest("POST", "/api/characters/lerncost-new", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - start := getCurrentTime() - GetLernCostNewSystem(c) - newSystemTimes[i] = getCurrentTime() - start - - if w.Code != http.StatusOK { - t.Logf("New system failed on iteration %d: %s", i, w.Body.String()) - } - } - - // Calculate averages - oldAvg := calculateAverage(oldSystemTimes) - newAvg := calculateAverage(newSystemTimes) - - fmt.Printf("Old System Average Time: %d μs\n", oldAvg) - fmt.Printf("New System Average Time: %d μs\n", newAvg) - - if newAvg < oldAvg { - improvement := float64(oldAvg-newAvg) / float64(oldAvg) * 100 - fmt.Printf("New system is %.1f%% faster\n", improvement) - } else { - regression := float64(newAvg-oldAvg) / float64(oldAvg) * 100 - fmt.Printf("New system is %.1f%% slower\n", regression) - } -} - -// Helper functions for performance testing -func getCurrentTime() int64 { - return 0 // Placeholder - in real implementation would use time.Now().UnixNano() -} - -func calculateAverage(times []int64) int64 { - var sum int64 - for _, t := range times { - sum += t - } - return sum / int64(len(times)) -} diff --git a/backend/character/lerncost_handler.go b/backend/character/lerncost_handler.go index 1ec50eb..2f5ade3 100644 --- a/backend/character/lerncost_handler.go +++ b/backend/character/lerncost_handler.go @@ -66,92 +66,6 @@ type MultiLevelCostResponse struct { CanAffordTotal bool `json:"can_afford_total"` } -// GetLernCost -func GetLernCost(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 - } - 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 - } - var costResult gsmaster.SkillCostResultNew - costResult.CharacterID = charID - - // Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist - if len(character.Typ) > 3 { - costResult.CharacterClass = gsmaster.GetClassAbbreviationOld(character.Typ) - } else { - costResult.CharacterClass = character.Typ - } - - // Normalize skill name (trim whitespace, proper case) - costResult.SkillName = strings.TrimSpace(request.Name) - - // Lasse Kategorie und Schwierigkeit leer, damit CalcSkillLernCost die beste Option wählt - // costResult.Category = gsmaster.GetSkillCategory(request.Name) - // costResult.Difficulty = gsmaster.GetSkillDifficulty(costResult.Category, costResult.SkillName) - var response []gsmaster.SkillCostResultNew - - // Für "learn" Aktion: nur eine Berechnung, da Lernkosten einmalig sind - if request.Action == "learn" { - levelResult := gsmaster.SkillCostResultNew{ - CharacterID: costResult.CharacterID, - CharacterClass: costResult.CharacterClass, - SkillName: costResult.SkillName, - Category: costResult.Category, - Difficulty: costResult.Difficulty, - TargetLevel: 1, // Lernkosten sind für das Erlernen der Fertigkeit (Level 1) - } - err := gsmaster.GetLernCostNextLevelOld(&request, &levelResult, request.Reward, 1, character.Typ) - 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 - for i := request.CurrentLevel + 1; i <= 18; i++ { - levelResult := gsmaster.SkillCostResultNew{ - CharacterID: costResult.CharacterID, - CharacterClass: costResult.CharacterClass, - SkillName: costResult.SkillName, - Category: costResult.Category, - Difficulty: costResult.Difficulty, - TargetLevel: i, - } - err := gsmaster.GetLernCostNextLevelOld(&request, &levelResult, request.Reward, i, character.Typ) - 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) -} - // GetLernCostNewSystem verwendet das neue Datenbank-Lernkosten-System // und produziert die gleichen Ergebnisse wie GetLernCost. // @@ -530,134 +444,6 @@ func applySpellRewardNewSystem(result *gsmaster.SkillCostResultNew, reward *stri } } -// GetSkillCost berechnet die Kosten zum Erlernen oder Verbessern einer Fertigkeit -func GetSkillCost(c *gin.Context) { - // Charakter-ID aus der URL abrufen - charID := c.Param("id") - - // Charakter aus der Datenbank laden - var character models.Char - if err := character.FirstID(charID); err != nil { - respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden") - return - } - - // Request-Parameter abrufen - var request SkillCostRequest - if err := c.ShouldBindJSON(&request); err != nil { - respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error()) - return - } - - // Normalize skill name (trim whitespace, proper case) - request.Name = strings.TrimSpace(request.Name) - - // Validate current level for improvement - if request.Action == "improve" && request.CurrentLevel <= 0 { - // Try to get current level from character's skills - currentLevel := getCurrentSkillLevel(&character, request.Name, request.Type) - if currentLevel == -1 { - respondWithError(c, http.StatusBadRequest, "Fertigkeit nicht bei diesem Charakter vorhanden oder current_level erforderlich") - return - } - request.CurrentLevel = currentLevel - } - - // Handle multi-level cost calculation - if request.TargetLevel > 0 && request.Action == "improve" { - response := calculateMultiLevelCost(&character, &request) - if response == nil { - respondWithError(c, http.StatusBadRequest, "Fehler bei der Multi-Level-Kostenberechnung") - return - } - c.JSON(http.StatusOK, response) - return - } - - // Single cost calculation - cost, originalCost, skillInfo, err := calculateSingleCostOld(&character, &request) - if err != nil { - respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error()) - return - } - - // Originalkosten berechnen (ohne PP-Reduktion) - originalRequest := request - originalRequest.UsePP = 0 - _, _, _, err = calculateSingleCostOld(&character, &originalRequest) - if err != nil { - respondWithError(c, http.StatusBadRequest, "Fehler bei der ursprünglichen Kostenberechnung: "+err.Error()) - return - } - - // PP-Informationen sammeln (fertigkeitsspezifisch) - availablePP := getPPForSkill(&character, request.Name) - ppUsed := request.UsePP - if ppUsed > availablePP { - ppUsed = availablePP - } - - // PP-Reduktion berechnen - var ppReduction int - if request.UsePP > 0 { - if request.Action == "improve" { - ppReduction = ppUsed // PP entsprechen direkt der TE-Reduktion - } else if request.Action == "learn" && request.Type == "spell" { - ppReduction = ppUsed // PP entsprechen direkt der LE-Reduktion - } - } - - // Check if character can afford it - canAfford := canCharacterAfford(&character, cost) - - // Belohnungsinformationen berechnen - var rewardApplied string - var savings *models.LearnCost - var goldUsedForEP int - - if request.Reward != nil && request.Reward.Type != "" { - rewardApplied = request.Reward.Type - - // Ersparnisse berechnen - savings = &models.LearnCost{ - Ep: originalCost.Ep - cost.Ep, - LE: originalCost.LE - cost.LE, - Money: originalCost.Money - cost.Money, - } - - // Gold für EP berechnen - if request.Reward.Type == "gold_for_ep" && request.Reward.UseGoldForEP { - goldUsedForEP = (cost.Money - originalCost.Money) / 10 - } - } - - // Create response - response := &SkillCostResponse{ - LearnCost: cost, - SkillName: request.Name, - SkillType: request.Type, - Action: request.Action, - CharacterID: character.ID, - CurrentLevel: request.CurrentLevel, - TargetLevel: request.TargetLevel, - Category: skillInfo.Category, - Difficulty: skillInfo.Difficulty, - CanAfford: canAfford, - Notes: generateNotes(&character, &request, cost), - PPUsed: ppUsed, - PPAvailable: availablePP, - PPReduction: ppReduction, - OriginalCost: originalCost.Ep, - FinalCost: cost.Ep, - RewardApplied: rewardApplied, - OriginalCostStruct: originalCost, - Savings: savings, - GoldUsedForEP: goldUsedForEP, - } - - c.JSON(http.StatusOK, response) -} - // Helper function to get current skill level from character func getCurrentSkillLevel(character *models.Char, skillName, skillType string) int { switch skillType { @@ -680,69 +466,6 @@ func getCurrentSkillLevel(character *models.Char, skillName, skillType string) i return -1 } -// calculateSingleCostOld is deprecated. Use calculateSkillLearnCostNewSystem or calculateSpellLearnCostNewSystem instead. -// This function uses the old hardcoded learning cost system. -func calculateSingleCostOld(character *models.Char, request *SkillCostRequest) (*models.LearnCost, *models.LearnCost, *skillInfo, error) { - var cost *models.LearnCost - var err error - var info skillInfo - - switch { - case request.Action == "learn" && request.Type == "skill": - cost, err = gsmaster.CalculateDetailedSkillLearningCostOld(request.Name, character.Typ) - if err == nil { - info = GetSkillInfoCategoryAndDifficultyOld(request.Name, request.Type) - } - - case request.Action == "improve" && request.Type == "skill": - cost, err = gsmaster.CalculateDetailedSkillImprovementCostOld(request.Name, character.Typ, request.CurrentLevel) - if err == nil { - info = GetSkillInfoCategoryAndDifficultyOld(request.Name, request.Type) - } - - case request.Action == "improve" && request.Type == "weapon": - cost, err = gsmaster.CalculateDetailedSkillImprovementCostOld(request.Name, character.Typ, request.CurrentLevel) - if err == nil { - info = GetSkillInfoCategoryAndDifficultyOld(request.Name, request.Type) - } - - case request.Action == "learn" && request.Type == "spell": - cost, err = gsmaster.CalculateDetailedSpellLearningCostOld(request.Name, character.Typ) - if err == nil { - info = getSpellInfo(request.Name) - } - - default: - return nil, nil, nil, fmt.Errorf("ungültige Kombination aus Aktion und Typ") - } - - if err != nil { - return nil, nil, nil, err - } - - // Belohnungen anwenden, falls spezifiziert - originalCost := *cost // Kopie der ursprünglichen Kosten - if request.Reward != nil && request.Reward.Type != "" { - cost = applyReward(cost, request) - } - - // Praxispunkte anwenden, falls angefordert (fertigkeitsspezifisch) - if request.UsePP > 0 { - availablePP := getPPForSkill(character, request.Name) - finalEP, finalLE, _ := applyPPReduction(request, cost, availablePP) - - // Erstelle eine neue LearnCost mit den reduzierten Werten - cost = &models.LearnCost{ - Stufe: cost.Stufe, - LE: finalLE, - Ep: finalEP, - Money: cost.Money, // Geldkosten bleiben unverändert - } - } - - return cost, &originalCost, &info, err -} - // applyReward wendet Belohnungen auf die Kosten an func applyReward(cost *models.LearnCost, request *SkillCostRequest) *models.LearnCost { if request.Reward == nil || request.Reward.Type == "" { @@ -793,150 +516,12 @@ func applyReward(cost *models.LearnCost, request *SkillCostRequest) *models.Lear return &newCost } -// Helper function to calculate multi-level costs -func calculateMultiLevelCost(character *models.Char, request *SkillCostRequest) *MultiLevelCostResponse { - if request.TargetLevel <= request.CurrentLevel { - return nil - } - - var levelCosts []SkillCostResponse - totalEP := 0 - totalMoney := 0 - remainingPP := request.UsePP - - for level := request.CurrentLevel; level < request.TargetLevel; level++ { - tempRequest := *request - tempRequest.CurrentLevel = level - tempRequest.Action = "improve" - - // Verteile die PP auf die verschiedenen Level - if remainingPP > 0 { - tempRequest.UsePP = 1 // Maximal 1 PP pro Level - remainingPP-- - } else { - tempRequest.UsePP = 0 - } - - cost, originalCost, skillInfo, err := calculateSingleCostOld(character, &tempRequest) - if err != nil { - continue - } - - // Originalkosten berechnen (ohne PP) - originalRequest := tempRequest - originalRequest.UsePP = 0 - _, _, _, _ = calculateSingleCostOld(character, &originalRequest) - - // PP-Informationen sammeln (fertigkeitsspezifisch) - availablePP := getPPForSkill(character, request.Name) - ppUsed := tempRequest.UsePP - if ppUsed > availablePP { - ppUsed = availablePP - } - - ppReduction := 0 - if tempRequest.UsePP > 0 { - ppReduction = ppUsed - } - - // Belohnungsinformationen für Level berechnen - var rewardApplied string - var savings *models.LearnCost - var goldUsedForEP int - - if tempRequest.Reward != nil && tempRequest.Reward.Type != "" { - rewardApplied = tempRequest.Reward.Type - - // Ersparnisse berechnen - savings = &models.LearnCost{ - Ep: originalCost.Ep - cost.Ep, - LE: originalCost.LE - cost.LE, - Money: originalCost.Money - cost.Money, - } - - // Gold für EP berechnen - if tempRequest.Reward.Type == "gold_for_ep" && tempRequest.Reward.UseGoldForEP { - goldUsedForEP = (cost.Money - originalCost.Money) / 10 - } - } - - levelCost := SkillCostResponse{ - LearnCost: cost, - SkillName: request.Name, - SkillType: request.Type, - Action: "improve", - CharacterID: character.ID, - CurrentLevel: level, - TargetLevel: level + 1, - Category: skillInfo.Category, - Difficulty: skillInfo.Difficulty, - CanAfford: canCharacterAfford(character, cost), - PPUsed: ppUsed, - PPAvailable: availablePP, - PPReduction: ppReduction, - OriginalCost: originalCost.Ep, - FinalCost: cost.Ep, - RewardApplied: rewardApplied, - OriginalCostStruct: originalCost, - Savings: savings, - GoldUsedForEP: goldUsedForEP, - } - - levelCosts = append(levelCosts, levelCost) - totalEP += cost.Ep - totalMoney += cost.Money - } - - totalCost := &models.LearnCost{ - Stufe: request.TargetLevel, - LE: 0, - Ep: totalEP, - Money: totalMoney, - } - - return &MultiLevelCostResponse{ - SkillName: request.Name, - SkillType: request.Type, - CharacterID: character.ID, - CurrentLevel: request.CurrentLevel, - TargetLevel: request.TargetLevel, - LevelCosts: levelCosts, - TotalCost: totalCost, - CanAffordTotal: canCharacterAfford(character, totalCost), - } -} - // Helper structures and functions type skillInfo struct { Category string Difficulty string } -// GetSkillInfoCategoryAndDifficultyOld is deprecated. Use models.GetSkillCategoryAndDifficulty instead. -// This function uses the old hardcoded skill categorization system. -func GetSkillInfoCategoryAndDifficultyOld(skillName, skillType string) skillInfo { - var skill models.Skill - if err := skill.First(skillName); err != nil { - return skillInfo{Category: "unknown", Difficulty: "unknown"} - } - - // Fallback für fehlende Category und Difficulty Werte - category := skill.Category - difficulty := skill.Difficulty - - if category == "" { - // Standard-Kategorien basierend auf Skill-Namen - category = gsmaster.GetDefaultCategoryOld(skillName) - } - - if difficulty == "" { - // Standard-Schwierigkeit für verschiedene Skills - difficulty = gsmaster.GetDefaultDifficultyOld(skillName) - } - - return skillInfo{Category: category, Difficulty: difficulty} -} - func getSpellInfo(spellName string) skillInfo { var spell models.Spell if err := spell.First(spellName); err != nil { @@ -1041,18 +626,3 @@ func applyPPReduction(request *SkillCostRequest, cost *models.LearnCost, availab return finalEP, finalLE, reduction } - -func CalcSkillLearnCost(req *gsmaster.LernCostRequest, skillCostInfo *gsmaster.SkillCostResultNew) error { - // Fallback-Werte für Skills ohne definierte Kategorie/Schwierigkeit - - result, err := gsmaster.CalculateSkillLearningCostsOld(skillCostInfo.CharacterClass, skillCostInfo.Category, skillCostInfo.Difficulty) - if err != nil { - return err - } - - //Stufe: 0, // Lernen startet bei Stufe 0 - skillCostInfo.LE = result.LE - skillCostInfo.EP = result.EP - skillCostInfo.GoldCost = result.GoldCost - return nil -} diff --git a/backend/character/lerncost_handler_test.go b/backend/character/lerncost_handler_test.go index a964d01..ffccc12 100644 --- a/backend/character/lerncost_handler_test.go +++ b/backend/character/lerncost_handler_test.go @@ -15,169 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestImprovedSkillCostAPI(t *testing.T) { - // Setup test database - database.SetupTestDB() - defer database.ResetTestDB() - - // Migrate the schema - err := models.MigrateStructure() - assert.NoError(t, err) - - // Create test skill data - err = createTestSkillData() - assert.NoError(t, err) - defer cleanupTestSkillData() - - // Create test character - testChar := createChar() - testChar.ID = 1 // Set the ID to match our test requests - err = testChar.Create() - assert.NoError(t, err) - - // Setup Gin in test mode - gin.SetMode(gin.TestMode) - - // Create test cases - testCases := []struct { - name string - request SkillCostRequest - expectedStatus int - description string - }{ - { - name: "Learn new skill", - request: SkillCostRequest{ - Name: "Menschenkenntnis", - Type: "skill", - Action: "learn", - }, - expectedStatus: http.StatusOK, - description: "Should calculate costs for learning a new skill", - }, - { - name: "Improve existing skill", - request: SkillCostRequest{ - Name: "Menschenkenntnis", - Type: "skill", - Action: "improve", - CurrentLevel: 10, - }, - expectedStatus: http.StatusOK, - description: "Should calculate costs for improving an existing skill", - }, - { - name: "Multi-level improvement", - request: SkillCostRequest{ - Name: "Menschenkenntnis", - Type: "skill", - Action: "improve", - CurrentLevel: 10, - TargetLevel: 13, - }, - expectedStatus: http.StatusOK, - description: "Should calculate costs for multi-level improvement", - }, - { - name: "Invalid request - missing name", - request: SkillCostRequest{ - Type: "skill", - Action: "learn", - }, - expectedStatus: http.StatusBadRequest, - description: "Should return error for missing skill name", - }, - { - name: "Invalid request - invalid type", - request: SkillCostRequest{ - Name: "Test", - Type: "invalid", - Action: "learn", - }, - expectedStatus: http.StatusBadRequest, - description: "Should return error for invalid skill type", - }, - { - name: "Invalid request - invalid action", - request: SkillCostRequest{ - Name: "Test", - Type: "skill", - Action: "invalid", - }, - expectedStatus: http.StatusBadRequest, - description: "Should return error for invalid action", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - // Create request body - requestBody, _ := json.Marshal(tc.request) - - // Create HTTP request - req, _ := http.NewRequest("POST", "/api/characters/1/skill-cost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - // Create response recorder - w := httptest.NewRecorder() - - // Create Gin context - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "1"}} - - // Note: This test would need a proper database setup to work fully - // For now, we're just testing the request parsing and validation - - fmt.Printf("Test: %s\n", tc.description) - fmt.Printf("Request: %+v\n", tc.request) - fmt.Printf("Expected Status: %d\n", tc.expectedStatus) - - // Call the actual handler function - GetSkillCost(c) - - // Check the response status - assert.Equal(t, tc.expectedStatus, w.Code, "Status code should match expected") - - // If successful, validate response structure - if w.Code == http.StatusOK { - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - // Check for expected fields in successful responses - if tc.request.TargetLevel > 0 { - // Multi-level response - assert.Contains(t, response, "LevelCosts", "Multi-level response should contain LevelCosts") - } else { - // Single-level response - assert.Contains(t, response, "SkillName", "Response should contain SkillName") - assert.Contains(t, response, "SkillType", "Response should contain SkillType") - assert.Contains(t, response, "Action", "Response should contain Action") - } - } - - // Validate request structure - var parsedRequest SkillCostRequest - err := json.Unmarshal(requestBody, &parsedRequest) - assert.NoError(t, err, "Request should be valid JSON") - - // Test validation logic - if tc.request.Name == "" { - assert.Empty(t, parsedRequest.Name, "Name should be empty when not provided") - } - - if tc.request.Type != "" { - assert.Equal(t, tc.request.Type, parsedRequest.Type, "Type should match") - } - - if tc.request.Action != "" { - assert.Equal(t, tc.request.Action, parsedRequest.Action, "Action should match") - } - }) - } -} - // Test the response structures func TestSkillCostResponseStructures(t *testing.T) { t.Run("SkillCostResponse structure", func(t *testing.T) { @@ -254,645 +91,7 @@ func TestHelperFunctions(t *testing.T) { }) } -// Test integration with gsmaster exported functions -func TestGSMasterIntegration(t *testing.T) { - t.Run("GetDefaultCategory integration", func(t *testing.T) { - // Test that we can access the exported function from gsmaster - category := gsmaster.GetDefaultCategoryOld("Menschenkenntnis") - assert.Equal(t, "Sozial", category, "Should return correct category for Menschenkenntnis") - - category = gsmaster.GetDefaultCategoryOld("Stichwaffen") - assert.Equal(t, "Waffen", category, "Should return correct category for Stichwaffen") - - // Test fallback for unknown skill - category = gsmaster.GetDefaultCategoryOld("NonExistentSkill") - assert.Equal(t, "Alltag", category, "Should return default category for unknown skill") - }) - - t.Run("GetDefaultDifficulty integration", func(t *testing.T) { - // Test that we can access the exported function from gsmaster - difficulty := gsmaster.GetDefaultDifficultyOld("Menschenkenntnis") - assert.Equal(t, "schwer", difficulty, "Should return correct difficulty for Menschenkenntnis") - - difficulty = gsmaster.GetDefaultDifficultyOld("Stichwaffen") - assert.Equal(t, "leicht", difficulty, "Should return correct difficulty for Stichwaffen") - - // Test fallback for unknown skill - difficulty = gsmaster.GetDefaultDifficultyOld("NonExistentSkill") - assert.Equal(t, "normal", difficulty, "Should return default difficulty for unknown skill") - }) - - t.Run("Reward system structures", func(t *testing.T) { - // Test RewardOptions structure - rewards := RewardOptions{ - Type: "free_learning", - UseGoldForEP: true, - MaxGoldEP: 50, - } - - // Test JSON marshaling - jsonData, err := json.Marshal(rewards) - assert.NoError(t, err, "RewardOptions should be marshallable to JSON") - - // Test JSON unmarshaling - var parsedRewards RewardOptions - err = json.Unmarshal(jsonData, &parsedRewards) - assert.NoError(t, err, "RewardOptions should be unmarshallable from JSON") - - assert.Equal(t, rewards.Type, parsedRewards.Type, "Type should match") - assert.Equal(t, rewards.UseGoldForEP, parsedRewards.UseGoldForEP, "UseGoldForEP should match") - assert.Equal(t, rewards.MaxGoldEP, parsedRewards.MaxGoldEP, "MaxGoldEP should match") - - // Test validation of reward types - validTypes := []string{"free_learning", "free_spell_learning", "half_ep_improvement", "gold_for_ep"} - for _, validType := range validTypes { - rewards.Type = validType - _, err := json.Marshal(rewards) - assert.NoError(t, err, fmt.Sprintf("Should marshal valid type: %s", validType)) - } - }) - - t.Run("Reward system integration with gsmaster functions", func(t *testing.T) { - // Test that the reward system works with the exported gsmaster functions - // This simulates the flow where we get skill info from gsmaster and apply rewards - - skillName := "Menschenkenntnis" - - // Get skill info using exported functions - category := gsmaster.GetDefaultCategoryOld(skillName) - difficulty := gsmaster.GetDefaultDifficultyOld(skillName) - - assert.Equal(t, "Sozial", category, "Should get correct category from gsmaster") - assert.Equal(t, "schwer", difficulty, "Should get correct difficulty from gsmaster") - - // Test reward structure that would be used in the actual API - rewards := RewardOptions{ - Type: "half_ep_improvement", - UseGoldForEP: false, - MaxGoldEP: 0, - } - - // Test that structure is valid - jsonData, err := json.Marshal(rewards) - assert.NoError(t, err, "Reward options should marshal correctly") - - var parsedRewards RewardOptions - err = json.Unmarshal(jsonData, &parsedRewards) - assert.NoError(t, err, "Reward options should unmarshal correctly") - assert.Equal(t, "half_ep_improvement", parsedRewards.Type, "Reward type should be preserved") - }) -} - // Test GetLernCost endpoint specifically with gsmaster.LernCostRequest structure -func TestGetLernCostEndpoint(t *testing.T) { - // Setup test database - database.SetupTestDB(true, true) - defer database.ResetTestDB() - - // Migrate the schema - err := models.MigrateStructure() - assert.NoError(t, err) - - // Setup Gin in test mode - gin.SetMode(gin.TestMode) - - t.Run("GetLernCost with Athletik for Krieger character", func(t *testing.T) { - // Create request body using gsmaster.LernCostRequest structure - requestData := gsmaster.LernCostRequest{ - CharId: 20, // CharacterID = 20 - Name: "Athletik", // SkillName = Athletik - CurrentLevel: 9, // CurrentLevel = 9 - Type: "skill", // Type = skill - Action: "improve", // Action = improve (since we have current level) - TargetLevel: 0, // TargetLevel = 0 (will calculate up to level 18) - UsePP: 0, // No practice points used - UseGold: 0, - Reward: &[]string{"default"}[0], // Default reward type - } - requestBody, _ := json.Marshal(requestData) - - // Create HTTP request - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - // Create response recorder - w := httptest.NewRecorder() - - // Create Gin context - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "20"}} - - fmt.Printf("Test: GetLernCost for Athletik improvement for Krieger character ID 20\n") - fmt.Printf("Request: CharId=%d, SkillName=%s, CurrentLevel=%d, TargetLevel=%d\n", - requestData.CharId, requestData.Name, requestData.CurrentLevel, requestData.TargetLevel) - - // Call the actual handler function - GetLernCost(c) - - // Print the actual response to see what we get - fmt.Printf("Response Status: %d\n", w.Code) - fmt.Printf("Response Body: %s\n", w.Body.String()) - - // Check if we got an error response first - if w.Code != http.StatusOK { - var errorResponse map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &errorResponse) - if err == nil { - fmt.Printf("Error Response: %+v\n", errorResponse) - } - assert.Fail(t, "Expected successful response but got error: %s", w.Body.String()) - return - } - - // Parse and validate response for success case - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON array of SkillCostResultNew") - - // Should have costs for levels 10, 11, 12, ... up to 18 (from current level 9) - assert.Greater(t, len(response), 0, "Should return learning costs for multiple levels") - assert.LessOrEqual(t, len(response), 9, "Should not return more than 9 levels (10-18)") - - // Validate the first entry (level 10) - if len(response) > 0 { - firstResult := response[0] - assert.Equal(t, "20", firstResult.CharacterID, "Character ID should match") - assert.Equal(t, "Athletik", firstResult.SkillName, "Skill name should match") - assert.Equal(t, 10, firstResult.TargetLevel, "First target level should be 10") - - // Character class should be "Kr" (abbreviation for "Krieger") - assert.Equal(t, "Kr", firstResult.CharacterClass, "Character class should be abbreviated to 'Kr'") - - // Should have valid costs - assert.Greater(t, firstResult.EP, 0, "EP cost should be greater than 0") - assert.GreaterOrEqual(t, firstResult.GoldCost, 0, "Gold cost should be 0 or greater") - - fmt.Printf("Level 10 cost: EP=%d, GoldCost=%d, LE=%d\n", - firstResult.EP, firstResult.GoldCost, firstResult.LE) - fmt.Printf("Category=%s, Difficulty=%s\n", - firstResult.Category, firstResult.Difficulty) - } - - // Find cost for level 12 specifically to test mid-range - var level12Cost *gsmaster.SkillCostResultNew - for i := range response { - if response[i].TargetLevel == 12 { - level12Cost = &response[i] - break - } - } - - if level12Cost != nil { - assert.Equal(t, 12, level12Cost.TargetLevel, "Target level should be 12") - assert.Greater(t, level12Cost.EP, 0, "EP cost should be greater than 0 for level 12") - - fmt.Printf("Level 12 cost: EP=%d, GoldCost=%d, LE=%d\n", - level12Cost.EP, level12Cost.GoldCost, level12Cost.LE) - } else { - fmt.Printf("No cost found for level 12. Available levels: ") - for _, cost := range response { - fmt.Printf("%d ", cost.TargetLevel) - } - fmt.Println() - } - - // Verify all target levels are sequential and start from current level + 1 - expectedLevel := 10 // Current level 9 + 1 - for _, cost := range response { - assert.Equal(t, expectedLevel, cost.TargetLevel, - "Target levels should be sequential starting from %d", expectedLevel) - assert.Equal(t, "Athletik", cost.SkillName, "All entries should have correct skill name") - assert.Equal(t, "Kr", cost.CharacterClass, "All entries should have correct character class") - expectedLevel++ - } - }) - - t.Run("GetLernCost Athletik - Detailed Cost Analysis for Each Level", func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - UsePP: 0, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed") - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Detailed Cost Analysis for Athletik (Levels 10-18) ===\n") - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for _, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Validate each level's costs - assert.Greater(t, cost.EP, 0, "EP cost should be positive for level %d", cost.TargetLevel) - assert.GreaterOrEqual(t, cost.GoldCost, 0, "Gold cost should be non-negative for level %d", cost.TargetLevel) - assert.GreaterOrEqual(t, cost.LE, 0, "LE cost should be non-negative for level %d", cost.TargetLevel) - assert.Equal(t, 0, cost.PPUsed, "PP Used should be 0 when UsePP=0 for level %d", cost.TargetLevel) - assert.Equal(t, 0, cost.GoldUsed, "Gold Used should be 0 when UseGold=0 for level %d", cost.TargetLevel) - - // Verify cost progression (higher levels should generally cost more) - if cost.TargetLevel > 10 { - prevLevel := cost.TargetLevel - 1 - var prevCost *gsmaster.SkillCostResultNew - for i := range response { - if response[i].TargetLevel == prevLevel { - prevCost = &response[i] - break - } - } - if prevCost != nil { - assert.GreaterOrEqual(t, cost.EP, prevCost.EP, - "EP cost should not decrease from level %d to %d", prevLevel, cost.TargetLevel) - } - } - } - }) - - t.Run("GetLernCost Athletik - With Practice Points Usage", func(t *testing.T) { - testCases := []struct { - usePP int - description string - }{ - {5, "Using 5 Practice Points"}, - {10, "Using 10 Practice Points"}, - {20, "Using 20 Practice Points"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: tc.usePP, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed for UsePP=%d", tc.usePP) - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Cost Analysis with %d Practice Points ===\n", tc.usePP) - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for i, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Simple validation: PP should be reasonable and Gold should be 0 - assert.LessOrEqual(t, cost.PPUsed, 50, "PP Used should be reasonable for level %d", cost.TargetLevel) - assert.Equal(t, 0, cost.GoldUsed, "Gold Used should be 0 when UseGold=0 for level %d", cost.TargetLevel) - - // EP cost should be non-negative - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - - // When enough PP are available, early levels should have 0 EP cost - if i == 0 && tc.usePP >= 2 { - assert.Equal(t, 0, cost.EP, "Level 10 should have 0 EP cost when enough PP available") - } - - // EP cost validation - if cost.PPUsed > 0 { - // When PP are used, EP should be reduced or zero - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - } else { - // When no PP are used, EP should be positive - assert.Greater(t, cost.EP, 0, "EP cost should be positive when no PP used for level %d", cost.TargetLevel) - } - } - }) - } - }) - - t.Run("GetLernCost Athletik - With Gold Usage", func(t *testing.T) { - testCases := []struct { - useGold int - description string - }{ - {50, "Using 50 Gold"}, - {100, "Using 100 Gold"}, - {200, "Using 200 Gold"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - UseGold: tc.useGold, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed for UseGold=%d", tc.useGold) - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Cost Analysis with %d Gold ===\n", tc.useGold) - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for i, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Validate Gold usage based on EP needs and cumulative usage - remainingGold := tc.useGold - - // Calculate cumulative Gold usage for previous levels - for j := 0; j < i; j++ { - if j < len(response) { - remainingGold -= response[j].GoldUsed - } - } - - // Current level's expected Gold usage - epCostWithoutGold := cost.EP + (cost.GoldUsed / 10) // Reverse calculate original EP - maxGoldUsable := epCostWithoutGold * 10 // Max gold that can be used (10 gold = 1 EP) - - expectedGoldUsed := 0 - if remainingGold > 0 { - if remainingGold >= maxGoldUsable { - expectedGoldUsed = maxGoldUsable - } else { - expectedGoldUsed = remainingGold - } - } - - assert.Equal(t, expectedGoldUsed, cost.GoldUsed, "Gold Used should match calculated value for level %d (remaining Gold: %d, max usable: %d)", cost.TargetLevel, remainingGold, maxGoldUsable) - assert.Equal(t, 0, cost.PPUsed, "PP Used should be 0 when UsePP=0 for level %d", cost.TargetLevel) - - // EP cost validation - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - } - }) - } - }) - - t.Run("GetLernCost Athletik - Combined PP and Gold Usage", func(t *testing.T) { - testCases := []struct { - usePP int - useGold int - description string - }{ - {10, 50, "Using 10 PP and 50 Gold"}, - {15, 100, "Using 15 PP and 100 Gold"}, - {25, 200, "Using 25 PP and 200 Gold"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: tc.usePP, - UseGold: tc.useGold, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed for UsePP=%d, UseGold=%d", tc.usePP, tc.useGold) - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Cost Analysis with %d PP and %d Gold ===\n", tc.usePP, tc.useGold) - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for _, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Calculate original TE needed (LE + PP Used = original TE) - teNeeded := cost.LE + cost.PPUsed - - // Calculate original EP before gold usage (EP + Gold/10 = original EP) - epAfterPP := cost.EP + (cost.GoldUsed / 10) - maxGoldUsable := epAfterPP * 10 - - // Validate that resources are used reasonably - assert.LessOrEqual(t, cost.PPUsed, teNeeded, "PP Used should not exceed TE needed for level %d", cost.TargetLevel) - assert.LessOrEqual(t, cost.GoldUsed, maxGoldUsable, "Gold Used should not exceed max usable for level %d", cost.TargetLevel) - - // EP cost should be non-negative - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - } - }) - } - }) - - t.Run("GetLernCost Athletik - Cost Comparison Baseline vs Resources", func(t *testing.T) { - // First get baseline costs (no resources used) - baselineRequest := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - baselineBody, _ := json.Marshal(baselineRequest) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(baselineBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - var baselineResponse []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &baselineResponse) - assert.NoError(t, err, "Baseline response should be valid JSON") - - // Now get costs with resources - resourceRequest := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 15, - UseGold: 100, - Reward: &[]string{"default"}[0], - } - resourceBody, _ := json.Marshal(resourceRequest) - - req2, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(resourceBody)) - req2.Header.Set("Content-Type", "application/json") - - w2 := httptest.NewRecorder() - c2, _ := gin.CreateTestContext(w2) - c2.Request = req2 - - GetLernCost(c2) - - var resourceResponse []gsmaster.SkillCostResultNew - err = json.Unmarshal(w2.Body.Bytes(), &resourceResponse) - assert.NoError(t, err, "Resource response should be valid JSON") - - // Compare the results - fmt.Printf("\n=== Cost Comparison: Baseline vs Using Resources ===\n") - fmt.Printf("Level | Baseline EP | Resource EP | EP Saved | PP Used | Gold Used\n") - fmt.Printf("------|-------------|-------------|----------|---------|----------\n") - - assert.Equal(t, len(baselineResponse), len(resourceResponse), "Both responses should have same number of levels") - - for i, baseline := range baselineResponse { - if i < len(resourceResponse) { - resource := resourceResponse[i] - assert.Equal(t, baseline.TargetLevel, resource.TargetLevel, "Target levels should match") - - epSaved := baseline.EP - resource.EP - fmt.Printf("%5d | %11d | %11d | %8d | %7d | %9d\n", - baseline.TargetLevel, baseline.EP, resource.EP, epSaved, resource.PPUsed, resource.GoldUsed) - - // Validate that using resources reduces EP cost (or at least doesn't increase it) - assert.LessOrEqual(t, resource.EP, baseline.EP, - "EP cost should not increase when using resources for level %d", baseline.TargetLevel) - } - } - }) - - t.Run("GetLernCost with invalid character ID", func(t *testing.T) { - // Test with non-existent character ID - requestData := gsmaster.LernCostRequest{ - CharId: 999, // Non-existent character - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/999/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "999"}} - - GetLernCost(c) - - // Should return 404 Not Found - assert.Equal(t, http.StatusNotFound, w.Code, "Status code should be 404 Not Found") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - assert.Contains(t, response, "error", "Response should contain error message") - - fmt.Printf("Error case - Invalid character ID: %s\n", response["error"]) - }) - - t.Run("GetLernCost with invalid request structure", func(t *testing.T) { - // Test with missing required fields - requestData := map[string]interface{}{ - "char_id": "invalid", // Invalid type - should be uint - "name": "", // Empty name - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/20/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "20"}} - - GetLernCost(c) - - // Should return 400 Bad Request - assert.Equal(t, http.StatusBadRequest, w.Code, "Status code should be 400 Bad Request") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - assert.Contains(t, response, "error", "Response should contain error message") - - fmt.Printf("Error case - Invalid request: %s\n", response["error"]) - }) -} // Test GetLernCost endpoint specifically with gsmaster.LernCostRequest structure func TestGetLernCostEndpointNewSystem(t *testing.T) { @@ -1021,428 +220,4 @@ func TestGetLernCostEndpointNewSystem(t *testing.T) { } }) - t.Run("GetLernCost Athletik - Detailed Cost Analysis for Each Level", func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, // Calculate all levels - UsePP: 0, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed") - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Detailed Cost Analysis for Athletik (Levels 10-18) ===\n") - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for _, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Validate each level's costs - assert.Greater(t, cost.EP, 0, "EP cost should be positive for level %d", cost.TargetLevel) - assert.GreaterOrEqual(t, cost.GoldCost, 0, "Gold cost should be non-negative for level %d", cost.TargetLevel) - assert.GreaterOrEqual(t, cost.LE, 0, "LE cost should be non-negative for level %d", cost.TargetLevel) - assert.Equal(t, 0, cost.PPUsed, "PP Used should be 0 when UsePP=0 for level %d", cost.TargetLevel) - assert.Equal(t, 0, cost.GoldUsed, "Gold Used should be 0 when UseGold=0 for level %d", cost.TargetLevel) - - // Verify cost progression (higher levels should generally cost more) - if cost.TargetLevel > 10 { - prevLevel := cost.TargetLevel - 1 - var prevCost *gsmaster.SkillCostResultNew - for i := range response { - if response[i].TargetLevel == prevLevel { - prevCost = &response[i] - break - } - } - if prevCost != nil { - assert.GreaterOrEqual(t, cost.EP, prevCost.EP, - "EP cost should not decrease from level %d to %d", prevLevel, cost.TargetLevel) - } - } - } - }) - - t.Run("GetLernCost Athletik - With Practice Points Usage", func(t *testing.T) { - testCases := []struct { - usePP int - description string - }{ - {5, "Using 5 Practice Points"}, - {10, "Using 10 Practice Points"}, - {20, "Using 20 Practice Points"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: tc.usePP, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed for UsePP=%d", tc.usePP) - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Cost Analysis with %d Practice Points ===\n", tc.usePP) - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for i, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Simple validation: PP should be reasonable and Gold should be 0 - assert.LessOrEqual(t, cost.PPUsed, 50, "PP Used should be reasonable for level %d", cost.TargetLevel) - assert.Equal(t, 0, cost.GoldUsed, "Gold Used should be 0 when UseGold=0 for level %d", cost.TargetLevel) - - // EP cost should be non-negative - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - - // When enough PP are available, early levels should have 0 EP cost - if i == 0 && tc.usePP >= 2 { - assert.Equal(t, 0, cost.EP, "Level 10 should have 0 EP cost when enough PP available") - } - - // EP cost validation - if cost.PPUsed > 0 { - // When PP are used, EP should be reduced or zero - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - } else { - // When no PP are used, EP should be positive - assert.Greater(t, cost.EP, 0, "EP cost should be positive when no PP used for level %d", cost.TargetLevel) - } - } - }) - } - }) - - t.Run("GetLernCost Athletik - With Gold Usage", func(t *testing.T) { - testCases := []struct { - useGold int - description string - }{ - {50, "Using 50 Gold"}, - {100, "Using 100 Gold"}, - {200, "Using 200 Gold"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - UseGold: tc.useGold, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed for UseGold=%d", tc.useGold) - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Cost Analysis with %d Gold ===\n", tc.useGold) - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for i, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Validate Gold usage based on EP needs and cumulative usage - remainingGold := tc.useGold - - // Calculate cumulative Gold usage for previous levels - for j := 0; j < i; j++ { - if j < len(response) { - remainingGold -= response[j].GoldUsed - } - } - - // Current level's expected Gold usage - epCostWithoutGold := cost.EP + (cost.GoldUsed / 10) // Reverse calculate original EP - maxGoldUsable := epCostWithoutGold * 10 // Max gold that can be used (10 gold = 1 EP) - - expectedGoldUsed := 0 - if remainingGold > 0 { - if remainingGold >= maxGoldUsable { - expectedGoldUsed = maxGoldUsable - } else { - expectedGoldUsed = remainingGold - } - } - - assert.Equal(t, expectedGoldUsed, cost.GoldUsed, "Gold Used should match calculated value for level %d (remaining Gold: %d, max usable: %d)", cost.TargetLevel, remainingGold, maxGoldUsable) - assert.Equal(t, 0, cost.PPUsed, "PP Used should be 0 when UsePP=0 for level %d", cost.TargetLevel) - - // EP cost validation - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - } - }) - } - }) - - t.Run("GetLernCost Athletik - Combined PP and Gold Usage", func(t *testing.T) { - testCases := []struct { - usePP int - useGold int - description string - }{ - {10, 50, "Using 10 PP and 50 Gold"}, - {15, 100, "Using 15 PP and 100 Gold"}, - {25, 200, "Using 25 PP and 200 Gold"}, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - requestData := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: tc.usePP, - UseGold: tc.useGold, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - assert.Equal(t, http.StatusOK, w.Code, "Request should succeed for UsePP=%d, UseGold=%d", tc.usePP, tc.useGold) - - var response []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - fmt.Printf("\n=== Cost Analysis with %d PP and %d Gold ===\n", tc.usePP, tc.useGold) - fmt.Printf("Level | EP Cost | Gold Cost | LE Cost | PP Used | Gold Used\n") - fmt.Printf("------|---------|-----------|---------|---------|----------\n") - - for _, cost := range response { - fmt.Printf("%5d | %7d | %9d | %7d | %7d | %9d\n", - cost.TargetLevel, cost.EP, cost.GoldCost, cost.LE, cost.PPUsed, cost.GoldUsed) - - // Calculate original TE needed (LE + PP Used = original TE) - teNeeded := cost.LE + cost.PPUsed - - // Calculate original EP before gold usage (EP + Gold/10 = original EP) - epAfterPP := cost.EP + (cost.GoldUsed / 10) - maxGoldUsable := epAfterPP * 10 - - // Validate that resources are used reasonably - assert.LessOrEqual(t, cost.PPUsed, teNeeded, "PP Used should not exceed TE needed for level %d", cost.TargetLevel) - assert.LessOrEqual(t, cost.GoldUsed, maxGoldUsable, "Gold Used should not exceed max usable for level %d", cost.TargetLevel) - - // EP cost should be non-negative - assert.GreaterOrEqual(t, cost.EP, 0, "EP cost should be non-negative for level %d", cost.TargetLevel) - } - }) - } - }) - - t.Run("GetLernCost Athletik - Cost Comparison Baseline vs Resources", func(t *testing.T) { - // First get baseline costs (no resources used) - baselineRequest := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - UseGold: 0, - Reward: &[]string{"default"}[0], - } - baselineBody, _ := json.Marshal(baselineRequest) - - req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(baselineBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - - GetLernCost(c) - - var baselineResponse []gsmaster.SkillCostResultNew - err := json.Unmarshal(w.Body.Bytes(), &baselineResponse) - assert.NoError(t, err, "Baseline response should be valid JSON") - - // Now get costs with resources - resourceRequest := gsmaster.LernCostRequest{ - CharId: 20, - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 15, - UseGold: 100, - Reward: &[]string{"default"}[0], - } - resourceBody, _ := json.Marshal(resourceRequest) - - req2, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(resourceBody)) - req2.Header.Set("Content-Type", "application/json") - - w2 := httptest.NewRecorder() - c2, _ := gin.CreateTestContext(w2) - c2.Request = req2 - - GetLernCost(c2) - - var resourceResponse []gsmaster.SkillCostResultNew - err = json.Unmarshal(w2.Body.Bytes(), &resourceResponse) - assert.NoError(t, err, "Resource response should be valid JSON") - - // Compare the results - fmt.Printf("\n=== Cost Comparison: Baseline vs Using Resources ===\n") - fmt.Printf("Level | Baseline EP | Resource EP | EP Saved | PP Used | Gold Used\n") - fmt.Printf("------|-------------|-------------|----------|---------|----------\n") - - assert.Equal(t, len(baselineResponse), len(resourceResponse), "Both responses should have same number of levels") - - for i, baseline := range baselineResponse { - if i < len(resourceResponse) { - resource := resourceResponse[i] - assert.Equal(t, baseline.TargetLevel, resource.TargetLevel, "Target levels should match") - - epSaved := baseline.EP - resource.EP - fmt.Printf("%5d | %11d | %11d | %8d | %7d | %9d\n", - baseline.TargetLevel, baseline.EP, resource.EP, epSaved, resource.PPUsed, resource.GoldUsed) - - // Validate that using resources reduces EP cost (or at least doesn't increase it) - assert.LessOrEqual(t, resource.EP, baseline.EP, - "EP cost should not increase when using resources for level %d", baseline.TargetLevel) - } - } - }) - - t.Run("GetLernCost with invalid character ID", func(t *testing.T) { - // Test with non-existent character ID - requestData := gsmaster.LernCostRequest{ - CharId: 999, // Non-existent character - Name: "Athletik", - CurrentLevel: 9, - Type: "skill", - Action: "improve", - TargetLevel: 0, - UsePP: 0, - Reward: &[]string{"default"}[0], - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/999/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "999"}} - - GetLernCost(c) - - // Should return 404 Not Found - assert.Equal(t, http.StatusNotFound, w.Code, "Status code should be 404 Not Found") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - assert.Contains(t, response, "error", "Response should contain error message") - - fmt.Printf("Error case - Invalid character ID: %s\n", response["error"]) - }) - - t.Run("GetLernCost with invalid request structure", func(t *testing.T) { - // Test with missing required fields - requestData := map[string]interface{}{ - "char_id": "invalid", // Invalid type - should be uint - "name": "", // Empty name - } - requestBody, _ := json.Marshal(requestData) - - req, _ := http.NewRequest("POST", "/api/characters/20/lerncost", bytes.NewBuffer(requestBody)) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "20"}} - - GetLernCost(c) - - // Should return 400 Bad Request - assert.Equal(t, http.StatusBadRequest, w.Code, "Status code should be 400 Bad Request") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - assert.Contains(t, response, "error", "Response should contain error message") - - fmt.Printf("Error case - Invalid request: %s\n", response["error"]) - }) } diff --git a/backend/character/routes.go b/backend/character/routes.go index 0641e46..2894ba2 100644 --- a/backend/character/routes.go +++ b/backend/character/routes.go @@ -28,12 +28,12 @@ func RegisterRoutes(r *gin.RouterGroup) { // Lernen und Verbessern (mit automatischem Audit-Log) charGrp.POST("/:id/learn-skill-new", LearnSkill) // Fertigkeit lernen (neues System) - charGrp.POST("/:id/learn-skill", LearnSkillOld) // Fertigkeit lernen (altes System) + //charGrp.POST("/:id/learn-skill", LearnSkillOld) // Fertigkeit lernen (altes System) charGrp.POST("/:id/learn-spell-new", LearnSpell) // Zauber lernen (neues System) - charGrp.POST("/:id/learn-spell", LearnSpellOld) // Zauber lernen (altes System) + //charGrp.POST("/:id/learn-spell", LearnSpellOld) // Zauber lernen (altes System) // Fertigkeiten-Information - charGrp.GET("/:id/available-skills", GetAvailableSkillsOld) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen) + //charGrp.GET("/:id/available-skills", GetAvailableSkillsOld) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen) charGrp.POST("/available-skills-new", GetAvailableSkillsNewSystem) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen) charGrp.POST("/available-skills-creation", GetAvailableSkillsForCreation) // Verfügbare Fertigkeiten mit Lernkosten für Charaktererstellung charGrp.POST("/available-spells-creation", GetAvailableSpellsForCreation) // Verfügbare Zauber mit Lernkosten für Charaktererstellung @@ -41,7 +41,7 @@ func RegisterRoutes(r *gin.RouterGroup) { charGrp.GET("/spell-details", GetSpellDetails) // Detaillierte Informationen zu einem bestimmten Zauber // Belohnungsarten für verschiedene Lernszenarien - charGrp.GET("/:id/reward-types", GetRewardTypesOld) // Verfügbare Belohnungsarten je nach Kontext + charGrp.GET("/:id/reward-types", GetRewardTypesStatic) // Verfügbare Belohnungsarten je nach Kontext // Praxispunkte-Verwaltung charGrp.GET("/:id/practice-points", GetPracticePoints) // NewSystem @@ -50,8 +50,8 @@ func RegisterRoutes(r *gin.RouterGroup) { charGrp.POST("/:id/practice-points/use", UsePracticePoint) // NewSystem // System-Information - charGrp.GET("/character-classes", GetCharacterClassesHandlerOld) - charGrp.GET("/skill-categories", GetSkillCategoriesHandlerOld) + //charGrp.GET("/character-classes", GetCharacterClassesHandlerOld) + charGrp.GET("/skill-categories", GetSkillCategoriesHandlerStatic) // Character Creation charGrp.GET("/create-sessions", ListCharacterSessions) // Aktive Sessions für Benutzer auflisten diff --git a/backend/character/skill_api_test.go b/backend/character/skill_api_test.go deleted file mode 100644 index e56c9be..0000000 --- a/backend/character/skill_api_test.go +++ /dev/null @@ -1,259 +0,0 @@ -package character - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "bamort/database" - "bamort/models" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - -func TestGetAvailableSkills(t *testing.T) { - // Setup test database with real data - database.SetupTestDB(true, true) - defer database.ResetTestDB() - - // Setup Gin in test mode - gin.SetMode(gin.TestMode) - - t.Run("Get available skills for existing character - default reward type", func(t *testing.T) { - // Get a character ID from the test data - var testChar models.Char - err := database.DB.Preload("Fertigkeiten").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&testChar).Error - assert.NoError(t, err, "Should find a test character") - - // Gib dem Charakter genug EP und Gold für Tests - testChar.Erfahrungsschatz.EP = 1000 - testChar.Vermoegen.Goldstücke = 1000 - database.DB.Save(&testChar.Erfahrungsschatz) - database.DB.Save(&testChar.Vermoegen) - - characterID := fmt.Sprintf("%d", testChar.ID) - - // Create HTTP request with default reward type - req, _ := http.NewRequest("GET", fmt.Sprintf("/api/characters/%s/available-skills?reward_type=default", characterID), nil) - req.Header.Set("Content-Type", "application/json") - - // Create response recorder - w := httptest.NewRecorder() - - // Create Gin context - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: characterID}} - - fmt.Printf("Test: Get available skills for character ID %s (default reward)\n", characterID) - fmt.Printf("Character EP: %d, Gold: %d\n", testChar.Erfahrungsschatz.EP, testChar.Vermoegen.Goldstücke) - - // Call the handler function - GetAvailableSkillsOld(c) - - // Print the response for debugging - fmt.Printf("Response Status: %d\n", w.Code) - fmt.Printf("Response Body: %s\n", w.Body.String()) - - // Assert response status - assert.Equal(t, http.StatusOK, w.Code, "Status code should be 200 OK") - - // Parse response - var response map[string]interface{} - err = json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - // Check response structure - assert.Contains(t, response, "skills_by_category", "Response should contain skills_by_category field") - - // Check if skills_by_category is an object - skillsByCategory, ok := response["skills_by_category"].(map[string]interface{}) - assert.True(t, ok, "skills_by_category should be an object") - - fmt.Printf("Found %d skill categories\n", len(skillsByCategory)) - - // Check each category - totalSkills := 0 - for categoryName, categorySkills := range skillsByCategory { - fmt.Printf("Category: %s\n", categoryName) - - skills, ok := categorySkills.([]interface{}) - assert.True(t, ok, fmt.Sprintf("Category %s should contain an array of skills", categoryName)) - - totalSkills += len(skills) - fmt.Printf(" Skills in category: %d\n", len(skills)) - - // Check structure of first skill in category if exists - if len(skills) > 0 { - firstSkill, ok := skills[0].(map[string]interface{}) - assert.True(t, ok, "First skill should be an object") - - // Check required fields - assert.Contains(t, firstSkill, "name", "Skill should have name field") - assert.Contains(t, firstSkill, "epCost", "Skill should have epCost field") - assert.Contains(t, firstSkill, "goldCost", "Skill should have goldCost field") - - skillName, ok := firstSkill["name"].(string) - assert.True(t, ok, "Skill name should be a string") - assert.NotEmpty(t, skillName, "Skill name should not be empty") - - epCost, ok := firstSkill["epCost"].(float64) - assert.True(t, ok, "EP cost should be a number") - assert.GreaterOrEqual(t, epCost, float64(0), "EP cost should be non-negative") - - goldCost, ok := firstSkill["goldCost"].(float64) - assert.True(t, ok, "Gold cost should be a number") - assert.GreaterOrEqual(t, goldCost, float64(0), "Gold cost should be non-negative") - - fmt.Printf(" First skill: %s (EP: %.0f, Gold: %.0f)\n", - skillName, epCost, goldCost) - } - } - - fmt.Printf("Total available skills: %d\n", totalSkills) - }) - - t.Run("Get available skills for existing character - noGold reward type", func(t *testing.T) { - // Get a character ID from the test data - var testChar models.Char - err := database.DB.Preload("Fertigkeiten").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&testChar).Error - assert.NoError(t, err, "Should find a test character") - - // Gib dem Charakter genug EP und Gold für Tests - testChar.Erfahrungsschatz.EP = 1000 - testChar.Vermoegen.Goldstücke = 1000 - database.DB.Save(&testChar.Erfahrungsschatz) - database.DB.Save(&testChar.Vermoegen) - - characterID := fmt.Sprintf("%d", testChar.ID) - - // Create HTTP request with noGold reward type - req, _ := http.NewRequest("GET", fmt.Sprintf("/api/characters/%s/available-skills?reward_type=noGold", characterID), nil) - req.Header.Set("Content-Type", "application/json") - - // Create response recorder - w := httptest.NewRecorder() - - // Create Gin context - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: characterID}} - - fmt.Printf("Test: Get available skills for character ID %s (noGold reward)\n", characterID) - - // Call the handler function - GetAvailableSkillsOld(c) - - // Print the response for debugging - fmt.Printf("Response Status: %d\n", w.Code) - fmt.Printf("Response Body: %s\n", w.Body.String()) - - // Assert response status - assert.Equal(t, http.StatusOK, w.Code, "Status code should be 200 OK") - - // Parse response - var response map[string]interface{} - err = json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - // Check response structure - assert.Contains(t, response, "skills_by_category", "Response should contain skills_by_category field") - - skillsByCategory, ok := response["skills_by_category"].(map[string]interface{}) - assert.True(t, ok, "skills_by_category should be an object") - - // Check that skills have goldCost = 0 for noGold reward type - for categoryName, categorySkills := range skillsByCategory { - skills, ok := categorySkills.([]interface{}) - assert.True(t, ok, fmt.Sprintf("Category %s should contain an array of skills", categoryName)) - - if len(skills) > 0 { - firstSkill, ok := skills[0].(map[string]interface{}) - assert.True(t, ok, "First skill should be an object") - - goldCost, ok := firstSkill["goldCost"].(float64) - assert.True(t, ok, "Gold cost should be a number") - assert.Equal(t, float64(0), goldCost, "Gold cost should be 0 for noGold reward type") - - fmt.Printf("Category %s - First skill gold cost: %.0f (should be 0)\n", categoryName, goldCost) - } - } - }) - - t.Run("Error case - character not found", func(t *testing.T) { - // Test with non-existent character ID - req, _ := http.NewRequest("GET", "/api/characters/99999/available-skills", nil) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: "99999"}} - - GetAvailableSkillsOld(c) - - // Should return 404 - assert.Equal(t, http.StatusNotFound, w.Code, "Status code should be 404 Not Found") - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - assert.Contains(t, response, "error", "Response should contain error message") - }) - - t.Run("Check that learned skills are excluded", func(t *testing.T) { - // Get a character with some skills - var testChar models.Char - err := database.DB.Preload("Fertigkeiten").First(&testChar).Error - assert.NoError(t, err, "Should find a test character") - - characterID := fmt.Sprintf("%d", testChar.ID) - - // Get list of learned skills - learnedSkillNames := make(map[string]bool) - for _, skill := range testChar.Fertigkeiten { - learnedSkillNames[skill.Name] = true - } - - // Make request - req, _ := http.NewRequest("GET", fmt.Sprintf("/api/characters/%s/available-skills", characterID), nil) - w := httptest.NewRecorder() - c, _ := gin.CreateTestContext(w) - c.Request = req - c.Params = []gin.Param{{Key: "id", Value: characterID}} - - GetAvailableSkillsOld(c) - - assert.Equal(t, http.StatusOK, w.Code, "Status code should be 200 OK") - - var response map[string]interface{} - err = json.Unmarshal(w.Body.Bytes(), &response) - assert.NoError(t, err, "Response should be valid JSON") - - skillsByCategory, ok := response["skills_by_category"].(map[string]interface{}) - assert.True(t, ok, "skills_by_category should be an object") - - // Check that no learned skills appear in available skills - for categoryName, categorySkills := range skillsByCategory { - skills, ok := categorySkills.([]interface{}) - assert.True(t, ok, fmt.Sprintf("Category %s should contain an array of skills", categoryName)) - - for _, skillInterface := range skills { - skill, ok := skillInterface.(map[string]interface{}) - assert.True(t, ok, "Skill should be an object") - - skillName, ok := skill["name"].(string) - assert.True(t, ok, "Skill name should be a string") - - // Assert that this skill is not in the learned skills list - assert.False(t, learnedSkillNames[skillName], - fmt.Sprintf("Learned skill '%s' should not appear in available skills", skillName)) - } - } - - fmt.Printf("Verified that %d learned skills are excluded from available skills\n", len(testChar.Fertigkeiten)) - }) -} diff --git a/backend/character/skill_learning_dialog_test.go b/backend/character/skill_learning_dialog_test.go deleted file mode 100644 index 8b30273..0000000 --- a/backend/character/skill_learning_dialog_test.go +++ /dev/null @@ -1,296 +0,0 @@ -package character - -import ( - "bamort/gsmaster" - "bamort/models" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestSkillLearningDialogWorkflow testet den kompletten Workflow, den das SkillLearningDialog verwendet -func TestSkillLearningDialogWorkflow(t *testing.T) { - // Setup - gin.SetMode(gin.TestMode) - - t.Run("Reward Types API - entspricht Frontend loadRewardTypes()", func(t *testing.T) { - // Test verschiedene Lerntypen wie sie das Frontend sendet - testCases := []struct { - learningType string - skillName string - skillType string - expectedLen int - }{ - {"improve", "Menschenkenntnis", "skill", 4}, // EP, Gold, PP, Mixed - {"learn", "Klettern", "skill", 2}, // EP, Gold - //{"spell", "Licht", "spell", 4}, // EP, Gold, PP, Mixed - } - - for _, tc := range testCases { - t.Run(tc.learningType+"_"+tc.skillName, func(t *testing.T) { - router := gin.New() - router.GET("/api/characters/:id/reward-types", GetRewardTypesOld) - - url := "/api/characters/1/reward-types?learning_type=" + tc.learningType + - "&skill_name=" + tc.skillName + "&skill_type=" + tc.skillType - - req, _ := http.NewRequest("GET", url, nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - require.NoError(t, err) - - // Überprüfe Response-Struktur wie sie das Frontend erwartet - assert.Contains(t, response, "reward_types") - assert.Contains(t, response, "learning_type") - assert.Contains(t, response, "skill_name") - assert.Contains(t, response, "character_id") - - rewardTypes := response["reward_types"].([]interface{}) - assert.GreaterOrEqual(t, len(rewardTypes), tc.expectedLen-1) // Mindestens erwartete Anzahl - - // Überprüfe Struktur der Reward Types wie Frontend sie erwartet - if len(rewardTypes) > 0 { - firstReward := rewardTypes[0].(map[string]interface{}) - assert.Contains(t, firstReward, "value") - assert.Contains(t, firstReward, "label") - } - }) - } - }) - - t.Run("Skill All Level Costs API - entspricht Frontend loadLearningCosts()", func(t *testing.T) { - // Teste nur die Request-Struktur und Skip den DB-Teil - t.Skip("Skipping DB-dependent test - testing request structure only") - - // Test der Request-Struktur wie das Frontend sie sendet - requestData := LearnRequestStruct{ - SkillType: "skill", - Name: "Menschenkenntnis", - Stufe: 10, - } - - // Überprüfe, dass die Request-Struktur korrekt ist - requestBody, err := json.Marshal(requestData) - require.NoError(t, err) - - var parsedRequest LearnRequestStruct - err = json.Unmarshal(requestBody, &parsedRequest) - require.NoError(t, err) - - // Überprüfe, dass alle Felder korrekt geparst werden - assert.Equal(t, "skill", parsedRequest.SkillType) - assert.Equal(t, "Menschenkenntnis", parsedRequest.Name) - assert.Equal(t, 10, parsedRequest.Stufe) - - // Teste erwartete Response-Struktur (ohne DB-Aufruf) - expectedResponse := []models.LearnCost{ - {Stufe: 11, Ep: 120, Money: 60, LE: 2}, - {Stufe: 12, Ep: 140, Money: 70, LE: 2}, - } - - // Überprüfe Response-Format wie das Frontend es erwartet - responseBody, _ := json.Marshal(expectedResponse) - var response []models.LearnCost - err = json.Unmarshal(responseBody, &response) - require.NoError(t, err) - - if len(response) > 0 { - cost := response[0] - assert.NotZero(t, cost.Stufe) // target_level im Frontend - assert.NotZero(t, cost.Ep) // ep_cost im Frontend - assert.NotZero(t, cost.Money) // gold_cost im Frontend - // cost.LE wird als pp_cost verwendet - } - }) - - t.Run("Frontend Conversion Logic Test", func(t *testing.T) { - // Simuliere die Konvertierung wie sie das Frontend durchführt - mockApiResponse := []models.LearnCost{ - {Stufe: 11, Ep: 120, Money: 60, LE: 2}, - {Stufe: 12, Ep: 140, Money: 70, LE: 2}, - {Stufe: 13, Ep: 160, Money: 80, LE: 3}, - } - - // Simuliere Frontend-Konvertierung - availableEP := 1000 - availableGold := 500 - availablePP := 10 - - var convertedLevels []map[string]interface{} - cumulativeEP := 0 - cumulativeGold := 0 - cumulativePP := 0 - - for _, cost := range mockApiResponse { - cumulativeEP += cost.Ep - cumulativeGold += cost.Money - cumulativePP += cost.LE - - level := map[string]interface{}{ - "targetLevel": cost.Stufe, - "epCost": cost.Ep, - "goldCost": cost.Money, - "ppCost": cost.LE, - "totalEpCost": cumulativeEP, - "totalGoldCost": cumulativeGold, - "totalPpCost": cumulativePP, - "canAfford": map[string]bool{ - "ep": availableEP >= cumulativeEP, - "gold": availableGold >= cumulativeGold, - "pp": availablePP >= cumulativePP, - }, - } - convertedLevels = append(convertedLevels, level) - } - - // Überprüfe Konvertierung - assert.Len(t, convertedLevels, 3) - - firstLevel := convertedLevels[0] - assert.Equal(t, 11, firstLevel["targetLevel"]) - assert.Equal(t, 120, firstLevel["epCost"]) - assert.Equal(t, 120, firstLevel["totalEpCost"]) // Erste Stufe = einzelne Kosten - - lastLevel := convertedLevels[2] - assert.Equal(t, 420, lastLevel["totalEpCost"]) // 120 + 140 + 160 - assert.Equal(t, 210, lastLevel["totalGoldCost"]) // 60 + 70 + 80 - - // Verfügbarkeits-Test - canAfford := lastLevel["canAfford"].(map[string]bool) - assert.True(t, canAfford["ep"]) // 1000 >= 420 - assert.True(t, canAfford["gold"]) // 500 >= 210 - assert.True(t, canAfford["pp"]) // 10 >= 7 - }) - - t.Run("Skill Learning Execution Test", func(t *testing.T) { - // Test der Request-Struktur für Lernausführung - // (DB-Operationen werden übersprungen) - t.Skip("Skipping DB-dependent test - testing request structure only") - - // Request wie das Frontend ihn für executeDetailedLearning sendet - requestData := gsmaster.LernCostRequest{ - Name: "Menschenkenntnis", - CurrentLevel: 10, - Type: "skill", - Action: "improve", - } - - // Verify the request structure is correct - assert.Equal(t, "Menschenkenntnis", requestData.Name) - assert.Equal(t, 10, requestData.CurrentLevel) - assert.Equal(t, "skill", requestData.Type) - assert.Equal(t, "improve", requestData.Action) - - // Das Frontend erwartet nach erfolgreichem Lernen diese Response-Felder: - expectedResponseFields := []string{"message", "skill_name", "ep_cost", "remaining_ep"} - assert.NotEmpty(t, expectedResponseFields, "Response sollte diese Felder enthalten") - }) -} - -// Test für die Frontend-Authentifizierung -func TestSkillLearningDialogAuth(t *testing.T) { - t.Run("Authentication Error Handling", func(t *testing.T) { - // Test wie das Frontend mit Auth-Fehlern umgeht - router := gin.New() - - // Mock eines Auth-Middlewares der 401 zurückgibt - router.Use(func(c *gin.Context) { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) - c.Abort() - }) - - router.GET("/api/characters/:id/reward-types", GetRewardTypesOld) - - req, _ := http.NewRequest("GET", "/api/characters/1/reward-types", nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusUnauthorized, w.Code) - - // Das Frontend sollte bei 401 das 'auth-error' Event emittieren - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - require.NoError(t, err) - assert.Contains(t, response, "error") - }) -} - -// Test verschiedener Belohnungstypen -func TestRewardTypeVariations(t *testing.T) { - testCases := []struct { - name string - learningType string - skillType string - expectedRewards []string - }{ - { - name: "Improve Skill", - learningType: "improve", - skillType: "skill", - expectedRewards: []string{"ep", "gold", "pp", "mixed"}, - }, - { - name: "Learn Skill", - learningType: "learn", - skillType: "skill", - expectedRewards: []string{"ep", "gold"}, - }, - { - name: "Weapon Training", - learningType: "improve", - skillType: "weapon", - expectedRewards: []string{"ep", "gold", "pp", "mixed", "training"}, - }, - { - name: "Spell Learning", - learningType: "spell", - skillType: "spell", - expectedRewards: []string{"ep", "gold", "pp", "mixed"}, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - router := gin.New() - router.GET("/api/characters/:id/reward-types", GetRewardTypesOld) - - url := "/api/characters/1/reward-types?learning_type=" + tc.learningType + - "&skill_type=" + tc.skillType + "&skill_name=TestSkill" - - req, _ := http.NewRequest("GET", url, nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, req) - - assert.Equal(t, http.StatusOK, w.Code) - - var response map[string]interface{} - err := json.Unmarshal(w.Body.Bytes(), &response) - require.NoError(t, err) - - rewardTypes := response["reward_types"].([]interface{}) - - // Überprüfe, dass erwartete Reward Types vorhanden sind - foundRewards := make(map[string]bool) - for _, reward := range rewardTypes { - rewardMap := reward.(map[string]interface{}) - value := rewardMap["value"].(string) - foundRewards[value] = true - } - - for _, expectedReward := range tc.expectedRewards { - assert.True(t, foundRewards[expectedReward], - "Erwartete Belohnungsart '%s' nicht gefunden für %s", expectedReward, tc.name) - } - }) - } -} diff --git a/backend/character/skill_update_test.go b/backend/character/skill_update_test.go index 4062f0d..5f85848 100644 --- a/backend/character/skill_update_test.go +++ b/backend/character/skill_update_test.go @@ -1,19 +1,6 @@ package character -import ( - "bytes" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - "bamort/database" - "bamort/models" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" -) - +/* func TestImproveSkillUpdatesLevel(t *testing.T) { // Setup test database database.SetupTestDB() @@ -204,3 +191,4 @@ func TestImproveSkillUpdatesLevel(t *testing.T) { t.Logf("New skill 'Schwimmen' successfully created at level 1") }) } +*/ diff --git a/backend/character/system_information_handlers.go b/backend/character/system_information_handlers.go index de7db5a..7688f43 100644 --- a/backend/character/system_information_handlers.go +++ b/backend/character/system_information_handlers.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" ) +/* // GetCharacterClassesHandlerOld gibt alle verfügbaren Charakterklassen zurück func GetCharacterClassesHandlerOld(c *gin.Context) { // Vereinfachte Antwort mit den drei Hauptklassen @@ -29,9 +30,9 @@ func GetCharacterClassesHandlerOld(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"character_classes": classes}) } - -// GetSkillCategoriesHandlerOld gibt alle verfügbaren Fertigkeitskategorien zurück -func GetSkillCategoriesHandlerOld(c *gin.Context) { +*/ +// GetSkillCategoriesHandlerStatic gibt alle verfügbaren Fertigkeitskategorien zurück +func GetSkillCategoriesHandlerStatic(c *gin.Context) { categories := map[string]interface{}{ "Alltag": map[string]interface{}{ "name": "Alltag", diff --git a/backend/cmd/test_learning_costs/main.go b/backend/cmd/test_learning_costs/main.go deleted file mode 100644 index bc17724..0000000 --- a/backend/cmd/test_learning_costs/main.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "bamort/database" - "bamort/gsmaster" - "bamort/models" - "fmt" - "strings" -) - -func main() { - // Initialize config and database - database.SetupTestDB(true, true) - defer database.ResetTestDB() - - // Test some skills for Magier character class - testSkills := []string{"Schreiben", "Athletik", "Lesen von Zauberformeln", "Zaubern"} - characterClass := "Ma" // Magier abbreviation - - fmt.Printf("Testing learning costs for character class: %s\n", characterClass) - fmt.Println(strings.Repeat("=", 50)) - - for _, skillName := range testSkills { - // Test the same logic as our handler - fmt.Printf("\n--- Testing skill: %s ---\n", skillName) - - // Try gsmaster.FindBestCategoryForSkillLearningOld - bestCategory, difficulty, err := gsmaster.FindBestCategoryForSkillLearningOld(skillName, characterClass) - if err == nil && bestCategory != "" { - fmt.Printf("✅ FindBestCategory: Category=%s, Difficulty=%s\n", bestCategory, difficulty) - } else { - fmt.Printf("❌ FindBestCategory: ERROR: %v\n", err) - } - - // Try models.GetSkillCategoryAndDifficultyNewSystem - skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, characterClass) - if err == nil { - fmt.Printf("✅ GetSkillCategory: Category=%s, LearnCost=%d\n", - skillLearningInfo.CategoryName, skillLearningInfo.LearnCost) - } else { - fmt.Printf("❌ GetSkillCategory: ERROR: %v\n", err) - } - } - - // Also test with empty character class - fmt.Println("\nTesting with empty character class:") - for _, skillName := range testSkills { - skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, "") - if err != nil { - fmt.Printf("❌ Skill: %s (empty class) - ERROR: %v\n", skillName, err) - continue - } - - fmt.Printf("✅ Skill: %s (empty class) - Category: %s, LearnCost: %d\n", - skillName, - skillLearningInfo.CategoryName, - skillLearningInfo.LearnCost) - } -} diff --git a/backend/gsmaster/beidhändiger_kampf_test.go b/backend/gsmaster/beidhändiger_kampf_test.go deleted file mode 100644 index bed770e..0000000 --- a/backend/gsmaster/beidhändiger_kampf_test.go +++ /dev/null @@ -1,89 +0,0 @@ -package gsmaster - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestBeidhändigerKampfFürPS(t *testing.T) { - // Testfall für das Erlernen von "Beidhändiger Kampf" durch einen Priester Streiter (PS) - t.Run("Lernkosten für Beidhändiger Kampf als PS", func(t *testing.T) { - // Verwende exportierte Funktion zum Berechnen der Lernkosten - result, err := CalculateDetailedSkillLearningCostOld("Beidhändiger Kampf", "PS") - assert.NoError(t, err, "Es sollte keinen Fehler beim Berechnen der Lernkosten geben") - assert.True(t, result.LE > 0, "LE-Kosten sollten größer als 0 sein") - - // Ausgabe der Ergebnisse - fmt.Printf("Lernkosten für Beidhändiger Kampf als PS:\n") - fmt.Printf("Lerneinheiten (LE): %d\n", result.LE) - fmt.Printf("Erfahrungspunkte (EP): %d\n", result.Ep) - fmt.Printf("Geldkosten (GS): %d\n", result.Money) - - // Überprüfung der Werte basierend auf der aktuellen Implementierung - assert.Equal(t, 2, result.LE, "LE-Kosten sollten 2 sein") - assert.Equal(t, 40, result.Ep, "EP-Kosten sollten 40 sein") - assert.Equal(t, 40, result.Money, "Geldkosten sollten 40 GS sein") - }) - - // Testfall für das Verbessern von "Beidhändiger Kampf" durch einen Priester Streiter (PS) - // von Stufe 5 auf 6 - t.Run("Verbesserungskosten für Beidhändiger Kampf als PS von 5 auf 6", func(t *testing.T) { - // Verwende exportierte Funktion zum Berechnen der Verbesserungskosten - result, err := CalculateDetailedSkillImprovementCostOld("Beidhändiger Kampf", "PS", 5) - assert.NoError(t, err, "Es sollte keinen Fehler beim Berechnen der Verbesserungskosten geben") - assert.True(t, result.LE > 0, "LE-Kosten sollten größer als 0 sein") - - // Ausgabe der Ergebnisse - fmt.Printf("\nVerbesserungskosten für Beidhändiger Kampf als PS (von 5 auf 6):\n") - fmt.Printf("Trainingseinheiten (TE): %d\n", result.LE) - fmt.Printf("Erfahrungspunkte (EP): %d\n", result.Ep) - fmt.Printf("Geldkosten (GS): %d\n", result.Money) - - // Überprüfung der Werte basierend auf der aktuellen vereinfachten Implementierung - assert.Equal(t, 1, result.LE, "TE-Kosten sollten 1 sein (vereinfachte Implementierung)") - assert.Equal(t, 40, result.Ep, "EP-Kosten sollten 40 sein") - assert.Equal(t, 40, result.Money, "Geldkosten sollten 40 GS sein") - }) - - // Testfall für das Verbessern von "Beidhändiger Kampf" durch einen Priester Streiter (PS) - // von Stufe 6 auf 7 - t.Run("Verbesserungskosten für Beidhändiger Kampf als PS von 6 auf 7", func(t *testing.T) { - // Verwende exportierte Funktion zum Berechnen der Verbesserungskosten - result, err := CalculateDetailedSkillImprovementCostOld("Beidhändiger Kampf", "PS", 6) - assert.NoError(t, err, "Es sollte keinen Fehler beim Berechnen der Verbesserungskosten geben") - assert.True(t, result.LE > 0, "LE-Kosten sollten größer als 0 sein") - - // Ausgabe der Ergebnisse - fmt.Printf("\nVerbesserungskosten für Beidhändiger Kampf als PS (von 6 auf 7):\n") - fmt.Printf("Trainingseinheiten (TE): %d\n", result.LE) - fmt.Printf("Erfahrungspunkte (EP): %d\n", result.Ep) - fmt.Printf("Geldkosten (GS): %d\n", result.Money) - - // Überprüfung der Werte basierend auf der aktuellen vereinfachten Implementierung - assert.Equal(t, 1, result.LE, "TE-Kosten sollten 1 sein (vereinfachte Implementierung)") - assert.Equal(t, 40, result.Ep, "EP-Kosten sollten 40 sein") - assert.Equal(t, 40, result.Money, "Geldkosten sollten 40 GS sein") - }) - - // Testfall für das Verbessern von "Beidhändiger Kampf" durch einen Priester Streiter (PS) - // von Stufe 7 auf 8 - t.Run("Verbesserungskosten für Beidhändiger Kampf als PS von 7 auf 8", func(t *testing.T) { - // Verwende exportierte Funktion zum Berechnen der Verbesserungskosten - result, err := CalculateDetailedSkillImprovementCostOld("Beidhändiger Kampf", "PS", 7) - assert.NoError(t, err, "Es sollte keinen Fehler beim Berechnen der Verbesserungskosten geben") - assert.True(t, result.LE > 0, "LE-Kosten sollten größer als 0 sein") - - // Ausgabe der Ergebnisse - fmt.Printf("\nVerbesserungskosten für Beidhändiger Kampf als PS (von 7 auf 8):\n") - fmt.Printf("Trainingseinheiten (TE): %d\n", result.LE) - fmt.Printf("Erfahrungspunkte (EP): %d\n", result.Ep) - fmt.Printf("Geldkosten (GS): %d\n", result.Money) - - // Überprüfung der Werte basierend auf der aktuellen vereinfachten Implementierung - assert.Equal(t, 1, result.LE, "TE-Kosten sollten 1 sein (vereinfachte Implementierung)") - assert.Equal(t, 40, result.Ep, "EP-Kosten sollten 40 sein") - assert.Equal(t, 40, result.Money, "Geldkosten sollten 40 GS sein") - }) -} diff --git a/backend/gsmaster/learning_costs.go b/backend/gsmaster/learning_costs.go index df29ff9..36502a5 100644 --- a/backend/gsmaster/learning_costs.go +++ b/backend/gsmaster/learning_costs.go @@ -3,8 +3,6 @@ package gsmaster import ( "bamort/database" "bamort/models" - "errors" - "fmt" ) // LearningCostsTable strukturiert die Daten aus Lerntabellen.md @@ -322,98 +320,6 @@ func GetLearningCosts() *LearningCostsTable { return learningCosts } -// CalculateSkillLearningCostsOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -// CalculateSkillLearningCostsOld berechnet die Kosten für das Lernen einer Fertigkeit -func CalculateSkillLearningCostsOld(characterClass, category, difficulty string) (*SkillCostResult, error) { - //Überprüfe ob die tabelle vorhanden ist in der die EP-Kosten pro LE for die einzelnen Kategorien für jede Charakterklasse definiert sind - if learningCosts.EPPerTE == nil { - return nil, errors.New("keine EP-per-TE-Definition gefunden") - } - - // Konvertiere Vollnamen der Charakterklasse zu Abkürzungen falls nötig - classKey := GetClassAbbreviationOld(characterClass) - - // Hole die EP-Kosten pro TE für die angegebene Charakterklasse - classData, exists := learningCosts.EPPerTE[classKey] - if !exists { - return nil, fmt.Errorf("unbekannte Charakterklasse: %s (gesucht als %s)", characterClass, classKey) - } - - // Ermittle die EP pro TE für die angegebene Kategorie - epPerTE, exists := classData[category] - if !exists { - return nil, fmt.Errorf("unbekannte Kategorie '%s' für Klasse %s", category, characterClass) - } - - // 1 Lerneinheit(LE) kostet 3 mal so viel wie eine Trainingseinheit (TE) +6 EP when der Charakter ein Elf ist - - if learningCosts.BaseLearnCost == nil { - return nil, errors.New("keine LE-Definition gefunden") - } - - difficultyData, exists := learningCosts.BaseLearnCost[difficulty] - if !exists { - return nil, fmt.Errorf("unbekannte Schwierigkeit: %s", difficulty) - } - - le := difficultyData["LE"] - totalEP := epPerTE * le - - return &SkillCostResult{ - CharacterClass: characterClass, - SkillName: "", - Category: category, - Difficulty: difficulty, - EP: totalEP, - LE: le, - GoldCost: totalEP, // 1 EP = 1 GS - Details: map[string]interface{}{ - "ep_per_te": epPerTE, - "le_needed": le, - }, - }, nil -} - -// CalculateSpellLearningCostsOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -// CalculateSpellLearningCostsOld berechnet die Kosten für das Lernen eines Zaubers -func CalculateSpellLearningCostsOld(characterClass, spellSchool string, leNeeded int) (*SkillCostResult, error) { - if learningCosts.SpellEPPerLE == nil { - return nil, errors.New("keine Zauber-EP-Definition gefunden") - } - - // Konvertiere Vollnamen zu Abkürzungen falls nötig - classKey := GetClassAbbreviationOld(characterClass) - - classData, exists := learningCosts.SpellEPPerLE[classKey] - if !exists { - return nil, fmt.Errorf("Charakterklasse %s kann keine Zauber lernen", characterClass) - } - - epPerLE, exists := classData[spellSchool] - if !exists || epPerLE == 0 { - return nil, fmt.Errorf("Charakterklasse %s kann keine Zauber der Schule %s lernen", characterClass, spellSchool) - } - - totalEP := epPerLE * leNeeded - - return &SkillCostResult{ - CharacterClass: characterClass, - SkillName: "", - Category: spellSchool, - Difficulty: "", - EP: totalEP, - LE: leNeeded, - GoldCost: totalEP, // 1 EP = 1 GS - Details: map[string]interface{}{ - "ep_per_le": epPerLE, - "le_needed": leNeeded, - "spell_school": spellSchool, - }, - }, nil -} - // SkillCostResult definiert das Ergebnis einer Kostenberechnung type SkillCostResult struct { CharacterClass string `json:"character_class"` @@ -442,85 +348,6 @@ type SkillCostResultNew struct { Details map[string]interface{} `json:"details"` } -// CalculateDetailedSkillLearningCostOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -// CalculateDetailedSkillLearningCostOld berechnet die Kosten für das Lernen einer Fertigkeit mit Details -func CalculateDetailedSkillLearningCostOld(skillName, characterClass string) (*models.LearnCost, error) { - // Fallback-Werte für Skills ohne definierte Kategorie/Schwierigkeit - category := GetDefaultCategoryOld(skillName) - difficulty := GetDefaultDifficultyOld(skillName) - - result, err := CalculateSkillLearningCostsOld(characterClass, category, difficulty) - if err != nil { - return nil, err - } - - // Konvertiere SkillCostResult zu LearnCost - return &models.LearnCost{ - Stufe: 0, // Lernen startet bei Stufe 0 - LE: result.LE, - Ep: result.EP, - Money: result.GoldCost, - }, nil -} - -// CalculateDetailedSkillImprovementCostOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -func CalculateDetailedSkillImprovementCostOld(skillName, characterClass string, currentLevel int) (*models.LearnCost, error) { - // Fallback-Werte für Skills ohne definierte Kategorie/Schwierigkeit - category := GetDefaultCategoryOld(skillName) - difficulty := GetDefaultDifficultyOld(skillName) - - // Verwende die Lernkosten als Basis für Verbesserungen - // In einer vollständigen Implementierung würden hier die ImprovementCost-Tabellen verwendet - baseCost, err := CalculateSkillLearningCostsOld(characterClass, category, difficulty) - if err != nil { - return nil, err - } - - // Vereinfachte Verbesserungslogik: höhere Level = höhere Kosten - improvementFactor := 1.0 - if currentLevel > 10 { - improvementFactor = 1.5 - } else if currentLevel > 15 { - improvementFactor = 2.0 - } - - improvedEP := int(float64(baseCost.EP) * improvementFactor) - - // Konvertiere zu LearnCost - return &models.LearnCost{ - Stufe: currentLevel + 1, // Ziel-Stufe - LE: 1, // TE für Verbesserung (meist 1) - Ep: improvedEP, - Money: improvedEP, - }, nil -} - -// CalculateDetailedSpellLearningCostOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -// CalculateDetailedSpellLearningCostOld berechnet die Kosten für das Lernen eines Zaubers -func CalculateDetailedSpellLearningCostOld(spellName, characterClass string) (*models.LearnCost, error) { - // Standard-Zauberschule bestimmen - spellSchool := getDefaultSpellSchoolOld(spellName) - - // Standard LE für Zauber - standardLE := 4 - - result, err := CalculateSpellLearningCostsOld(characterClass, spellSchool, standardLE) - if err != nil { - return nil, err - } - - // Konvertiere SkillCostResult zu LearnCost - return &models.LearnCost{ - Stufe: 0, // Lernen startet bei Stufe 0 - LE: result.LE, - Ep: result.EP, - Money: result.GoldCost, - }, nil -} - // SkillCategoryOption definiert eine Kategorie-Schwierigkeit-Kombination für eine Fertigkeit type SkillCategoryOption struct { Category string `json:"category"` @@ -528,347 +355,6 @@ type SkillCategoryOption struct { LE int `json:"le"` } -// GetAvailableSkillCategories gibt alle verfügbaren Kategorie-Kombinationen für eine Fertigkeit zurück -func GetAvailableSkillCategories(skillName string) []SkillCategoryOption { - // Basierend auf den offiziellen Lerntabellen.md - alle möglichen Kombinationen - skillOptions := map[string][]SkillCategoryOption{ - "Klettern": { - {Category: "Alltag", Difficulty: "leicht", LE: 1}, - {Category: "Halbwelt", Difficulty: "leicht", LE: 1}, - {Category: "Körper", Difficulty: "leicht", LE: 1}, - }, - "Glücksspiel": { - {Category: "Alltag", Difficulty: "leicht", LE: 1}, - {Category: "Halbwelt", Difficulty: "leicht", LE: 1}, - }, - "Reiten": { - {Category: "Alltag", Difficulty: "leicht", LE: 1}, - {Category: "Kampf", Difficulty: "leicht", LE: 1}, - }, - "Anführen": { - {Category: "Kampf", Difficulty: "normal", LE: 2}, - {Category: "Sozial", Difficulty: "leicht", LE: 2}, - }, - "Etikette": { - {Category: "Alltag", Difficulty: "schwer", LE: 2}, - {Category: "Sozial", Difficulty: "leicht", LE: 2}, - }, - "Gassenwissen": { - {Category: "Halbwelt", Difficulty: "schwer", LE: 2}, - {Category: "Sozial", Difficulty: "normal", LE: 2}, - {Category: "Unterwelt", Difficulty: "leicht", LE: 2}, - }, - "Betäuben": { - {Category: "Halbwelt", Difficulty: "sehr schwer", LE: 10}, - {Category: "Kampf", Difficulty: "schwer", LE: 10}, - }, - "Athletik": { - {Category: "Kampf", Difficulty: "normal", LE: 2}, - {Category: "Körper", Difficulty: "schwer", LE: 2}, - }, - "Balancieren": { - {Category: "Halbwelt", Difficulty: "leicht", LE: 1}, - {Category: "Körper", Difficulty: "leicht", LE: 1}, - }, - "Akrobatik": { - {Category: "Halbwelt", Difficulty: "normal", LE: 2}, - {Category: "Körper", Difficulty: "schwer", LE: 2}, - }, - "Schleichen": { - {Category: "Freiland", Difficulty: "schwer", LE: 4}, - {Category: "Unterwelt", Difficulty: "normal", LE: 4}, - }, - "Spurensuche": { - {Category: "Freiland", Difficulty: "schwer", LE: 4}, - {Category: "Unterwelt", Difficulty: "normal", LE: 4}, - }, - "Tarnen": { - {Category: "Freiland", Difficulty: "schwer", LE: 4}, - {Category: "Unterwelt", Difficulty: "normal", LE: 4}, - }, - "Stehlen": { - {Category: "Halbwelt", Difficulty: "schwer", LE: 2}, - {Category: "Unterwelt", Difficulty: "leicht", LE: 2}, - }, - "Verhören": { - {Category: "Sozial", Difficulty: "normal", LE: 2}, - {Category: "Unterwelt", Difficulty: "normal", LE: 4}, - }, - "Menschenkenntnis": { - {Category: "Sozial", Difficulty: "schwer", LE: 4}, - {Category: "Unterwelt", Difficulty: "schwer", LE: 4}, - }, - "Schreiben": { - {Category: "Alltag", Difficulty: "normal", LE: 1}, - {Category: "Wissen", Difficulty: "leicht", LE: 1}, - }, - "Sprache": { - {Category: "Alltag", Difficulty: "normal", LE: 1}, - {Category: "Wissen", Difficulty: "leicht", LE: 1}, - }, - "Erste Hilfe": { - {Category: "Alltag", Difficulty: "schwer", LE: 2}, - {Category: "Wissen", Difficulty: "normal", LE: 2}, - }, - "Meditieren": { - {Category: "Körper", Difficulty: "schwer", LE: 2}, - {Category: "Wissen", Difficulty: "normal", LE: 2}, - }, - "Naturkunde": { - {Category: "Freiland", Difficulty: "normal", LE: 2}, - {Category: "Wissen", Difficulty: "schwer", LE: 2}, - }, - "Pflanzenkunde": { - {Category: "Freiland", Difficulty: "normal", LE: 2}, - {Category: "Wissen", Difficulty: "schwer", LE: 2}, - }, - "Tierkunde": { - {Category: "Freiland", Difficulty: "normal", LE: 2}, - {Category: "Wissen", Difficulty: "schwer", LE: 2}, - }, - } - - if options, exists := skillOptions[skillName]; exists { - return options - } - - // Fallback: verwende Standard-Mapping (erste gefundene Kategorie) - category := GetDefaultCategoryOld(skillName) - difficulty := GetDefaultDifficultyOld(skillName) - - // Bestimme LE basierend auf Schwierigkeit - le := 2 // Standard - switch difficulty { - case "leicht": - le = 1 - case "normal": - le = 2 - case "schwer": - le = 4 - case "sehr schwer": - le = 10 - } - - return []SkillCategoryOption{ - {Category: category, Difficulty: difficulty, LE: le}, - } -} - -// GetDefaultCategoryOld is deprecated. Use models.GetSkillCategoryAndDifficulty instead. -// This function uses the old hardcoded skill categorization system. -// GetDefaultCategoryOld gibt die erste (bevorzugte) Kategorie für eine Fertigkeit zurück -func GetDefaultCategoryOld(skillName string) string { - // WICHTIG: Wir verwenden bewusst die erste gefundene Kategorie als Standard. - // Für das Lernen ist es unerheblich, aber später wird es für andere Dinge wichtig werden. - // Die Reihenfolge der Kategorien ist nach Wichtigkeit/Häufigkeit sortiert. - categoryMap := map[string]string{ - "Stichwaffen": "Waffen", - "Einhandschlagwaffen": "Waffen", - "Zweihandschlagwaffen": "Waffen", - "Geländelauf": "Körper", - "Klettern": "Körper", - "Schwimmen": "Körper", - "Reiten": "Körper", - "Balancieren": "Körper", - "Tauchen": "Körper", - "Akrobatik": "Körper", - "Athletik": "Körper", - "Laufen": "Körper", - "Meditieren": "Körper", - "Seilkunst": "Alltag", - "Bootfahren": "Alltag", - "Glücksspiel": "Alltag", - "Wagenlenken": "Alltag", - "Schreiben": "Alltag", - "Sprache": "Alltag", - "Erste Hilfe": "Alltag", - "Etikette": "Alltag", - "Gerätekunde": "Alltag", - "Geschäftssinn": "Alltag", - "Musizieren": "Alltag", - "Schleichen": "Freiland", - "Spurensuche": "Freiland", - "Tarnen": "Freiland", - "Überleben": "Freiland", - "Naturkunde": "Freiland", - "Pflanzenkunde": "Freiland", - "Tierkunde": "Freiland", - "Gassenwissen": "Unterwelt", - "Stehlen": "Unterwelt", - "Fallen entdecken": "Unterwelt", - "Schlösser öffnen": "Unterwelt", - "Fallenmechanik": "Unterwelt", - "Meucheln": "Unterwelt", - "Anführen": "Sozial", - "Verführen": "Sozial", - "Verstellen": "Sozial", - "Beredsamkeit": "Sozial", - "Verhören": "Sozial", - "Menschenkenntnis": "Sozial", - "Lesen von Zauberschrift": "Wissen", - "Alchimie": "Wissen", - "Heilkunde": "Wissen", - "Landeskunde": "Wissen", - "Zauberkunde": "Wissen", - } - - if category, exists := categoryMap[skillName]; exists { - return category - } - - // Standard-Fallback - return "Alltag" -} - -func GetDifficulty(skillName string, category string) string { - // aktuell nur ein Wrapper das die Stantruktur noch keine Fettigkeiten in mehreren Kategorien enthält - return GetDefaultDifficultyOld(skillName) -} - -// GetDefaultDifficultyOld is deprecated. Use models.GetSkillCategoryAndDifficulty instead. -// This function uses the old hardcoded skill categorization system. -// GetDefaultDifficultyOld gibt die erste (bevorzugte) Schwierigkeit für eine Fertigkeit zurück -func GetDefaultDifficultyOld(skillName string) string { - // WICHTIG: Korrespondiert mit getDefaultCategory() - verwendet die Schwierigkeit - // der ersten (bevorzugten) Kategorie für konsistente Ergebnisse. - // Schwierigkeitszuordnung basierend auf dem Fertigkeitsnamen und Lerntabellen.md - difficultyMap := map[string]string{ - // Waffen (aus Waffen-Kategorie) - "Stichwaffen": "leicht", // 2 LE - "Einhandschlagwaffen": "normal", // 4 LE - "Zweihandschlagwaffen": "schwer", // 6 LE - - // Körper-Fertigkeiten - "Geländelauf": "leicht", // Körper: leicht (1 LE) - "Klettern": "leicht", // Körper: leicht (1 LE) - "Schwimmen": "leicht", // Körper: leicht (1 LE) - "Balancieren": "leicht", // Körper: leicht (1 LE) - "Reiten": "leicht", // Alltag: leicht (1 LE) - "Tauchen": "normal", // Körper: normal (1 LE) - "Akrobatik": "schwer", // Körper: schwer (2 LE) - "Athletik": "schwer", // Körper: schwer (2 LE) - "Laufen": "schwer", // Körper: schwer (2 LE) - "Meditieren": "schwer", // Körper: schwer (2 LE) - - // Alltag-Fertigkeiten - "Seilkunst": "leicht", // Alltag: leicht (1 LE) - "Bootfahren": "leicht", // Alltag: leicht (1 LE) - "Glücksspiel": "leicht", // Alltag: leicht (1 LE) - "Wagenlenken": "leicht", // Alltag: leicht (1 LE) - "Musizieren": "leicht", // Alltag: leicht (1 LE) - "Schreiben": "normal", // Alltag: normal (1 LE) - "Sprache": "normal", // Alltag: normal (1 LE) - "Erste Hilfe": "schwer", // Alltag: schwer (2 LE) - "Etikette": "schwer", // Alltag: schwer (2 LE) - "Gerätekunde": "sehr schwer", // Alltag: sehr schwer (10 LE) - "Geschäftssinn": "sehr schwer", // Alltag: sehr schwer (10 LE) - - // Freiland-Fertigkeiten - "Überleben": "leicht", // Freiland: leicht (1 LE) - "Naturkunde": "normal", // Freiland: normal (2 LE) - "Pflanzenkunde": "normal", // Freiland: normal (2 LE) - "Tierkunde": "normal", // Freiland: normal (2 LE) - "Schleichen": "schwer", // Freiland: schwer (4 LE) - "Spurensuche": "schwer", // Freiland: schwer (4 LE) - "Tarnen": "schwer", // Freiland: schwer (4 LE) - - // Unterwelt-Fertigkeiten - "Gassenwissen": "leicht", // Unterwelt: leicht (2 LE) - "Stehlen": "leicht", // Unterwelt: leicht (2 LE) - "Fallen entdecken": "normal", // Unterwelt: normal (4 LE) - "Schlösser öffnen": "normal", // Unterwelt: normal (4 LE) - "Fallenmechanik": "schwer", // Unterwelt: schwer (10 LE) - "Meucheln": "schwer", // Unterwelt: schwer (10 LE) - - // Sozial-Fertigkeiten - "Anführen": "leicht", // Sozial: leicht (2 LE) - "Verführen": "leicht", // Sozial: leicht (2 LE) - "Verstellen": "leicht", // Sozial: leicht (2 LE) - "Beredsamkeit": "normal", // Sozial: normal (2 LE) - "Verhören": "normal", // Sozial: normal (2 LE) - "Menschenkenntnis": "schwer", // Sozial: schwer (4 LE) - - // Wissen-Fertigkeiten - "Lesen von Zauberschrift": "leicht", // Wissen: leicht (1 LE) - "Alchimie": "schwer", // Wissen: schwer (2 LE) - "Heilkunde": "schwer", // Wissen: schwer (2 LE) - "Landeskunde": "schwer", // Wissen: schwer (2 LE) - "Zauberkunde": "schwer", // Wissen: schwer (2 LE) - } - - if difficulty, exists := difficultyMap[skillName]; exists { - return difficulty - } - - // Standard-Fallback - return "normal" -} - -// getDefaultSpellSchoolOld is deprecated. Use the new database-based spell categorization system instead. -// This function uses hardcoded spell-to-school mapping. -// getDefaultSpellSchoolOld gibt eine Standard-Zauberschule für einen Zauber zurück -func getDefaultSpellSchoolOld(spellName string) string { - // Vereinfachte Zuordnung von Zauber zu Schulen - spellSchoolMap := map[string]string{ - "Licht": "Erschaffen", - "Sehen": "Erkennen", - "Heilen": "Verändern", - "Schutz": "Beherrschen", - "Unsichtbarkeit": "Verändern", - "Feuerlanze": "Zerstören", - "Eislanze": "Zerstören", - "Blitze": "Zerstören", - "Schweben": "Bewegen", - "Teleportation": "Bewegen", - } - - if school, exists := spellSchoolMap[spellName]; exists { - return school - } - - // Standard-Fallback - return "Verändern" -} - -// GetClassAbbreviationOld is deprecated. Use standardized class names or database lookups instead. -// This function uses hardcoded class name mapping. -// GetClassAbbreviationOld konvertiert Charakterklassen-Vollnamen zu Abkürzungen -func GetClassAbbreviationOld(characterClass string) string { - // Mapping von Vollnamen zu Abkürzungen - classMap := map[string]string{ - // Abenteurer-Klassen - "Assassine": "As", - "Barbar": "Bb", - "Glücksritter": "Gl", - "Händler": "Hä", - "Krieger": "Kr", - "Spitzbube": "Sp", - "Waldläufer": "Wa", - // Zauberer-Klassen - "Barde": "Ba", - "Ordenskrieger": "Or", - "Druide": "Dr", - "Hexer": "Hx", - "Magier": "Ma", - "Priester": "PB", // Standard Priester = Beschützer - "Priester Beschützer": "PB", - "Priester Streiter": "PS", - "Schamane": "Sc", - } - - // Prüfe ob es bereits eine Abkürzung ist - if len(characterClass) <= 2 { - return characterClass - } - - // Suche nach Vollname - if abbrev, exists := classMap[characterClass]; exists { - return abbrev - } - - // Fallback: originale Eingabe zurückgeben - return characterClass -} func GetClassAbbreviationNewSystem(characterClass string) string { // Try to find by code first (e.g., "Kr" -> "Kr") var charClass models.CharacterClass diff --git a/backend/gsmaster/lernkosten_maps.go b/backend/gsmaster/lernkosten_maps.go index e65be8e..10e6e97 100644 --- a/backend/gsmaster/lernkosten_maps.go +++ b/backend/gsmaster/lernkosten_maps.go @@ -491,48 +491,6 @@ var learningCostsData = &LearningCostsTable2{ }, } -// GetSkillCategoryOld is deprecated. Use models.GetSkillCategoryAndDifficulty instead. -// This function uses the old hardcoded skill categorization system. -func GetSkillCategoryOld(skillName string) string { - - for category, difficulties := range learningCostsData.ImprovementCost { - for _, data := range difficulties { - if contains(data.Skills, skillName) { - return category - } - } - } - return "Unbekannt" -} - -// GetSkillDifficultyOld is deprecated. Use models.GetSkillCategoryAndDifficulty instead. -// This function uses the old hardcoded skill categorization system. -func GetSkillDifficultyOld(category string, skillName string) string { - // Wenn eine Kategorie angegeben ist, suche nur in dieser Kategorie - if category != "" { - difficulties, ok := learningCostsData.ImprovementCost[category] - if !ok { - return "Unbekannt" // Kategorie nicht gefunden - } - for difficulty, data := range difficulties { - if contains(data.Skills, skillName) { - return difficulty - } - } - return "Unbekannt" // Skill in der angegebenen Kategorie nicht gefunden - } - - // Wenn keine Kategorie angegeben ist, durchsuche alle Kategorien und gib das erste Vorkommen zurück - for _, difficulties := range learningCostsData.ImprovementCost { - for difficulty, data := range difficulties { - if contains(data.Skills, skillName) { - return difficulty - } - } - } - return "Unbekannt" -} - // contains checks if a slice contains a specific string func contains(slice []string, item string) bool { for _, s := range slice { @@ -558,351 +516,3 @@ func GetSpellInfoNewSystem(spellName string) (string, int, error) { return spell.Category, spell.Stufe, nil } - -// GetSpecializationOld is deprecated. Use appropriate character data access methods instead. -// This function is a placeholder that should not be used in production. -// GetSpecializationOld returns the specialization school for a character (placeholder) -// This should be implemented to get the actual specialization from character data -func GetSpecializationOld(characterID string) string { - // TODO: Implement actual character specialization lookup - // For now, return a default specialization - return "Beherrschen" -} - -// findBestCategoryForSkillImprovementOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -// findBestCategoryForSkillImprovementOld findet die Kategorie mit den niedrigsten EP-Kosten für eine Fertigkeit -func findBestCategoryForSkillImprovementOld(skillName, characterClass string, level int) (string, string, error) { - classKey := characterClass - - // Sammle alle Kategorien und Schwierigkeiten, in denen die Fertigkeit verfügbar ist - type categoryOption struct { - category string - difficulty string - epCost int - } - - var options []categoryOption - - for category, difficulties := range learningCostsData.ImprovementCost { - for difficulty, data := range difficulties { - if contains(data.Skills, skillName) { - // Prüfe ob EP-Kosten für diese Kategorie und Klasse existieren - epPerTE, exists := learningCostsData.EPPerTE[classKey][category] - if exists { - // Hole die Trainingskosten für level - trainCost, hasCost := data.TrainCosts[level] - if hasCost { - totalEP := epPerTE * trainCost - options = append(options, categoryOption{ - category: category, - difficulty: difficulty, - epCost: totalEP, - }) - } - } - } - } - } - - if len(options) == 0 { - return "", "", fmt.Errorf("keine verfügbare Kategorie für Fertigkeit '%s' und Klasse '%s' auf Level %d gefunden", skillName, characterClass, level) - } - - // Finde die Option mit den niedrigsten EP-Kosten - bestOption := options[0] - for _, option := range options[1:] { - if option.epCost < bestOption.epCost { - bestOption = option - } - } - - return bestOption.category, bestOption.difficulty, nil -} - -// FindBestCategoryForSkillLearningOld is deprecated. Use the new database-based learning cost system instead. -// This function uses the old hardcoded learning cost data. -// FindBestCategoryForSkillLearningOld findet die Kategorie mit den niedrigsten EP-Kosten für das Lernen einer Fertigkeit -func FindBestCategoryForSkillLearningOld(skillName, characterClass string) (string, string, error) { - classKey := characterClass - - // Sammle alle Kategorien und Schwierigkeiten, in denen die Fertigkeit verfügbar ist - type categoryOption struct { - category string - difficulty string - epCost int - } - - var options []categoryOption - - for category, difficulties := range learningCostsData.ImprovementCost { - for difficulty, data := range difficulties { - if contains(data.Skills, skillName) { - // Prüfe ob EP-Kosten für diese Kategorie und Klasse existieren - epPerTE, exists := learningCostsData.EPPerTE[classKey][category] - if exists { - // Für das Lernen verwenden wir LearnCost * 3 - learnCost := data.LearnCost - totalEP := epPerTE * learnCost * 3 - options = append(options, categoryOption{ - category: category, - difficulty: difficulty, - epCost: totalEP, - }) - } - } - } - } - - if len(options) == 0 { - return "", "", fmt.Errorf("keine verfügbare Kategorie für Fertigkeit '%s' und Klasse '%s' gefunden", skillName, characterClass) - } - - // Finde die Option mit den niedrigsten EP-Kosten - bestOption := options[0] - for _, option := range options[1:] { - if option.epCost < bestOption.epCost { - bestOption = option - } - } - - return bestOption.category, bestOption.difficulty, nil -} - -// CalcSkillLernCostOld is deprecated. Use CalcSkillLernCost instead. -// This function uses the old hardcoded learning cost system. -func CalcSkillLernCostOld(costResult *SkillCostResultNew, reward *string) error { - // Berechne die Lernkosten basierend auf den aktuellen Werten im costResult - // Hier sollte die Logik zur Berechnung der Lernkosten implementiert werden - //Finde EP kosten für die Kategorie für die Charakterklasse aus learningCostsData.EPPerTE - // Konvertiere Vollnamen der Charakterklasse zu Abkürzungen falls nötig - //classKey := getClassAbbreviation(costResult.CharacterClass) - classKey := costResult.CharacterClass - - // Wenn Kategorie und Schwierigkeit noch nicht gesetzt sind, finde die beste Option - if costResult.Category == "" || costResult.Difficulty == "" { - bestCategory, bestDifficulty, err := FindBestCategoryForSkillLearningOld(costResult.SkillName, classKey) - if err != nil { - return err - } - costResult.Category = bestCategory - costResult.Difficulty = bestDifficulty - } - - epPerTE, ok := learningCostsData.EPPerTE[classKey][costResult.Category] - if !ok { - return fmt.Errorf("EP-Kosten für Kategorie '%s' und Klasse '%s' nicht gefunden", costResult.Category, costResult.CharacterClass) - } - // finde LE für den Skill aufgrund der Kategorie und schwierigkeit aus DifficultyData - learnCost, ok := learningCostsData.ImprovementCost[costResult.Category][costResult.Difficulty] - if !ok { - return fmt.Errorf("Lernkosten für Kategorie '%s' und Schwierigkeit '%s' nicht gefunden", costResult.Category, costResult.Difficulty) - } - costResult.LE = learnCost.LearnCost - costResult.EP = epPerTE * costResult.LE * 3 - costResult.GoldCost = costResult.LE * 200 // Beispiel: 200 Gold pro LE - - // Apply reward logic - if reward != nil { - switch *reward { - case "noGold": - costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung - case "halveep": - costResult.EP = costResult.EP / 2 // Halbe EP-Kosten - costResult.GoldCost = 0 // Keine Goldkosten bei halven EP - case "halveepnoGold": - costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung - costResult.EP = costResult.EP / 2 // Halbe EP-Kosten - case "default": - // Keine Änderungen, normale Kosten - } - } - - return nil -} - -// CalcSkillImproveCostOld is deprecated. Use CalcSkillImproveCost instead. -// This function uses the old hardcoded learning cost system. -// CalcSkillImproveCostOld berechnet die Kosten für die Verbesserung einer Fertigkeit -func CalcSkillImproveCostOld(costResult *SkillCostResultNew, currentLevel int, reward *string) error { - // Für Skill-Verbesserung könnten die Kosten vom aktuellen Level abhängen - - //Finde EP kosten für die Kategorie für die Charakterklasse aus learningCostsData.EPPerTE - //classKey := getClassAbbreviation(costResult.CharacterClass) - classKey := costResult.CharacterClass - - if costResult.TargetLevel > 0 { - currentLevel = costResult.TargetLevel - 1 // Wenn ein Ziellevel angegeben ist, verwende dieses - } - - // Wenn Kategorie und Schwierigkeit noch nicht gesetzt sind, finde die beste Option - if costResult.Category == "" || costResult.Difficulty == "" { - bestCategory, bestDifficulty, err := findBestCategoryForSkillImprovementOld(costResult.SkillName, classKey, currentLevel+1) - if err != nil { - return err - } - costResult.Category = bestCategory - costResult.Difficulty = bestDifficulty - } - - epPerTE, ok := learningCostsData.EPPerTE[classKey][costResult.Category] - if !ok { - return fmt.Errorf("EP-Kosten für Kategorie '%s' und Klasse '%s' nicht gefunden", costResult.Category, costResult.CharacterClass) - } - - diffData := learningCostsData.ImprovementCost[costResult.Category][costResult.Difficulty] - - trainCost := diffData.TrainCosts[currentLevel+1] - if trainCost < costResult.PPUsed { - costResult.PPUsed = trainCost //maximal so viele PP verwenden wie TE benötigt werden - trainCost = 0 // Wenn PP verwendet werden, setze die Kosten auf - } else if costResult.PPUsed > 0 { - trainCost -= costResult.PPUsed // Wenn PP verwendet werden, setze die Kosten auf die PP - } - // Apply reward logic - costResult.LE = trainCost - costResult.EP = epPerTE * trainCost - costResult.GoldCost = trainCost * 20 // Beispiel: 20 Gold pro TE - - if reward != nil && *reward == "halveep" { - costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung - } - if reward != nil && *reward == "halveepnoGold" { - costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung - costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung - } - if costResult.GoldUsed > 0 { - // 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden - maxEPFromGold := costResult.EP / 2 - epFromGold := costResult.GoldUsed / 10 - - if epFromGold > maxEPFromGold { - // Beschränke auf maximal EP/2 - epFromGold = maxEPFromGold - costResult.GoldUsed = epFromGold * 10 - } - - // Reduziere EP um die durch Gold ersetzte Menge - costResult.EP -= epFromGold - } - - return nil -} - -// CalcSpellLernCostOld is deprecated. Use CalcSpellLernCost instead. -// This function uses the old hardcoded learning cost system. -// CalcSpellLernCostOld berechnet die Kosten für das Erlernen eines Zaubers -func CalcSpellLernCostOld(costResult *SkillCostResultNew, reward *string) error { - // Für Zauber verwenden wir eine ähnliche Logik wie für Skills - // TODO: Implementiere spezifische Zauber-Kostenlogik wenn verfügbar - classKey := costResult.CharacterClass - spellCategory, spellLevel, err := GetSpellInfoNewSystem(costResult.SkillName) - if err != nil { - return fmt.Errorf("failed to get spell info: %w", err) - } - SpellEPPerLE, ok := learningCostsData.SpellEPPerLE[classKey][spellCategory] - if !ok { - return fmt.Errorf("EP-Kosten für Zauber '%s' und Klasse '%s' nicht gefunden", costResult.SkillName, classKey) - } - if classKey == "Ma" { - spezialgebiet := GetSpecializationOld(costResult.CharacterID) - if spellCategory == spezialgebiet { - SpellEPPerLE = 30 // Spezialgebiet für Magier - } - } - - trainCost := learningCostsData.SpellLEPerLevel[spellLevel] // LE pro Stufe des Zaubers - if costResult.PPUsed > 0 { - trainCost -= costResult.PPUsed // Wenn PP verwendet werden, reduziere die LE-Kosten - if trainCost < 0 { - trainCost = 0 // Verhindere negative LE-Kosten - } - } - costResult.LE = trainCost // Setze die LE-Kosten - costResult.EP = trainCost * SpellEPPerLE // EP-Kosten für das Lernen des Zaubers - costResult.GoldCost = trainCost * 100 // Beispiel: 100 Gold pro LE - costResult.Category = spellCategory - costResult.Difficulty = fmt.Sprintf("Stufe %d", spellLevel) // Zauber haben keine Schwierigkeit, sondern eine Stufe - if reward != nil && *reward == "spruchrolle" { - costResult.GoldCost = 20 // 20 Gold für Jeden Versuch - costResult.EP = costResult.EP / 3 // 1/3 EP-Kosten bei Erfolg - } else { - - if reward != nil && *reward == "halveep" { - costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung - } - if reward != nil && *reward == "halveepnoGold" { - costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung - costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung - } - } - - // Anwenden von Gold für EP Konvertierung (falls Gold verwendet wird) - if costResult.GoldUsed > 0 { - // 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden - maxEPFromGold := costResult.EP / 2 - epFromGold := costResult.GoldUsed / 10 - - if epFromGold > maxEPFromGold { - // Beschränke auf maximal EP/2 - epFromGold = maxEPFromGold - costResult.GoldUsed = epFromGold * 10 - } - - // Reduziere EP um die durch Gold ersetzte Menge - costResult.EP -= epFromGold - } - - return nil -} - -// GetLernCostNextLevelOld is deprecated. Use GetLernCostNextLevel instead. -// This function uses the old hardcoded learning cost system. -func GetLernCostNextLevelOld(request *LernCostRequest, costResult *SkillCostResultNew, reward *string, level int, characterRasse string) error { - // Diese Funktion berechnet die Kosten für das Erlernen oder Verbessern einer Fertigkeit oder eines Zaubers - // abhängig von der Aktion (learn/improve) und der Belohnung. - // die Berechnung erfolgt immer für genau 1 Level - // Diese Funktion wird in GetLernCost aufgerufen. - - // Übertrage PP aus dem Request für die Kostenberechnung - // PP sind nur bei "improve" und "spell learn" erlaubt, nicht bei "skill learn" - costResult.PPUsed = 0 - // Gold für EP wird nur bei "improve" Aktionen und "spell learn" verwendet, nicht beim "skill learn" - costResult.GoldUsed = 0 - - switch { - case request.Action == "learn" && request.Type == "skill": - // Skill-Lernen: Kein PP und kein Gold für EP erlaubt - err := CalcSkillLernCostOld(costResult, request.Reward) - if err != nil { - return fmt.Errorf("fehler bei der Kostenberechnung: %w", err) - } - // extrakosten für elfen - if characterRasse == "Elf" { - costResult.EP += 6 - } - case request.Action == "learn" && request.Type == "spell": - // Zauber-Lernen: PP und Gold für EP ist erlaubt - costResult.PPUsed = request.UsePP - costResult.GoldUsed = request.UseGold - err := CalcSpellLernCostOld(costResult, request.Reward) - if err != nil { - return fmt.Errorf("fehler bei der Kostenberechnung: %w", err) - } - // extrakosten für elfen - if characterRasse == "Elf" { - costResult.EP += 6 - } - case request.Action == "improve" && request.Type == "skill": - // Skill-Verbesserung: PP und Gold für EP ist erlaubt - costResult.PPUsed = request.UsePP - costResult.GoldUsed = request.UseGold - err := CalcSkillImproveCostOld(costResult, request.CurrentLevel, request.Reward) - if err != nil { - return fmt.Errorf("fehler bei der Kostenberechnung: %w", err) - } - - default: - } - return nil -} diff --git a/backend/gsmaster/lernkosten_maps_test.go b/backend/gsmaster/lernkosten_maps_test.go index 79d11d1..158f3ec 100644 --- a/backend/gsmaster/lernkosten_maps_test.go +++ b/backend/gsmaster/lernkosten_maps_test.go @@ -11,180 +11,6 @@ func stringPtr(s string) *string { return &s } -// TestGetSkillCategory tests the GetSkillCategory function -func TestGetSkillCategory(t *testing.T) { - tests := []struct { - name string - skillName string - expected []string // Allow multiple valid categories for skills that appear in multiple places - }{ - { - name: "Skill in multiple categories", - skillName: "Klettern", // appears in Alltag, Halbwelt, and Körper - expected: []string{"Alltag", "Halbwelt", "Körper"}, - }, - { - name: "Skill in Freiland category", - skillName: "Überleben", - expected: []string{"Freiland"}, - }, - { - name: "Skill in Waffen category", - skillName: "Stichwaffen", - expected: []string{"Waffen"}, - }, - { - name: "Skill in Wissen category", - skillName: "Alchimie", - expected: []string{"Wissen"}, - }, - { - name: "Skill unique to one category", - skillName: "Gerätekunde", // only in Alltag sehr schwer - expected: []string{"Alltag"}, - }, - { - name: "Non-existent skill", - skillName: "NichtExistierendeFertigkeit", - expected: []string{"Unbekannt"}, - }, - { - name: "Empty skill name", - skillName: "", - expected: []string{"Unbekannt"}, - }, - { - name: "Case sensitive test", - skillName: "klettern", // lowercase - expected: []string{"Unbekannt"}, // should not match "Klettern" - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - result := GetSkillCategoryOld(tt.skillName) - - // Check if result is in the list of expected values - found := false - for _, expected := range tt.expected { - if result == expected { - found = true - break - } - } - - if !found { - t.Errorf("GetSkillCategory(%q) = %q, want one of %v", tt.skillName, result, tt.expected) - } - }) - } -} - -// TestGetSkillDifficulty tests the GetSkillDifficulty function -func TestGetSkillDifficulty(t *testing.T) { - tests := []struct { - name string - category string - skillName string - expected string - }{ - { - name: "Skill with specific category", - category: "Alltag", - skillName: "Klettern", - expected: "leicht", - }, - { - name: "Skill with specific category - normal difficulty", - category: "Alltag", - skillName: "Schreiben", - expected: "normal", - }, - { - name: "Skill with specific category - schwer difficulty", - category: "Alltag", - skillName: "Erste Hilfe", - expected: "schwer", - }, - { - name: "Skill with specific category - sehr schwer difficulty", - category: "Alltag", - skillName: "Gerätekunde", - expected: "sehr schwer", - }, - { - name: "Skill without category - should return first occurrence", - category: "", - skillName: "Klettern", - expected: "leicht", // appears as leicht in all categories where it exists - }, - { - name: "Skill without category - another skill", - category: "", - skillName: "Überleben", - expected: "leicht", // in Freiland - }, - { - name: "Skill in wrong category", - category: "Waffen", - skillName: "Klettern", // Klettern is not in Waffen category - expected: "Unbekannt", - }, - { - name: "Non-existent category", - category: "NichtExistierendeKategorie", - skillName: "Klettern", - expected: "Unbekannt", - }, - { - name: "Non-existent skill with valid category", - category: "Alltag", - skillName: "NichtExistierendeFertigkeit", - expected: "Unbekannt", - }, - { - name: "Non-existent skill without category", - category: "", - skillName: "NichtExistierendeFertigkeit", - expected: "Unbekannt", - }, - { - name: "Empty skill name with category", - category: "Alltag", - skillName: "", - expected: "Unbekannt", - }, - { - name: "Empty skill name without category", - category: "", - skillName: "", - expected: "Unbekannt", - }, - { - name: "Skill that appears in multiple categories - specific category", - category: "Halbwelt", - skillName: "Klettern", // also exists in Alltag and Körper - expected: "leicht", - }, - { - name: "Skill in Freiland schwer", - category: "Freiland", - skillName: "Schleichen", - expected: "schwer", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := GetSkillDifficultyOld(tt.category, tt.skillName) - if result != tt.expected { - t.Errorf("GetSkillDifficulty(%q, %q) = %q, want %q", tt.category, tt.skillName, result, tt.expected) - } - }) - } -} - // TestContains tests the contains helper function func TestContains(t *testing.T) { tests := []struct { @@ -253,121 +79,6 @@ func TestContains(t *testing.T) { } } -// TestCalcSkillLernCost tests the CalcSkillLernCost function -func TestCalcSkillLernCost(t *testing.T) { - tests := []struct { - name string - costResult *SkillCostResultNew - expectError bool - expectedLE int - expectedEP int - expectedGold int - }{ - { - name: "Valid calculation for Assassine Alltag leicht", - costResult: &SkillCostResultNew{ - CharacterClass: "As", - Category: "Alltag", - Difficulty: "leicht", - }, - expectError: false, - expectedLE: 1, // LearnCost for leicht in Alltag - expectedEP: 60, // 20 (EP per TE for As/Alltag) * 1 (LE) * 3 - expectedGold: 200, // 1 (LE) * 200 - }, - { - name: "Valid calculation for Krieger Waffen schwer", - costResult: &SkillCostResultNew{ - CharacterClass: "Kr", - Category: "Waffen", - Difficulty: "schwer", - }, - expectError: false, - expectedLE: 6, // LearnCost for schwer in Waffen - expectedEP: 180, // 10 (EP per TE for Kr/Waffen) * 6 (LE) * 3 - expectedGold: 1200, // 6 (LE) * 200 - }, - { - name: "Valid calculation for Magier Wissen normal", - costResult: &SkillCostResultNew{ - CharacterClass: "Ma", - Category: "Wissen", - Difficulty: "normal", - }, - expectError: false, - expectedLE: 2, // LearnCost for normal in Wissen - expectedEP: 60, // 10 (EP per TE for Ma/Wissen) * 2 (LE) * 3 - expectedGold: 400, // 2 (LE) * 200 - }, - { - name: "Invalid character class", - costResult: &SkillCostResultNew{ - CharacterClass: "InvalidClass", - Category: "Alltag", - Difficulty: "leicht", - }, - expectError: true, - }, - { - name: "Invalid category", - costResult: &SkillCostResultNew{ - CharacterClass: "As", - Category: "InvalidCategory", - Difficulty: "leicht", - }, - expectError: true, - }, - { - name: "Invalid difficulty", - costResult: &SkillCostResultNew{ - CharacterClass: "As", - Category: "Alltag", - Difficulty: "InvalidDifficulty", - }, - expectError: true, - }, - { - name: "Valid but category not in character class", - costResult: &SkillCostResultNew{ - CharacterClass: "As", - Category: "Schilde und Parierwaffen", // This category might not have EP costs for As - Difficulty: "normal", - }, - expectError: true, // Should fail because EP costs not found - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := CalcSkillLernCostOld(tt.costResult, nil) // nil reward for original tests - - if tt.expectError { - if err == nil { - t.Errorf("CalcSkillLernCost() expected error but got none") - } - return - } - - if err != nil { - t.Errorf("CalcSkillLernCost() unexpected error: %v", err) - return - } - - if tt.costResult.LE != tt.expectedLE { - t.Errorf("CalcSkillLernCost() LE = %d, want %d", tt.costResult.LE, tt.expectedLE) - } - - if tt.costResult.EP != tt.expectedEP { - t.Errorf("CalcSkillLernCost() EP = %d, want %d", tt.costResult.EP, tt.expectedEP) - } - - if tt.costResult.GoldCost != tt.expectedGold { - t.Errorf("CalcSkillLernCost() GoldCost = %d, want %d", tt.costResult.GoldCost, tt.expectedGold) - } - }) - } -} - // TestLearningCostsDataIntegrity tests the integrity of the learning costs data structure func TestLearningCostsDataIntegrity(t *testing.T) { // Test that learningCostsData is not nil @@ -422,324 +133,6 @@ func TestLearningCostsDataIntegrity(t *testing.T) { } } -// TestSkillCoverage tests that all skills in the data structure can be found by the functions -func TestSkillCoverage(t *testing.T) { - skillsFound := make(map[string]bool) - - // Collect all skills from the data structure - for category, difficulties := range learningCostsData.ImprovementCost { - for difficulty, data := range difficulties { - for _, skill := range data.Skills { - if skill != "" { // Skip empty skill names - skillsFound[skill] = false - - // Test that GetSkillCategory can find this skill - foundCategory := GetSkillCategoryOld(skill) - if foundCategory == "Unbekannt" { - t.Errorf("GetSkillCategory could not find skill: %s (should be in %s)", skill, category) - } - - // Test that GetSkillDifficulty can find this skill without category - foundDifficulty := GetSkillDifficultyOld("", skill) - if foundDifficulty == "Unbekannt" { - t.Errorf("GetSkillDifficulty could not find skill: %s (should have difficulty %s)", skill, difficulty) - } - - // Test that GetSkillDifficulty can find this skill with category - foundDifficultyWithCategory := GetSkillDifficultyOld(category, skill) - if foundDifficultyWithCategory == "Unbekannt" { - t.Errorf("GetSkillDifficulty could not find skill: %s in category %s (should have difficulty %s)", skill, category, difficulty) - } - - skillsFound[skill] = true - } - } - } - } - - t.Logf("Tested coverage for %d unique skills", len(skillsFound)) -} - -// TestFindBestCategoryForSkill tests the findBestCategoryForSkill function -func TestFindBestCategoryForSkill(t *testing.T) { - tests := []struct { - name string - skillName string - characterClass string - currentLevel int - expectedCategory string - expectError bool - }{ - { - name: "Klettern - should choose cheapest category", - skillName: "Klettern", - characterClass: "Kr", // Krieger - currentLevel: 13, // Level 13->14 - // Klettern ist in: Alltag (leicht), Halbwelt (leicht), Körper (leicht) - // Für Kr: Alltag=20 EP/TE, Halbwelt=30 EP/TE, Körper=20 EP/TE - // Level 13->14 kostet in allen 1 TE, also 20*1=20 EP für Alltag und Körper, 30*1=30 EP für Halbwelt - // Sollte Alltag oder Körper wählen (beide gleich günstig) - expectedCategory: "Alltag", // oder "Körper" - beide sind gleich günstig - expectError: false, - }, - { - name: "Non-existent skill", - skillName: "NichtExistierendeFertigkeit", - characterClass: "Kr", - currentLevel: 10, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - category, difficulty, err := findBestCategoryForSkillImprovementOld(tt.skillName, tt.characterClass, tt.currentLevel) - - if tt.expectError { - if err == nil { - t.Errorf("Expected error but got none") - } - return - } - - if err != nil { - t.Fatalf("Unexpected error: %v", err) - } - - // Für Klettern sind mehrere Kategorien gleich günstig, also akzeptieren wir alle - if tt.skillName == "Klettern" { - validCategories := []string{"Alltag", "Körper"} // Beide haben 20 EP/TE für Kr - found := false - for _, validCat := range validCategories { - if category == validCat { - found = true - break - } - } - if !found { - t.Errorf("Expected category to be one of %v, got %s", validCategories, category) - } - } else { - if category != tt.expectedCategory { - t.Errorf("Expected category %s, got %s", tt.expectedCategory, category) - } - } - - t.Logf("Skill %s for class %s at level %d: category=%s, difficulty=%s", - tt.skillName, tt.characterClass, tt.currentLevel, category, difficulty) - }) - } -} - -// TestCalcSkillLernCostWithRewards tests the reward logic in CalcSkillLernCost -func TestCalcSkillLernCostWithRewards(t *testing.T) { - tests := []struct { - name string - skillName string - characterClass string - reward *string - expectedGold int - expectedEPMult float64 // multiplier for EP (1.0 = normal, 0.5 = half) - }{ - { - name: "Default reward - normal costs", - skillName: "Klettern", - characterClass: "Kr", // Use abbreviation - reward: stringPtr("default"), - expectedGold: 200, // 1 LE * 200 Gold per LE - expectedEPMult: 1.0, - }, - { - name: "NoGold reward - no gold cost", - skillName: "Klettern", - characterClass: "Kr", // Use abbreviation - reward: stringPtr("noGold"), - expectedGold: 0, // Should be 0 with noGold reward - expectedEPMult: 1.0, - }, - { - name: "No reward - normal costs", - skillName: "Klettern", - characterClass: "Kr", // Use abbreviation - reward: nil, - expectedGold: 200, // 1 LE * 200 Gold per LE - expectedEPMult: 1.0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create cost result - cat, difficulty, _ := FindBestCategoryForSkillLearningOld(tt.skillName, tt.characterClass) - costResult := &SkillCostResultNew{ - CharacterClass: tt.characterClass, - SkillName: tt.skillName, - Category: cat, - Difficulty: difficulty, - } - - // Calculate normal costs first to get baseline EP - baselineResult := &SkillCostResultNew{ - CharacterClass: tt.characterClass, - SkillName: tt.skillName, - Category: costResult.Category, - Difficulty: costResult.Difficulty, - } - err := CalcSkillLernCostOld(baselineResult, stringPtr("default")) - if err != nil { - t.Fatalf("Failed to calculate baseline costs: %v", err) - } - - // Calculate costs with reward - err = CalcSkillLernCostOld(costResult, tt.reward) - if err != nil { - t.Fatalf("Failed to calculate costs: %v", err) - } - - // Check gold cost - if costResult.GoldCost != tt.expectedGold { - t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, costResult.GoldCost) - } - - // Check EP cost - expectedEP := int(float64(baselineResult.EP) * tt.expectedEPMult) - if costResult.EP != expectedEP { - t.Errorf("Expected EP %d (baseline %d * %.1f), got %d", expectedEP, baselineResult.EP, tt.expectedEPMult, costResult.EP) - } - - // LE should always be the same regardless of reward - if costResult.LE != baselineResult.LE { - t.Errorf("LE should be unchanged by rewards. Expected %d, got %d", baselineResult.LE, costResult.LE) - } - }) - } -} - -// TestCalcSpellLernCostWithRewards tests the reward logic in CalcSpellLernCost -/* -func TestCalcSpellLernCostWithRewards(t *testing.T) { - costResult := &SkillCostResultNew{ - CharacterClass: "Ma", // Use abbreviation - SkillName: "TestSpell", - Category: "Hellsicht", // Use existing category - Difficulty: "Schwer", - } - - // Test with noGold reward - err := CalcSpellLernCost(costResult, stringPtr("noGold")) - if err != nil { - t.Fatalf("Failed to calculate spell costs: %v", err) - } - - if costResult.GoldCost != 0 { - t.Errorf("Expected gold cost 0 with noGold reward, got %d", costResult.GoldCost) - } -} -*/ - -// TestCalcSkillImproveCostWithRewards tests the reward logic in CalcSkillImproveCost -func TestCalcSkillImproveCostWithRewards(t *testing.T) { - tests := []struct { - name string - skillName string - characterClass string - currentLevel int // represents the level the character currently has must be incremented by 1 when calculating the costs - ppUsed int - reward *string - expectedEP int - expectedGold int - }{ - { - name: "Normal improvement to 13 without reward", - skillName: "Klettern", - characterClass: "Kr", - currentLevel: 12, - ppUsed: 0, - reward: nil, - expectedEP: 20, // Kr has 20 EP/TE for Alltag, level 12->13 costs 0 TE, so 20*0=0 - expectedGold: 20, // 0 TE * 20 Gold per TE - }, - { - name: "Normal improvement to 14 without reward", - skillName: "Klettern", - characterClass: "Kr", - currentLevel: 13, - ppUsed: 0, - reward: nil, - expectedEP: 40, // Kr has 20 EP/TE for Alltag, level 13->14 costs 1 TE, so 20*1=20 - expectedGold: 40, // 1 TE * 20 Gold per TE - }, - { - name: "Improvement with halveep reward", - skillName: "Klettern", - characterClass: "Kr", - currentLevel: 13, - ppUsed: 0, - reward: stringPtr("halveep"), - expectedEP: 20, // Kr has 20 EP/TE for Alltag, level 13->14 costs 1 TE, so 20*1=20, halved = 10 - expectedGold: 40, // Gold cost not affected by halveep - }, - - { - name: "Improvement to 15 without reward", - skillName: "Klettern", - characterClass: "Kr", - currentLevel: 14, - ppUsed: 0, - reward: nil, - expectedEP: 100, // Kr has 20 EP/TE for Alltag, level 14->15 costs 2 TE, minus 1 PP = 1 TE, so 20*1=20 - expectedGold: 100, // 1 TE * 20 Gold per TE - }, - { - name: "Improvement to 15 with PP used", - skillName: "Klettern", - characterClass: "Kr", - currentLevel: 14, - ppUsed: 1, - reward: nil, - expectedEP: 80, // Kr has 20 EP/TE for Alltag, level 14->15 costs 2 TE, minus 1 PP = 1 TE, so 20*1=20 - expectedGold: 80, // 1 TE * 20 Gold per TE - }, - { - name: "Improvement with halveepnoGold reward", - skillName: "Klettern", - characterClass: "Kr", - currentLevel: 15, - ppUsed: 0, - reward: stringPtr("halveepnoGold"), - expectedEP: 100, // Kr has 20 EP/TE for Alltag, level 15->16 costs 5 TE, so 20*5=100, halved = 50 - expectedGold: 0, // Should be 0 with halveepnoGold reward - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - costResult := &SkillCostResultNew{ - CharacterClass: tt.characterClass, - SkillName: tt.skillName, - PPUsed: tt.ppUsed, - // Lassen Sie Kategorie und Schwierigkeit leer, damit die Funktion die beste auswählt - } - - err := CalcSkillImproveCostOld(costResult, tt.currentLevel, tt.reward) - if err != nil { - t.Fatalf("Failed to calculate improvement costs: %v", err) - } - - // Log the chosen category for debugging - t.Logf("Skill: %s, Class %s, Chosen category: %s, difficulty: %s", costResult.SkillName, costResult.CharacterClass, costResult.Category, costResult.Difficulty) - - if costResult.EP != tt.expectedEP { - t.Errorf("Expected EP %d, got %d", tt.expectedEP, costResult.EP) - } - - if costResult.GoldCost != tt.expectedGold { - t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, costResult.GoldCost) - } - }) - } -} - // TestGetSpellInfo tests the GetSpellInfo function func TestGetSpellInfo(t *testing.T) { @@ -840,345 +233,3 @@ func TestGetSpellInfo(t *testing.T) { }) } } - -// TestCalcSpellLernCostWithRewards tests the reward logic in CalcSpellLernCost -func TestCalcSpellLernCostWithRewards(t *testing.T) { - // Initialize test database with migration (but no test data since we don't have the preparedTestDB file) - database.SetupTestDB(true, false) // Use in-memory SQLite, no test data loading - defer database.ResetTestDB() - models.MigrateStructure() - - // Create minimal test spell data for our test - testSpells := []models.Spell{ - { - GameSystem: "midgard", - Name: "Schlummer", - Beschreibung: "Test spell for GetSpellInfo", - Quelle: "Test", - Stufe: 1, - Category: "Beherrschen", - }, - { - GameSystem: "midgard", - Name: "Erkennen von Krankheit", - Beschreibung: "Test spell for GetSpellInfo", - Quelle: "Test", - Stufe: 2, - Category: "Dweomer", - }, - { - GameSystem: "midgard", - Name: "Das Loblied", - Beschreibung: "Test spell for GetSpellInfo", - Quelle: "Test", - Stufe: 3, - Category: "Zauberlied", - }, - } - // Insert test data directly - for _, spell := range testSpells { - if err := database.DB.Create(&spell).Error; err != nil { - t.Fatalf("Failed to create test spell: %v", err) - } - } - - tests := []struct { - name string - spellName string - characterClass string - reward *string - expectedEP int - expectedGold int - }{ - { - name: "Simple spell for Magier without but specialized", - spellName: "Schlummer", - characterClass: "Ma", - reward: nil, - expectedEP: 30, // Ma has 60 EP/LE for Beherrschen, Furcht is level 1 = 1 LE, so 1*60=60 - expectedGold: 100, // 1 LE * 100 Gold per LE - }, - { - name: "Spell with spruchrolle no reward", - spellName: "Erkennen von Krankheit", - characterClass: "Ma", - reward: nil, - expectedEP: 120, // 60/3 for spruchrolle - expectedGold: 100, // Fixed 20 Gold for spruchrolle - }, - { - name: "Spell with spruchrolle reward", - spellName: "Erkennen von Krankheit", - characterClass: "Ma", - reward: stringPtr("spruchrolle"), - expectedEP: 40, // 60/3 for spruchrolle - expectedGold: 20, // Fixed 20 Gold for spruchrolle - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - costResult := &SkillCostResultNew{ - CharacterClass: tt.characterClass, - SkillName: tt.spellName, - CharacterID: "test-character", - } - - err := CalcSpellLernCostOld(costResult, tt.reward) - if err != nil { - t.Fatalf("Failed to calculate spell costs: %v", err) - } - - if costResult.EP != tt.expectedEP { - t.Errorf("Expected EP %d, got %d", tt.expectedEP, costResult.EP) - } - - if costResult.GoldCost != tt.expectedGold { - t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, costResult.GoldCost) - } - }) - } -} - -// TestGetSpecialization tests the GetSpecialization function -func TestGetSpecialization(t *testing.T) { - tests := []struct { - name string - characterID string - expectedSpec string - }{ - { - name: "Default specialization", - characterID: "123", - expectedSpec: "Beherrschen", - }, - { - name: "Another character", - characterID: "456", - expectedSpec: "Beherrschen", // Currently returns default - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := GetSpecializationOld(tt.characterID) - if result != tt.expectedSpec { - t.Errorf("Expected specialization %s, got %s", tt.expectedSpec, result) - } - }) - } -} - -// TestFindBestCategoryForSkillLearning tests the findBestCategoryForSkillLearning function -func TestFindBestCategoryForSkillLearning(t *testing.T) { - tests := []struct { - name string - skillName string - characterClass string - expectedCat string - expectedDiff string - expectError bool - }{ - { - name: "Klettern for Assassine - should find best category", - skillName: "Klettern", - characterClass: "As", - expectedCat: "Körper", // Should prefer Körper (10 EP/TE * 1 LE * 3 = 30 EP) over Alltag (20 EP/TE * 1 LE * 3 = 60 EP) - expectedDiff: "leicht", - expectError: false, - }, - { - name: "Schleichen for Spitzbube - should find Unterwelt", - skillName: "Schleichen", - characterClass: "Sp", - expectedCat: "Unterwelt", // Sp has 10 EP/TE for Unterwelt vs 30 EP/TE for Freiland - expectedDiff: "normal", - expectError: false, - }, - { - name: "Invalid skill", - skillName: "NonExistentSkill", - characterClass: "As", - expectError: true, - }, - { - name: "Invalid character class", - skillName: "Klettern", - characterClass: "InvalidClass", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - category, difficulty, err := FindBestCategoryForSkillLearningOld(tt.skillName, tt.characterClass) - - if tt.expectError { - if err == nil { - t.Error("Expected an error but got none") - } - return - } - - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - if category != tt.expectedCat { - t.Errorf("Expected category %s, got %s", tt.expectedCat, category) - } - - if difficulty != tt.expectedDiff { - t.Errorf("Expected difficulty %s, got %s", tt.expectedDiff, difficulty) - } - }) - } -} - -// TestGetLernCostNextLevel tests the GetLernCostNextLevel function -func TestGetLernCostNextLevel(t *testing.T) { - tests := []struct { - name string - request *LernCostRequest - costResult *SkillCostResultNew - reward *string - level int - characterTyp string - expectError bool - expectedEP int - expectedGold int - expectedElfEP int // Expected EP bonus for Elves - }{ - { - name: "Learn skill as Human", - request: &LernCostRequest{ - Action: "learn", - Type: "skill", - Reward: stringPtr("default"), - }, - costResult: &SkillCostResultNew{ - CharacterClass: "As", - SkillName: "Klettern", - Category: "Körper", - Difficulty: "leicht", - }, - level: 1, - characterTyp: "Mensch", - expectError: false, - expectedEP: 30, // 10 * 1 * 3 - expectedGold: 200, // 1 * 200 - }, - { - name: "Learn skill as Human Kr", - request: &LernCostRequest{ - Action: "learn", - Type: "skill", - Reward: stringPtr("default"), - }, - costResult: &SkillCostResultNew{ - CharacterClass: "Kr", - SkillName: "Abrichten", - Category: "Körper", - Difficulty: "leicht", - }, - level: 1, - characterTyp: "Mensch", - expectError: true, // TODO Abrichten kommt im Mysterium mit dem Tiermeister - expectedEP: 30, // 10 * 1 * 3 - expectedGold: 200, // 1 * 200 - }, - { - name: "Learn skill as Elf - should have EP bonus", - request: &LernCostRequest{ - Action: "learn", - Type: "skill", - Reward: stringPtr("default"), - }, - costResult: &SkillCostResultNew{ - CharacterClass: "As", - SkillName: "Klettern", - Category: "Körper", - Difficulty: "leicht", - }, - level: 1, - characterTyp: "Elf", - expectError: false, - expectedEP: 30, - expectedElfEP: 6, // Additional 6 EP for Elves - expectedGold: 200, - }, - { - name: "Improve skill as human", - request: &LernCostRequest{ - Action: "improve", - Type: "skill", - CurrentLevel: 12, - Reward: stringPtr("default"), - }, - costResult: &SkillCostResultNew{ - CharacterClass: "As", - SkillName: "Klettern", - Category: "Körper", - Difficulty: "leicht", - }, - level: 13, - characterTyp: "Mensch", - expectError: false, - expectedEP: 10, // 10 * 1 (TE cost for level 13) - expectedGold: 20, // 1 * 20 - }, - - { - name: "Improve skill as Elf", - request: &LernCostRequest{ - Action: "improve", - Type: "skill", - CurrentLevel: 12, - Reward: stringPtr("default"), - }, - costResult: &SkillCostResultNew{ - CharacterClass: "As", - SkillName: "Klettern", - Category: "Körper", - Difficulty: "leicht", - }, - level: 13, - characterTyp: "Elf", - expectError: false, - expectedEP: 10, // 10 * 1 (TE cost for level 13) - expectedGold: 20, // 1 * 20 - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := GetLernCostNextLevelOld(tt.request, tt.costResult, tt.reward, tt.level, tt.characterTyp) - - if tt.expectError { - if err == nil { - t.Error("Expected an error but got none") - } - return - } - - if err != nil { - t.Errorf("Unexpected error: %v", err) - return - } - - expectedTotalEP := tt.expectedEP - if tt.characterTyp == "Elf" && (tt.request.Action == "learn") { - expectedTotalEP += tt.expectedElfEP - } - - if tt.costResult.EP != expectedTotalEP { - t.Errorf("Expected EP %d, got %d", expectedTotalEP, tt.costResult.EP) - } - - if tt.costResult.GoldCost != tt.expectedGold { - t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, tt.costResult.GoldCost) - } - }) - } -} diff --git a/backend/gsmaster/lerntabellen_test.go b/backend/gsmaster/lerntabellen_test.go deleted file mode 100644 index 5e507b1..0000000 --- a/backend/gsmaster/lerntabellen_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package gsmaster - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestLerntabellenForHexer(t *testing.T) { - // Testfall für Menschenkenntnis als Hexer - Lernkosten - t.Run("Lernkosten für Menschenkenntnis als Hexer", func(t *testing.T) { - // Verwende die exportierte Funktion CalculateDetailedSkillLearningCost - result, err := CalculateDetailedSkillLearningCostOld("Menschenkenntnis", "Hexer") - assert.NoError(t, err, "CalculateDetailedSkillLearningCost sollte keinen Fehler zurückgeben") - assert.NotNil(t, result, "Ergebnis sollte nicht nil sein") - - // Ausgabe der Ergebnisse - fmt.Printf("Lernkosten für Menschenkenntnis als Hexer:\n") - fmt.Printf("Lerneinheiten (LE): %d\n", result.LE) - fmt.Printf("Erfahrungspunkte (EP): %d\n", result.Ep) - fmt.Printf("Geldkosten (GS): %d\n", result.Money) - - // Überprüfung der Werte basierend auf aktueller Implementierung - // Menschenkenntnis ist Sozial/schwer: 4 LE - // Hexer EP-Kosten für Sozial: 20 EP/TE - // Money-Kosten: 20 GS/LE - assert.Equal(t, 4, result.LE, "LE-Kosten sollten 4 sein") - assert.Equal(t, 80, result.Ep, "EP-Kosten sollten 80 sein (4 LE * 20 EP)") - assert.Equal(t, 80, result.Money, "Geldkosten sollten 80 GS sein (4 LE * 20 GS)") - }) - - // Testfall für Menschenkenntnis als Hexer - Verbesserungskosten - t.Run("Verbesserungskosten für Menschenkenntnis als Hexer", func(t *testing.T) { - // Verwende die exportierte Funktion CalculateDetailedSkillImprovementCost - currentLevel := 10 - result, err := CalculateDetailedSkillImprovementCostOld("Menschenkenntnis", "Hexer", currentLevel) - assert.NoError(t, err, "CalculateDetailedSkillImprovementCost sollte keinen Fehler zurückgeben") - assert.NotNil(t, result, "Ergebnis sollte nicht nil sein") - - // Ausgabe der Ergebnisse - fmt.Printf("\nVerbesserungskosten für Menschenkenntnis als Hexer (von %d auf %d):\n", currentLevel, currentLevel+1) - fmt.Printf("Zielstufe: %d\n", result.Stufe) - fmt.Printf("Erfahrungspunkte (EP): %d\n", result.Ep) - fmt.Printf("Geldkosten (GS): %d\n", result.Money) - - // Überprüfung der Werte basierend auf aktueller Implementierung - // Result.Stufe ist die Zielstufe (11), nicht die benötigten TE - assert.Equal(t, 11, result.Stufe, "Zielstufe sollte 11 sein (von 10 auf 11)") - assert.Equal(t, 80, result.Ep, "EP-Kosten sollten 80 sein") - assert.Equal(t, 80, result.Money, "Geldkosten sollten 80 GS sein") - }) -} diff --git a/backend/gsmaster/levelup.go b/backend/gsmaster/levelup.go index 44af604..63179f9 100644 --- a/backend/gsmaster/levelup.go +++ b/backend/gsmaster/levelup.go @@ -2,10 +2,8 @@ package gsmaster import ( "bamort/models" - "encoding/json" "errors" "fmt" - "os" ) type SkillGroup string @@ -69,106 +67,6 @@ var AllowedGroups = map[CharClass]map[SkillGroup]bool} var Config LevelConfig // holds all loaded data -// loadLevelingConfigOld is deprecated. Use the new database-based learning cost system instead. -// This function loads data from static JSON files. -func loadLevelingConfigOld(opts ...string) error { - // Adjust path as needed - filePath := "../testdata/leveldata.json" - if len(opts) > 0 { - filePath = opts[0] - } - file, err := os.Open(filePath) - if err != nil { - return fmt.Errorf("failed to open JSON file: %w", err) - } - defer file.Close() - - // Decode the JSON file into the ExportData structure - decoder := json.NewDecoder(file) - if err := decoder.Decode(&Config); err != nil { - return fmt.Errorf("failed to decode JSON file: %w", err) - } - return nil -} - -// CalculateSpellLearnCostOld is deprecated. Use CalcSpellLernCost instead. -// This function uses the old hardcoded learning cost system. -// CalculateSpellLearnCostOld combines SpellLearnCost with SpellEPPerSchoolByClass -func CalculateSpellLearnCostOld(spell string, class string) (int, error) { - if Config.AllowedSchools == nil { - loadLevelingConfigOld() - } - - var spl models.Spell - if err := spl.First(spell); err != nil { - return 0, errors.New("unbekannter Zauberspruch") - } - - if !Config.AllowedSchools[CharClass(class)][spl.Category] { - return 0, fmt.Errorf("die Klasse %s darf die Schule %s nicht lernen", class, spl.Category) - } - neededLE, ok := Config.SpellLearnCost[spl.Stufe] - if !ok { - return 0, fmt.Errorf("ungültige Zauberstufe: %d", spl.Stufe) - } - - classMap, ok := Config.SpellEPPerSchoolByClass[CharClass(class)] - if !ok { - return 0, fmt.Errorf("keine EP-Tabelle für Klasse: %s", class) - } - - epPerTE, found := classMap[spl.Category] - if !found { - return 0, fmt.Errorf("unbekannte Schule '%s' bei Klasse '%s'", spl.Category, class) - } - - // Gesamt-EP = benötigte LE * EP pro LE. - totalEP := neededLE * (epPerTE * 3) - // +6 EP for elves - if class == "Elf" { - totalEP += 6 - } - - return totalEP, nil -} - -// CalculateSkillLearnCostOld is deprecated. Use CalcSkillLernCost instead. -// This function uses the old hardcoded learning cost system. -// CalculateSkillLearnCostOld: erstmalige Kosten in EP -// Then refer to Config in your calculations: -func CalculateSkillLearnCostOld(skill string, class string) (int, error) { - /* - if !Config.AllowedGroups[class][skill.Group] { - return 0, fmt.Errorf("die Klasse %s darf %s nicht lernen", class, skill.Group) - } - */ - var skl models.Skill - if err := skl.First(skill); err != nil { - return 0, errors.New("unbekannte Fertigkeit") - } - - groupMap, ok := Config.BaseLearnCost[SkillGroup(skl.Category)] - if !ok { - return 0, errors.New("unbekannte Gruppe") - } - - baseLE, ok := groupMap[Difficulty(skl.Difficulty)] - if !ok { - return 0, errors.New("keine LE-Definition für diese Schwierigkeit") - } - epPerTE, ok := Config.EPPerTE[CharClass(class)][SkillGroup(skl.Category)] - if !ok { - return 0, fmt.Errorf("keine EP-Kosten für %s bei %s", class, skl.Category) - } - totalEP := baseLE * (epPerTE * 3) - // +6 EP for elves - if class == "Elf" { - totalEP += 6 - } - - return totalEP, nil -} - // CalculateImprovementCost: Kosten zum Steigern von +X auf +X+1 func CalculateSkillImprovementCost(skill string, class string, currentSkillLevel int) (*models.LearnCost, error) { return CalculateImprovementCost(skill, class, currentSkillLevel) @@ -209,12 +107,3 @@ func CalculateImprovementCost(skill string, class string, currentSkillLevel int) lCost.Money = lCost.Ep return &lCost, nil } - -func CalculateLearnCost(skillType string, name string, class string) (int, error) { - if skillType == "skill" { - return CalculateSkillLearnCostOld(name, class) - } else if skillType == "spell" { - return CalculateSpellLearnCostOld(name, class) - } - return 0, errors.New("unknown skill type") -} diff --git a/backend/gsmaster/levelup_test.go b/backend/gsmaster/levelup_test.go index bbab36c..00b37d9 100644 --- a/backend/gsmaster/levelup_test.go +++ b/backend/gsmaster/levelup_test.go @@ -56,269 +56,8 @@ func setupTestDB(opts ...bool) { } } -func TestLoadLevelingConfig(t *testing.T) { - // Save original Config - originalConfig := Config - - // Test invalid file path - os.Setenv("CONFIG_PATH", "/invalid/path/leveldata.json") - defer func() { - if r := recover(); r == nil { - t.Error("Expected panic with invalid file path") - } - // Restore original Config - Config = originalConfig - }() - - // Call init() which should panic - loadLevelingConfigOld("/invalid/path/leveldata.json") -} - -func TestInitValidConfig(t *testing.T) { - loadLevelingConfigOld() - setupTestDB(false) - /* - // Save original Config - originalConfig := Config - defer func() { - Config = originalConfig - }() - */ - - // Test with valid config file - os.Setenv("CONFIG_PATH", "/data/dev/bamort/config/leveldata.json") - loadLevelingConfigOld("../testdata/leveldata.json") - - // Verify Config was populated - assert.LessOrEqual(t, 1, len(Config.BaseLearnCost), "Expected BaseLearnCost to be populated") - assert.LessOrEqual(t, 1, len(Config.ImprovementCost), "Expected BaseLearImprovementCostnCost to be populated") - assert.LessOrEqual(t, 1, len(Config.EPPerTE), "Expected EPPerTE to be populated") -} - -func TestCalculateSpellLearnCost(t *testing.T) { - loadLevelingConfigOld() - setupTestDB(false) - /* - // Save original Config - originalConfig := Config - defer func() { - Config = originalConfig - }() - - // Set up test config - Config = LevelConfig{ - SpellLearnCost: map[int]int{ - 1: 1, - 2: 2, - }, - SpellEPPerSchoolByClass: map[CharClass]map[string]int{ - "Magier": {"Beweg": 10}, - "Elfe": {"Beweg": 15}, - }, - AllowedSchools: map[CharClass]map[string]bool{ - "Magier": {"Beweg": true}, - "Elfe": {"Beweg": true}, - }, - } - */ - - tests := []struct { - name string - spell SpellDefinition - //class CharClass - class string - wantEP int - wantErr bool - errContains string - }{ - { - name: "valid spell for magier", - spell: SpellDefinition{Name: "Angst", Stufe: 2, School: "Beherrschen"}, - class: "Magier", - wantEP: 180, // 1 LE * (10 EP * 3) - }, - { - name: "valid spell for elf", - spell: SpellDefinition{Name: "Angst", Stufe: 2, School: "Beherrschen"}, - class: "Elfe", - wantEP: 51, // (1 LE * (15 EP * 3)) + 6 - wantErr: true, - }, - { - name: "invalid spell level", - spell: SpellDefinition{Name: "Angst", Stufe: 99, School: "Beherrschen"}, - class: "Magier", - wantErr: true, - errContains: "ungültige Zauberstufe", - }, - { - name: "invalid class", - spell: SpellDefinition{Name: "Angst", Stufe: 2, School: "Beherrschen"}, - class: "InvalidClass", - wantErr: true, - errContains: "keine EP-Tabelle für Klasse", - }, - { - name: "invalid school", - spell: SpellDefinition{Name: "Angst", Stufe: 2, School: "Beherrschen"}, - class: "Magier", - wantErr: true, - errContains: "unbekannte Schule", - }, - { - name: "not allowed school", - spell: SpellDefinition{Name: "Angst", Stufe: 2, School: "Beherrschen"}, - class: "Krieger", - wantErr: true, - errContains: "darf die Schule", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := CalculateSpellLearnCostOld(tt.spell.Name, tt.class) - if tt.wantErr { - if err == nil { - assert.Error(t, err, "CalculateSpellLearnCost() expected error") - } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { - assert.Error(t, err, "CalculateSpellLearnCost() error = %v, should contain %v", err, tt.errContains) - //t.Errorf("CalculateSpellLearnCost() error = %v, should contain %v", err, tt.errContains) - } - return - } - if err != nil { - assert.NoError(t, err, "CalculateSpellLearnCost() unexpected error = %v", err) - //t.Errorf("CalculateSpellLearnCost() unexpected error = %v", err) - return - } - if got != tt.wantEP { - assert.Equal(t, tt.wantEP, got, "CalculateSpellLearnCost() = %v, want %v", got, tt.wantEP) - //t.Errorf("CalculateSpellLearnCost() = %v, want %v", got, tt.wantEP) - } - }) - } -} - -func TestCalculateLearnCost(t *testing.T) { - loadLevelingConfigOld() - setupTestDB(false) - // Save original Config - /* - originalConfig := Config - defer func() { - Config = originalConfig - }() - */ - - // Set up test config - /* - Config = LevelConfig{ - BaseLearnCost: map[SkillGroup]map[Difficulty]int{ - "Alltag": { - "leicht": 1, - "normal": 1, - "schwer": 2, - "sehr_schwer": 10, - }, - }, - EPPerTE: map[CharClass]map[SkillGroup]int{ - "Krieger": {"Alltag": 20}, - "Elf": {"Alltag": 30}, - }, - } - */ - - tests := []struct { - name string - skill SkillDefinition - class string - wantEP int - wantErr bool - errContains string - }{ - { - name: "valid skill for warrior", - skill: SkillDefinition{ - Name: "Bootfahren", - Group: "Alltag", - Difficulty: "leicht", - }, - class: "Krieger", - wantEP: 60, // 1 LE * (20 EP * 3) - }, - { - name: "valid skill for elf", - skill: SkillDefinition{ - Name: "Bootfahren", - Group: "Alltag", - Difficulty: "leicht", - }, - class: "Elf", - wantEP: 96, // (1 LE * (30 EP * 3)) + 6 - wantErr: true, - }, - { - name: "invalid group", - skill: SkillDefinition{ - Name: "Erste Hilfe", - Group: "InvalidGroup", - Difficulty: "leicht", - }, - class: "Krieger", - wantErr: true, - errContains: "unbekannte Gruppe", - }, - { - name: "invalid difficulty", - skill: SkillDefinition{ - Name: "Geländelauf", - Group: "Körper", - Difficulty: "invalid", - }, - class: "Krieger", - wantErr: true, - errContains: "keine LE-Definition für diese Schwierigkeit", - }, - { - name: "invalid class", - skill: SkillDefinition{ - Name: "Test", - Group: "Alltag", - Difficulty: "leicht", - }, - class: "InvalidClass", - wantErr: true, - errContains: "keine EP-Kosten für", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := CalculateSkillLearnCostOld(tt.skill.Name, tt.class) - if tt.wantErr { - if err == nil { - assert.Error(t, err, "CalculateLearnCost() expected error") - } else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { - assert.Error(t, err, "CalculateLearnCost() error = %v, should contain %v", err, tt.errContains) - //t.Errorf("CalculateLearnCost() error = %v, should contain %v", err, tt.errContains) - } - return - } - if err != nil { - assert.NoError(t, err, "CalculateLearnCost() unexpected error = %v", err) - //t.Errorf("CalculateLearnCost() unexpected error = %v", err) - return - } - if got != tt.wantEP { - assert.Equal(t, tt.wantEP, got, "CalculateLearnCost() = %v, want %v", got, tt.wantEP) - //t.Errorf("CalculateLearnCost() = %v, want %v", got, tt.wantEP) - } - }) - } -} - func TestCalculateImprovementCost(t *testing.T) { - loadLevelingConfigOld() + //loadLevelingConfigOld() setupTestDB(false) /* // Save original Config diff --git a/backend/testdata/prepared_test_data.db.backup b/backend/testdata/prepared_test_data.db.backup index 89e3f8e9bdf47d7bb92d4473e469a4f7ef102772..5246985e8943dad006e849429ca2d63dc5e24d51 100644 GIT binary patch delta 147404 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 delta 43910 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%0Ds+xQsQCNHN?>dI}CVcjy*U#2hFr^ z;p;}U&wzh?FS8N9zKOLiy!?(j6+O_27+XsUe^@1-kZ+YI4JQoO8zu}*hG^-p(*4q< z(ncv(|B&vVy8CsPXdAQ^@h{@f#jC|W;X~n3;TysZA&38zf1E!IYQCM<=B7viq2uqI zhjIz`Po0PI3H;(bR74=`+!YhpcfRud^7Bz8@&D?4l$$vg2l#pOiuzbA0VUlsLni39 z0$W%AG_1bih8viA9DRW56V5)MNNvXflzkR0j}TtV{NVV6eMSq_esG-rl$s&^^niMX z;&nSFU>-FzH8D8a_zH*${*=W{ij!di94jr!9~?l5F3wIsiJ??zPYi2P z12glpbG97lC}G&13#|$Zbl+qBK|!=&wU^$*Y4Kpr-S)7CXlP|_&n0Z7a4OtbBu2Ni_Q zyHML=cfe?Oc6NFOMuO1R&DuzZ4j?SP4aSL(ONVpL20yl0A_Hh636oGwfJGXY(4u2! z$V4y6UZG73tn|a^)K=P4Y0K#%N(M7u8zgq@!MXi&HYceCxrN&Gpo4@@`0XDUCNq3k zdS!E{Wa&>%6o@yKiuaiV$mS|Hf}hrKXhA~4T@9;!=K!_NnSDd{#?O>O3M*K5r%YC; zJ?p6IEEY=h{`nbY+h9dbUU5-gajteA8g{a5{{Z2H=9EZz($+n9?7_itm#xDNYr7L# zx()*QfFTkL#Ak+y49FY?1PoPba9j)9h)`%w+a&B^Oh9g_ykX=`hR?PESjSNM8nWCw zqXp`vMR{ep+5sq(_8BO0QWmsO=(59XFi(|Xnd&isD5^FQJyS1=V$1x@B#0Rnev~qq znC^GZ<6So~X|e1Ji*l_5ktSqAeN^Gk51_)fIz9MlgT$*B<+GX;I8CR0erC=w+wZgo zt3bAKF|lkNAScm6D!V{wt#)0Yv_6+zpw#1^1>hSOpcSM(n_bHH78lBMxf96#M5=od z*b&3>0u3@$Xjn`Ku{dENj_9Iu+%`)}WY*R;Gj;6dbNb0#rVu>+ld%6|8||Niwt!02 zHq;N1?8C<%9G#$UqNWZ|eo~nI1GY)~=nNUG59W@~+4?7DN!^%*DY%o=AN9y$_DVI7 zJ=!eby)LEE{Xu~J=Upg!4e9e!45G@gNq$>q46TM+<-f>{h7SyXFA-zHBR?lxRxs4~pRCPS)0Gz>x$c}&7LKa3ob362VcS~aYH1Qohu zI4BU-5Sg_aE9#>AU8ZCq8Vs7(#0zqGOJfOgTk%PQCJ}#E)|gSAf+ENo1AbZ7q^ehCV?m>7 z(XH?_W$~7h66DLnw_S;HAwf#?A64VM8<{w4I0QL3>kvxER~51~?&O^pemGxU%LhspiKHD>sqt|rQ*%kQsi^4MZZ&9Z1_oxn(&?1p=^BNVPwK>*TFHe z6g+YrYJtOMr>(z=O3A~1ewwJ1 zO~%9Tst+$PPa_>2Tt~gA3|*^UROTsjFQT=i^LRMm|NKRz6Ulu^nb%Cegw~PppTDGZ zK5xE+RuO;k%V-;c-*{QU|NYCT{CnKfym5hh1wAI?-@L1?x#K-#e88w4kx4{Hih2`) zKTA>5QT@v)>g)xsNqtg&z^48vMXy2{Mg!fhMxzoR=u~%5q)mNlI?HnOsgqox9p#o8 z`|PZl6=11_bdZ5D>&7V<51E;WuO0oP{S&bo_?i$FcgyJ1%(%lTtKmB=kA%{QLGXc& zJc9#a8jn;{VS(MF{$mmT^SkOynjw4cAZdqWyy%v$D5I*)YDG~zX+x#H}e>l4cqXJ>P}SgJVNr?!eEigTT_ zSu9qZZF|>SYFoV&$j#o4xZNK6ViXLpK;inDoMFD6iD)DB=cUUBw}w}@87**(`Q z#wpIu<_yuIINPVzi?NDxopVTxQJh3npll#_S5({VM$x2vZl3QHqp8!lV{W||r8s-W zcZx>E**&*Oloe-ZbE{}job6LxqNF(2IroTq#o4wuQ`7~Vvz?+=aW>DliXwH&JLW1x zVYlKYMb0bE?zwd$r#L&CJ4IG;woi47jN)A9Y!x+%vu*EAQ5|s3)`>`QHqVz4?XEN= zVE$1x(X;PK3kgrD|A91>h7X|?A)j1^g%39(3Bg>Bobmmo3QqUFCBG&=CqE^V)t~!h z{L8P^^$#4>Fh=7t;YFIJ1?HgUOSwq+fl5|guhL~}RYDy17Pp!CZ>CuNp1K#Ms;)QO zWysby=J?@0o~Zf98X0VZ^_I=zJkU61@;u3 zFearvOu${G^f4D{G+Xi88qHm(6ko?^8n8*LiNh{dqsRYZG-X;(LkVvwD1g-P!>lF~ zIx*UveadQj@c^f}0~KSD*R-N?{64SA#)Ui}zFf<4_#3U+RJw|{Bu*E&0U%V zR6}Gjf*X1)GZD8XL9$v+1F%*__CR2EyWcrAGub}@s{wQNiS}q3e`HG2 z)|e7Se{O^e)SPers0qZg!2NVz94^BJ_HmF$`i7gS0x}p5Cav!Y&0n!P~d}!iHas)kP!S5 z&FYO&OCv?GM&r^iM$srQ{US58+6!v=Zz}mC`ENk~c{y8B^CX8zEvrtJs(!15v*X|u z1T(56&5c_Am@Mahz7SQ+{6of1r)U_5tY$O_T3QH3@=5Z0u6)v9l3tO1EL|+sN-_E` z^?%bB>OR)}RClw^rOVLiwAX3R(Qee5#Fxce#ryjVSQ>_CrS4`~6@&1)VWyC8aW~P_ zT*J&tzRcZ7iT!GrDdMZ#D{0bS3^QeXk-LF_ zgUQKBc9#?0U@2J=B|#EIv}7qYTAkr8lT~F1I~ z3GzU{Gitj#k76Z^Gh3RI+$)GQNi0N|bTvxd)cr^-;r@HZxz3_j0olU1t_zJHR*jw*Ji z$f~-Wp)HxX!0{JF}f-{X$a*8{F z_(QxQ6uuOV_PX01ILL6u$|EZ1wC|HImiy!?nKe9Pz=oZMGU;>aS?SwSqhx^{!prn~ z^tJj}-8;I+by!!d{Ze}bYRq0Ox3)K$>eltlYOZ8^G%5HuuV)H4dk_7das$)P6>p>8 zFTR1v=hC-Elg$6@1}25e+7hj@ApFV=Od6NgO}~pbGRa&dyv%oeVCLo}(naIdGF%W9`_ zo+5E7*GCgQ{`4lMl*?L2zZV~d@3m{AN$u`E%v5sKZIsrvhXJ>A4UO~6VWyBPZl!MS zX2?s{YU-}J8E~^!1>Nav3r*(Io0)VjtC@a2N~1JU_e(c3Ip5>%V~ojIB|k2oC)*6a zGK?7z9L^n+46yq;rccy8tHU7QSsho~Xe5oyl}`XqZ7Yo=KTkfvtl~B_kb=jSkOie1N<6Nts{<&AI!@b>HJKBi*cXA2% z(Qa;)x^XpLS;>}a+)=Ig2Ni5RT2BHKabpF`q4)>L?GX7x?r^Q8;tfcicHY>xx zW}H&PCZRRhwUUj&T{Ubs-cZk)@e?)dEimh!sAZc`D}JV$jl+BDSP5TI%hFLyuGX91 zNS87$(%{?bSlE-q$Lm<<0$|Gst;B^*Oa)p+Fs=9p4Qz$hYi^(?Dku>dB}n2FuM%7HTF#r!CqG**ZE*`erkZYk^@>qcSGj)WU4XFjm^Qz^`I% zMH)AA+94e|$A6in*{xZFGb`8%)Q)#nupI$$tz_1DEvxXsN;V&@#fPJXIDB^x$1fbO zWIGVB2RX1{M>QKQ1+G|4!`9tw8_7}%o~ma5-GFf`VQ$mH%vz=vUE9Pm3(PAFZ*QVF`1A%!KO)dpGmyY!fJqvQpJ@02RwKmx0ONB@le zLH(`zZ|W!Y9r_}DobIIV8Qonvug;;{psUnbwV!HV(k^JP);hGSVgKs`@k#MM@fzsh z`^0r(kr)T(*8eU%A>8T`E)vFsMj?)WpZ{uh!>+sv+DCFV2cMdnw`UCg(bIc6u*$mB43%}1I?G}mb^(DZBCH5uw> z)OV=QQ*Tgb!ll6v(NpNVaBE-_tR|7+3HeL&#pyJpdHdE0YjS>?kL+$DGU7@Qr0(e% z*u$_#clGauJ9`cx09W{8NhJKI6IK{(_+qF>|2_;hvV3OhsU)5>pNV?*yw4=p#QLIj z&`?Q`F!X_>1B5GCvvbi>tS>4&00y-5lzX(4;WLr|{ImC=>(2MVrYLw`d!NbSb9@Hs ziT(h3qa2?^Ju8W))2FANu@9IHe1=a)Jr94t*tVE_S{*b>0s38VfZy(n*2nuq9W+mY zubsma{jkI1q?g5_1&dG6LEjJv+(-_*&z^No^Uu+Gt4|${LJmth z_d3XLv^Lg>*9IPW=> zRolmmxjM!>AI54A6wwIm2SstQ-nlUOkb4)jOrykj?=0oX{V&EWMhR)&8LFa}f6OHF zS>Cg$=g7xIlfBc_^CI!&c&Dg`|Afh?iS;_e(hg~Ils?uw86FVSQ!GLNmFGTBKc??{*=6y=T5$9soqUiv;| zV(Tnk`w}tE!R95|I*k%7-l4FNj`>-~evpUVN%$HJs&TSNyaQCSfBqEM%<=Y9W;CAx zXIAeXqB^+fGbSc2&f6EJ2Cc9KXSc0&ObpwjblKjsNO9oN&zSV8IPdPTcwy=i1?sXZ zQeC1zU3P}l#T5nWvV+Jxpi%*Ki2`-$4a?1e3U&g>tTv3dShJ zdAEfH_4LEOIh?46Q{6^AWO8d**cLcx2ovln*l{!p@!l;#6^iB4yxmkYZvGbojiKWNc2bY^B-Ou7)YEv9G@jm#)Z-u_8Qu;>XHGIU zTda2j@dWGI;OLCq0drxakm7CEsY(!j<|LEgiubN3T!zF4jTh9^*;$7ZvS;$H3lo*1 zPe#zEwP7er8bhOC@wP3Idr-fOLWXw@(V_sQ4deiU?Hs)NLk6bUh74~jkt4q3bI_?Q z?`rD#@#jpME5W-;r^*eY(f!+Fw-3)L913P{3(0;U(Ke+`H0l$)&0$DuV6)xsoFtnu zPRF=WNb)x6R0TmKCDaDj3gG51DOJJhZ49-5eS#F;1RBK{@5->IB-rW^EolhTgfKNW zf*RLHs<9E&xGto|HY2EUZCFMEYHS2Gt|8*Y?|;FVTqbXISaB$tYXr@$3L_k*xiS<+ zWmsVxyAh6{OpqH0vJmU72vd$w!$$(jmFywf|4z@){b5R4}K7*8;_ReE?C8q9*F0g5KeAoF}B ze+skYLMEJ&fc)vzCU0JttQ6Ii^>N-6L{_e#62f%WK1-{)ESkN!H0NYWDM-OCS&sGQ zgb5_{S+BNwvqM@Ugf5Viu3DB8yl~zvq_j)*57jT7u^Y`^8xeS@X48ID28~Jy(}~4~QHF*wIZR}0kHLb} ztS^qNUfr)VFYrdzfneTtyGT#YF2p6UZ!x77$)%ECf@2AH$Ub>Y-UOrKcv)rm8;pvN z!l?Kl9Tn#pVx*I>tn~}&HpvA^tdqD@Xxw0RWU7EoawS9CRuts3r+*kuyiCBl#Eh+- z95^rwiO}5T1;gQTqJ3g79G2NjBbwPbX#YZ@gix~5Ns=%NR%i@EL#b{!)^+UWV2~M_ z#-M175~v!vsA|^Opkai`b7~=*=(0e&7#h^kqgCVplHDxCL7NyN7!dnAnH>dD!(psT$45g)1cjbm~v@Q;s%F{6b zgG6X1Pm2_#i-V@}R3t5Q{DKwQN$Acyi`W=j9JGn2MkCFo4H`s6YVl1*J`tM1 zHJv%bRzrosEPW*XmvltBR@y6Vffdv!{rfQWI-F<6u&RJMZ4H6#tUBwe-VBw+$CHhObP3SLLmyK_{aJC z#`vrGeP9Non2+K<a$GKB2x>{Y`bhx=mfEHlX*=33MMigw93lVZYmWRdXVr z;A!VUWca3ct(GjSXFX5)bRlV*eP*v?28L(R%1x8fiagIc9+ul63_rO+o8Srtlv_P( z7m;S|a8V(IRA%+GEryZ9Op9TqiJmn)Q5s4NW;`P>4G0TP_O$Y(jT4fVUU&}Bb(Iz; zcvchEAqMGS0ymli!Ii~H9&S7?KQ6^WjzfZW4x$OePUVm)9-gUUs@0ZRht71%5fSg5EWiFWk+fANJG zpFhZoe1@l-$OKH#AcFk()yvq^li2W*kRmP3=Bjn`&*vUoD6 z6%zax{MCZbx3dC70P`3S;WCH-)fWtUKpT3GowYz&fNc)o@ku*t+mq-?3y4cOkZYfJ zl1qRJ&B|)42W)o`X(cRP5DbJ>C3tL#hCmpcV;r7#Rc(1OH{vdOME4{Q%A`bSiF045enylT)>*e4{-At-S8 zE#*KsWZDoYvp_i2|6pjGM*_oBfSt6)ajr7$rq{trN~_{L`XEgUQF@}B0W^@6jxdg& zhP%n2UD{v`u(hRn#AP*K!L9-o6HZYvC9D#Zhz}}J9G-H3TIYE<(!XGrgU#l%JS;8s zdmL<9PO^s~3J+Ni(;r(-cdn7Z9dlie;_w=N%px|Qg@fNI;lV{BY=touNq0qL=i&h}62SBO^> zC%exNaHm`(?w^BREQ~;Ts{1_3oo*bsi+ArQdV-6`*;w%Gqn?f6$%uEK8_*y)#!bvz zgr!lE?A}ZHx?~(0g>3gZysCl713!AJXpRk!nB$)3RgE+Z4X`oS-X=0~KupD1?m5z! zV9f+nr3CjZZ8!=hSWB#xLR0|ZJ10R065YeJ z*pE-Laloyea{JaKaGUHNqNN}?K|7M%gEXYb2_cE@0gBV<1RR^YpKyz(oou2b$-Rd} zgTh*DXT$ieNOSio6)B0{m4$3i+HG6++)w%B7 zN_i*YSHMhNZXlqvJl4G{*gD!r;EqKQqcYjOlk(j#1(~qAcaX}D`=;2KbgR2J&@#4w z0>UcqBAuEP_jX!KzC8sJN^$p4IXp-_neJ`0CciwzCTr(C?PO?uh2BCYbSY#)_r6w{ z&?Q{P{zIc@3|mz)ma`1sH*AH#R>Q*9!|c6C&&VfKaH``|`9=8ze1Am7-)I*k{KCC# z&H{5E_l11*ql}J`UsZt>#DBrHwT1UT5YjbAY_@y#3$LDFQ<~ODY^%zos#mec;0od0 za6fak?pxYdL{2yY1_ZmfTbLwnon|+3vFE6shiHw!Cj7Z`dwEN*JAONF*;IqvHMnyt zXTt?sxJ0;xN%zS5k^~l~Y~utxyM-$fdU(rLh}fz`{M{BV1?`TEXxhq^N#31Xc}qQs}J4q%<32|73Jp5EI_pJoTQHiAyj3s7MT^t;;t?0ykJGhN# zoAU8bJGgpb6Ud~I$fP(%NWl#|xpX*Q1vtCZn>XUKcX6G}7H>}n{?#sSCF)lAeq|S@ zUC7(b&BDeyzWFR}9qJ-`Z)s$|G8!lKaYe8h*U`sq#L+8R6MnCRO~8-$aa9_3&3b%O z7uSh45ejsFGa4T#R<@v0q7ywaP z7F=cQchDV1uIY|x&IanX!0Z9Nd*Dir>s;X8<4&d=;>bQ7(ZyT9VdTG%vn26*D!uA} zO8+YN4EqI}&#ckxRvnO&^ayN2Sq!P-@5SHg+x=0t9{IxEeITZ-=#nIQvJSu6!)M^^ zt-Ori>)~V2u)@T9JNQ|(a29VluLF6aKoR*B`19R-H2&Uh-iGW-o^3mL&fmEy3ZLA~ zcY*qKoW&2Ip@^^pX93nN7D2{WY!5__@8jdpF5KM7S#ZM^J{~{P2e%Zy+|J7+H8dEJ z!oz)hi^kox6YuQgcA|lZ&`Ucx9e!jBUk5r{ycyJF56uCF94O5FU^DkFJ}(}&P32vI zPb+ru-=&NLb29Gj;pZ9Ya@90mzm-dg=0*a??`ns6%g#3B?jv<94!^P);u=XU{COfA znRo9U#QEL)1R4u5l!%}1=C`OP2kzL#%9UbVRRz;yoq68a0KU%P%`2`?y6XLc{ z@Rq5qxUZ9o!Nof{2_LiwF?fBfkcH+5%^c;!y|F^AFgGq(+-NH-1;+|@?A`(bdM^r^ z9-9?VY!-p)QnPmd7(`>YB2Ol%>_zxW3uztWgc39pmf{t0!X~YI)WKW!LewY{wHUu> z7PR;Rvyg$#4n;NM7tBICKQPK$W`TBLjBpm34h!vz5k!0PP9LGw=ki=7w^3Fh z1HatKXCNmLX%c000b0Ujjqq>uCkaiGb_VV_Bnf7sRi#>YtDU!ufWA3G`WBBbNP;A1 zwsT1&VoFdE3dqUPzvGfFzBQ~QZ-kn2I*nYV^Ju0(ToP2}Dk6h8+!Q55;hT+MWZ*Zu z6$wK#93>2jJx<w5lO0o4PV#X(InVz7mRn<1Y6)XHaXv? z+P9$JBfKw1o$FZ-Hv{0*-^b+ZdGg2D_NcIZVR%}&S~l+|MhGNyA}q#%1^q$cEm=69 z1jV7!2W}C#X|Q}DKLMu^ru~LJC1*^xo-y63n8G|`x>b?G8Plzz@IN-)y71=|vB`i` z3yFo|Uy$npa_ylGY+cIxT_GRhXHPx9@jhUUXu({M2-x&*=iuvF( z7B<6bQkVffWozS%h0R4Bln8=m&ISwN4lPK!TsiiL@R z^w-Wngb35n>9~DLOq^P3shL_$Roeek8ra8@AR=Lkbb5T=8R_VnI+Y ztENt63&(C2R|NT9=Y$9mmSfYgW3!m#T9)tilYkRpKRjstl^)O;CZ~sI$UZOSy&`h< z5hjCm6Q`k1N2NHkON=X6mWfs4r^(s=uEm_SjX{KnBC`@>-T2im(dr7Sm*Z6W*1)KE z>CUi{mB_ibqNLTMkjgR-N(8os1ASl6>~T_X%DR38P#YsnQxi+g_^ocyH~=SHPO;Ej zH4He*ypPaLx0M6bA`SO8+G%c=t})<;w}_{==-o5~(c!(AB{sTo-c}GFFAUkRPIzttPVFsM`*x zk#Y|(+ul!3xc1M(`LGbO3+|09zP;1X1K1HwbZ`PpZ2jFn3N|dAGxNhC{3Yo1+W>tT zo8xO{;20m=DIny-uw7x)Yq!#DFD=$!LlZpL5vaD2=Hu(P(0rd3SF!9z&(bN_C5r-g z1s)yhULHN{Kpmjd^K=o0`s=zV_R_g;fbpmc4T4*`Xp+@l%zxFiP*wrk?&^{otiq^J4^kp7M+5vYs z=-UASQjxgzYfeKdkRfF|(F++OdJU~6tEPli4|0_D! zyA8a-LOP%HrDK!kRe*m=yS;K4-k@5B;>s4lh- zBekQp;ossaw-^GrnWq$PJs?Nw20nX@7`qnwsM5&jWNs8IKC6-+1Y;I;hLdpFWYl1h zej~Z0I{jz*qhQb?P4^ewb-H$)8tlUlYZJt0k3`vS7Gql7-`?V>dpZ+%1H(b(m@n;RDwrXH+gm=f`2?RD&_|#J!{?MagB%~zz>d!MZsx7 zlbgJ#5i!&V&ds)f!C){dL5`~a8#bEU>gAK@24l{|`fxCb@<5lL9z5M%OtaGLCJ${y zuz}xph&jQmw7TJR1_2T4TqmBAzyk3dtQmxpE_ zw*5*+OC`2)Kco;b&&RF_F>_a-`Zexbp2XsL=?D_x6k5(bHJiA4Qp{BscjM0=(HR>p9}^{!!}#D}yR5KBuKm9n?LiO(p)bp3_p(=$8ci zcR#144?eu|oHmbyOV4Wy2&{izOP@>_dtRGJ{NH$9+eF~+p4VmpT=?L5Z83t!H^QFS z*vx?4xYj>wqe#5%f7X_hc;EW7ww^L_`~@w2Hsp&J6hgT#YLjxvQy0OfGunD*|K?Fq2i~If!(|=dy<|Z&5`X9BimA2W zzjQ&g5&wLzb02wY=&}W|nE1!X#`}xGfBAygMEq54?akzQi*GH6oy4ElJ3ZS5{wo&5 zJ;XmeJlQ@0{wo*6Y2qK+m$zmN{8ufA2bJ*lHj@6;3*wc;zpbNYrU?B01@RW*FB_fM zI1TcP9crf17O@LmtuC;pCeM(ta{e*%W%Ktzht!z}ps>D+&D0 z0&@$2x5E_y0>8V!JWAjlV4j)4@4<0x0>8h&yhq@jM;QsgU2u4tz#l9yHUfXRz!VdB z_X5*I;5`dWCxQ3E32*}M1M&p^2*?xoV<1o95g*5>@&rBt_0*HIOIpH$a}i-vW68A3w^-0DcGL348*`6L=iR6Zj;MC-5mCPv8k4PvFx)p1@~- zJb}Ll@&rB$;}u|pI$i}%sN*$Yg*yHMyimtqff?%f8*oD%e+PD`<8@$% zI^FUbO2p^kTe9qM=&*rAU1fF0_1AK0Oe4}cx& z_z>8kj*oyH>i8Jgp^i_09qRZL*rATkfF0`i*HI>#uyYdFp^ndi9qRZ3*rARufgS4j z3fQ5JuYsLtkh%)EArJvG1ge1-0yV%2fpB5rJ_1=_gg_4XAdm+(2o!(|0>z_@34j)O zAW#P^5U2+Z2$X;U0u4alq_zqBRPs;dF=-#nNyh(o>~*_Ljhqe6i>Pc@HJc6XCWFdq zO-IE3cdixZ9yB!@+M~fjp4CFXN!En}2TdNCy%~->M?;`^1ja$03A7$%%mCsc&jcnwo(W8ZJQJ7%c_uI!@=Ra~h@3zhh@8Mw5IKQqK%T&K zAWvWhkS8z`$P<_a;5fGBE&F0C@uQfINZuK%T$?AWvW+kSDMR$P-u$ z>FTJb~pvp1=wqPhcgGC$I|06Ic!839LEF!~&=V@&wiac>?Q!Jb?{B zp1_qrp1?*RPhb;}C$JgF6W9Xe30wu_30w{232X)O1g-({1hxTr0@nh00@od7ECAL6 zc>>#kJb@d4Jb@iRp1_Sjp1@5&p1@8ZPvB-CPhc02C$JmH6SxJ)6Sx(~6Sxh?6W9af z3EU3k3G6+}!~xg=)K4Jb^<% zo!y0mCOH-mBH%&zg7nE`;S;nt{QP%1<#y3CI18-hW&;-BCnN;^cffr{DCwlHNz|4G5XK-ufn0kd-Xp3R(+w~r2A6$SKWiU+rX~d z*}5KGlP+Cn(0-`>llBqqceEG4rd)$ITPwl)$uEgN6|V%6!M`K%wl@T@Kdo3=ee}0h zFz)M9EUo_iw^m}ATCucheB7Em9PI;3gG+4}o7EN{7#Rf1LCWZ)Yia=ANrbmf=ggwT z2Q~&nA&Yhv&1#bm%nXK1Om6^#kzlP*o8<$8gJ5{(+mBn*Qeu2yWbhR8&}J#k2POu= zw9^Z~kjv@|S`{R(IfUDaHcN3nFe?}`v=?S!(k#aMz_cJu8;qVv&2z9HGv^pL>i~zE zT@B11ynYlP0*b@w~9%gRWq__CMxL_zL*qSEqa)KcglQxs{fn`Ckgp~D!HMKI< z2aC*Lrb*!>WZl%HHThtTIYg0IaRFo@&Id+TLG%Zou$sXdI2c)l3CN94Scy4tu&oN7 zUp`?a=ET98DtJD6!kXcV^@STy4Ow?K>0^ChKs7`gw!0K7ri6L0pBiF5xDQ5{xA?%A zYA9q;iAs(IAsh>;t2!U{(1i$F2E%xett~g59yVj$4b9 z%|0-o8Y-E9m0Xit<^%Joknj2@twpw29~evx@!dQ=Gd0i;dSTL)`&N(ueEE~s;;J|w z7)cE)oRDpA6X*ySN?ocWCNACwMp6SR(rE$}0lTOnMgmrDO`sxR4>cr!#kykROg=D& z8WMn_A|@`u2X;}xCS2cBR?F5nA6P;Sv80^upe9|Tbrv7kLk*=1`U1zWNFy0-jP-$W z)KG3AWE39jg#aPZ2j)@1c-x7mtZ{s@4=kdBp`dr4vc^M+CxxMelqy;<`x3)4wWS}P zLMADhd2|>gR;njL#aDiSFrnV1}NMi5AEagmaJ)?5sW~KOi6a ze`Wg8No-%9F?~r7iJUQgdB*hR=oG2A^q5gi$r;m^XG~w7F@1T)^rd1tbF?|VxdSa&gKV$mxWou@oRsWR=UL}1IXxF{QqCTORZtHY9fh&JkQBJ|6bec6fCW-wm-;kDMMIG+9-*aDr#6B0{Jei&tlYp;ph9fW=en zd&e3L*0j5677xE;wScFKdL9Cg%j(%2h#9(EpbXAR%D}#Lr@~*7QcU#nKqxfPvuSA| z(z^k{V6cL}acM9u<3Mm_iOthNMY;W55M`2Q1GO@H{<~J|0GYQHFdvZTF&>1s3C_Ih zdgfi%Gw-_o+jm{Hi|@K(@{TV4@@uPD`+JpkK-(nj6TaEcC!$77LXmrqOqSA`)$w`!N_QW9P$rrjeP>)%NUjdS7r*9~)5#o#ZR`+`H0PDg5A^tH^I_rX^6Q?st$(P2v P*OCWT7sj;l*RuZyNEwan