Lernkosten altes SysLernkosten vergleich bugfixing

This commit is contained in:
2025-07-30 11:09:21 +02:00
parent ee253bc3d8
commit 9126bd9f78
6 changed files with 749 additions and 79 deletions
+5 -3
View File
@@ -714,12 +714,14 @@ func calculateMultiLevelCosts(character *models.Char, skillName string, currentL
// Berechne Kosten für jedes Level
for _, targetLevel := range sortedLevels {
classAbr := getCharacterClass(character)
cat, difficulty, _ := gsmaster.FindBestCategoryForSkillLearning(skillName, classAbr)
levelResult := gsmaster.SkillCostResultNew{
CharacterID: fmt.Sprintf("%d", character.ID),
CharacterClass: getCharacterClass(character),
CharacterClass: classAbr,
SkillName: skillName,
Category: gsmaster.GetSkillCategory(skillName),
Difficulty: gsmaster.GetSkillDifficulty(gsmaster.GetSkillCategory(skillName), skillName),
Category: cat,
Difficulty: gsmaster.GetSkillDifficulty(difficulty, skillName),
TargetLevel: targetLevel,
}
@@ -0,0 +1,485 @@
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],
},
{
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],
},
{
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],
},
{
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],
},
{
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],
},
{
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],
},
{
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],
},
{
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],
},
{
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],
},
{
name: "learn Beeinflussen Test Basic",
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],
},
}
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 | New PP | 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 | %6d | %6d | %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(" PPUsed 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))
}
+172 -59
View File
@@ -92,39 +92,61 @@ func GetLernCost(c *gin.Context) {
// Normalize skill name (trim whitespace, proper case)
costResult.SkillName = strings.TrimSpace(request.Name)
costResult.Category = gsmaster.GetSkillCategory(request.Name)
costResult.Difficulty = gsmaster.GetSkillDifficulty(costResult.Category, costResult.SkillName)
// 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
for i := request.CurrentLevel + 1; i <= 18; i++ {
// 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: i,
TargetLevel: 1, // Lernkosten sind für das Erlernen der Fertigkeit (Level 1)
}
err := gsmaster.GetLernCostNextLevel(&request, &levelResult, request.Reward, i, character.Typ)
err := gsmaster.GetLernCostNextLevel(&request, &levelResult, request.Reward, 1, 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)
} 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.GetLernCostNextLevel(&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)
@@ -166,7 +188,7 @@ func GetLernCostNewSystem(c *gin.Context) {
// Normalize skill name (trim whitespace, proper case)
skillName := strings.TrimSpace(request.Name)
// Hole die beste Kategorie und Schwierigkeit für diese Fertigkeit und Klasse
// Hole die beste Kategorie und Schwierigkeit für diese Fertigkeit und Klasse aus der neuen Datenbank
skillInfo, err := models.GetSkillCategoryAndDifficulty(skillName, characterClass)
if err != nil {
respondWithError(c, http.StatusBadRequest, fmt.Sprintf("Fertigkeit '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", skillName, characterClass, err))
@@ -177,36 +199,54 @@ func GetLernCostNewSystem(c *gin.Context) {
remainingPP := request.UsePP
remainingGold := request.UseGold
for i := request.CurrentLevel + 1; i <= 18; i++ {
// Für "learn" Aktion: nur eine Berechnung, da Lernkosten einmalig sind
if request.Action == "learn" {
levelResult := gsmaster.SkillCostResultNew{
CharacterID: charID,
CharacterClass: characterClass,
SkillName: skillName,
Category: skillInfo.CategoryName,
Difficulty: skillInfo.DifficultyName,
TargetLevel: i,
TargetLevel: 1, // Lernkosten sind für das Erlernen der Fertigkeit (Level 1)
}
// Berechne Kosten mit dem neuen System
err := calculateCostNewSystem(&request, &levelResult, i, &remainingPP, &remainingGold, skillInfo)
err = calculateSkillLearnCostNewSystem(&request, &levelResult, &remainingPP, &remainingGold, skillInfo)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
response = append(response, levelResult)
} else {
// Für "improve" Aktion: berechne für jedes Level von current+1 bis 18
for i := request.CurrentLevel + 1; i <= 18; i++ {
levelResult := gsmaster.SkillCostResultNew{
CharacterID: charID,
CharacterClass: characterClass,
SkillName: skillName,
Category: skillInfo.CategoryName,
Difficulty: skillInfo.DifficultyName,
TargetLevel: i,
}
err = calculateSkillImproveCostNewSystem(&request, &levelResult, i, &remainingPP, &remainingGold, skillInfo)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
response = append(response, levelResult)
}
}
c.JSON(http.StatusOK, response)
}
// calculateCostNewSystem berechnet die Kosten für ein Level mit dem neuen Datenbank-System
func calculateCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.SkillCostResultNew, targetLevel int, remainingPP *int, remainingGold *int, skillInfo *models.SkillLearningInfo) error {
func calculateSkillImproveCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.SkillCostResultNew, targetLevel int, remainingPP *int, remainingGold *int, skillInfo *models.SkillLearningInfo) error {
// 1. Hole die TE-Kosten für die Verbesserung vom aktuellen Level
//currentLevel := targetLevel - 1
teRequired, err := models.GetImprovementCost(skillInfo.SkillName, skillInfo.CategoryName, skillInfo.DifficultyName, targetLevel)
if err != nil {
// Fallback: Verwende das alte System falls keine Daten in der neuen Datenbank
return fmt.Errorf("Verbesserungskosten nicht gefunden für %s (Level %d): %v", skillInfo.SkillName, targetLevel, err)
}
@@ -216,27 +256,20 @@ func calculateCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.
return fmt.Errorf("EP-Kosten pro TE nicht gefunden für Klasse %s, Kategorie %s: %v", result.CharacterClass, skillInfo.CategoryName, err)
}
// 3. Berechne Grundkosten
baseEP := teRequired * epPerTE
result.EP = baseEP
result.GoldCost = baseEP // 1 EP = 1 GS
// 3. Setze die ursprünglichen TE-Kosten
trainCost := teRequired
// 4. Anwenden von Praxispunkten (PP)
// 4. Anwenden von Praxispunkten (PP) - Exakt wie im alten System
ppUsed := 0
if *remainingPP > 0 {
// Maximal 1 PP pro Level verwenden, und nur so viele wie verfügbar
ppUsed = 1
if ppUsed > *remainingPP {
ppUsed = *remainingPP
if trainCost < *remainingPP {
ppUsed = trainCost // Maximal so viele PP verwenden wie TE benötigt werden
trainCost = 0 // Wenn PP alle TE abdecken, setze trainCost auf 0
} else if *remainingPP > 0 {
ppUsed = *remainingPP // Verwende alle verfügbaren PP
trainCost -= ppUsed // Reduziere TE um verwendete PP
}
// PP reduzieren EP-Kosten: 1 PP = 1 TE weniger
teAfterPP := teRequired - ppUsed
if teAfterPP < 0 {
teAfterPP = 0
}
result.EP = teAfterPP * epPerTE
result.PPUsed = ppUsed
*remainingPP -= ppUsed
@@ -245,26 +278,34 @@ func calculateCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.
}
}
// 5. Anwenden von Belohnungen
// 5. Berechne Kosten nach PP-Anwendung (wie im alten System)
result.LE = trainCost
result.EP = epPerTE * trainCost
result.GoldCost = trainCost * 20 // Wie im alten System: 20 Gold pro TE
// 6. Anwenden von Belohnungen
if request.Reward != nil {
applyRewardNewSystem(result, request.Reward, baseEP)
applyRewardNewSystem(result, request.Reward, result.EP)
}
// 6. Anwenden von Gold für EP (falls in Belohnung spezifiziert)
// 7. Anwenden von Gold für EP (falls verfügbar) - Beschränkt auf EP/2
goldUsed := 0
if request.Reward != nil && *request.Reward == "gold_for_ep" && *remainingGold > 0 {
// Maximal die Hälfte der EP durch Gold ersetzen (10 GS = 1 EP)
if *remainingGold > 0 {
// 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden
maxEPFromGold := result.EP / 2
maxGoldNeeded := maxEPFromGold * 10
goldUsed = maxGoldNeeded
if goldUsed > *remainingGold {
epFromGold := *remainingGold / 10
if epFromGold > maxEPFromGold {
// Beschränke auf maximal EP/2
epFromGold = maxEPFromGold
goldUsed = epFromGold * 10
} else {
// Verwende das verfügbare Gold
goldUsed = *remainingGold
}
epFromGold := goldUsed / 10
// Reduziere EP um die durch Gold ersetzte Menge
result.EP -= epFromGold
result.GoldCost += goldUsed
result.GoldUsed = goldUsed
*remainingGold -= goldUsed
@@ -273,11 +314,83 @@ func calculateCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.
}
}
// 7. Finale Geldkosten anpassen
if result.GoldUsed == 0 {
result.GoldCost = result.EP // Standard: 1 EP = 1 GS
return nil
}
// calculateSkillLearnCostNewSystem berechnet die Kosten für das Erlernen einer Fertigkeit (Action: "learn")
func calculateSkillLearnCostNewSystem(request *gsmaster.LernCostRequest, result *gsmaster.SkillCostResultNew, remainingPP *int, remainingGold *int, skillInfo *models.SkillLearningInfo) error {
// 1. Hole die EP-Kosten pro TE für diese Klasse und Kategorie
epPerTE, err := models.GetEPPerTEForClassAndCategory(result.CharacterClass, skillInfo.CategoryName)
if err != nil {
return fmt.Errorf("EP-Kosten pro TE nicht gefunden für Klasse %s, Kategorie %s: %v", result.CharacterClass, skillInfo.CategoryName, err)
}
// 2. Verwende die Lernkosten (LE) direkt aus der skillInfo - diese enthält bereits alle benötigten Informationen
learnCost := skillInfo.LearnCost
// 3. Berechne Kosten nach Lernregeln (wie im alten System)
result.LE = learnCost
result.EP = epPerTE * result.LE * 3 // Faktor 3 beim Lernen!
result.GoldCost = result.LE * 200 // 200 Gold pro LE (nicht 20 Gold pro TE)
// 4. Anwenden von Belohnungen
if request.Reward != nil {
applyRewardNewSystem(result, request.Reward, result.EP)
}
// 5. PP und Gold-für-EP Konvertierung beim Lernen
if request.Type == "spell" {
// PP-Anwendung für Zauber-Lernen: 1 PP = 1 LE Reduktion
ppUsed := 0
if *remainingPP > 0 {
if result.LE <= *remainingPP {
ppUsed = result.LE // Maximal so viele PP verwenden wie LE benötigt werden
result.LE = 0 // Wenn PP alle LE abdecken
} else {
ppUsed = *remainingPP // Verwende alle verfügbaren PP
result.LE -= ppUsed // Reduziere LE um verwendete PP
}
result.PPUsed = ppUsed
*remainingPP -= ppUsed
if *remainingPP < 0 {
*remainingPP = 0
}
// EP neu berechnen basierend auf reduzierter LE
result.EP = epPerTE * result.LE * 3
result.GoldCost = result.LE * 200
}
// Gold-für-EP Konvertierung für Zauber-Lernen
goldUsed := 0
if *remainingGold > 0 {
// 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden
maxEPFromGold := result.EP / 2
epFromGold := *remainingGold / 10
if epFromGold > maxEPFromGold {
// Beschränke auf maximal EP/2
epFromGold = maxEPFromGold
goldUsed = epFromGold * 10
} else {
// Verwende das verfügbare Gold
goldUsed = *remainingGold
}
// Reduziere EP um die durch Gold ersetzte Menge
result.EP -= epFromGold
result.GoldUsed = goldUsed
*remainingGold -= goldUsed
if *remainingGold < 0 {
*remainingGold = 0
}
}
}
// Für Skill-Lernen: Keine PP oder Gold-für-EP Anwendung erlaubt
return nil
}
+41 -14
View File
@@ -612,8 +612,8 @@ func findBestCategoryForSkillImprovement(skillName, characterClass string, level
return bestOption.category, bestOption.difficulty, nil
}
// findBestCategoryForSkillLearning findet die Kategorie mit den niedrigsten EP-Kosten für das Lernen einer Fertigkeit
func findBestCategoryForSkillLearning(skillName, characterClass string) (string, string, error) {
// FindBestCategoryForSkillLearning findet die Kategorie mit den niedrigsten EP-Kosten für das Lernen einer Fertigkeit
func FindBestCategoryForSkillLearning(skillName, characterClass string) (string, string, error) {
classKey := characterClass
// Sammle alle Kategorien und Schwierigkeiten, in denen die Fertigkeit verfügbar ist
@@ -669,7 +669,7 @@ func CalcSkillLernCost(costResult *SkillCostResultNew, reward *string) error {
// Wenn Kategorie und Schwierigkeit noch nicht gesetzt sind, finde die beste Option
if costResult.Category == "" || costResult.Difficulty == "" {
bestCategory, bestDifficulty, err := findBestCategoryForSkillLearning(costResult.SkillName, classKey)
bestCategory, bestDifficulty, err := FindBestCategoryForSkillLearning(costResult.SkillName, classKey)
if err != nil {
return err
}
@@ -758,16 +758,18 @@ func CalcSkillImproveCost(costResult *SkillCostResultNew, currentLevel int, rewa
costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung
}
if costResult.GoldUsed > 0 {
// 10 Gold = 1 EP
// 10 Gold = 1 EP, aber maximal EP/2 kann durch Gold ersetzt werden
maxEPFromGold := costResult.EP / 2
epFromGold := costResult.GoldUsed / 10
if epFromGold > costResult.EP {
// Maximal so viel Gold verwenden wie EP benötigt werden
costResult.GoldUsed = costResult.EP * 10
costResult.EP = 0
} else {
// Reduziere EP um die Menge, die durch Gold ersetzt wird
costResult.EP -= epFromGold
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
@@ -819,6 +821,22 @@ func CalcSpellLernCost(costResult *SkillCostResultNew, reward *string) error {
}
}
// 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
}
@@ -828,12 +846,15 @@ func GetLernCostNextLevel(request *LernCostRequest, costResult *SkillCostResultN
// die Berechnung erfolgt immer für genau 1 Level
// Diese Funktion wird in GetLernCost aufgerufen.
// Übertrage PP und Gold aus dem Request für die Kostenberechnung
costResult.PPUsed = request.UsePP
costResult.GoldUsed = request.UseGold
// Ü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 := CalcSkillLernCost(costResult, request.Reward)
if err != nil {
return fmt.Errorf("fehler bei der Kostenberechnung: %w", err)
@@ -843,6 +864,9 @@ func GetLernCostNextLevel(request *LernCostRequest, costResult *SkillCostResultN
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 := CalcSpellLernCost(costResult, request.Reward)
if err != nil {
return fmt.Errorf("fehler bei der Kostenberechnung: %w", err)
@@ -852,6 +876,9 @@ func GetLernCostNextLevel(request *LernCostRequest, costResult *SkillCostResultN
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 := CalcSkillImproveCost(costResult, request.CurrentLevel, request.Reward)
if err != nil {
return fmt.Errorf("fehler bei der Kostenberechnung: %w", err)
+5 -3
View File
@@ -62,6 +62,7 @@ func TestGetSkillCategory(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSkillCategory(tt.skillName)
// Check if result is in the list of expected values
@@ -569,11 +570,12 @@ func TestCalcSkillLernCostWithRewards(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create cost result
cat, difficulty, _ := FindBestCategoryForSkillLearning(tt.skillName, tt.characterClass)
costResult := &SkillCostResultNew{
CharacterClass: tt.characterClass,
SkillName: tt.skillName,
Category: GetSkillCategory(tt.skillName),
Difficulty: GetSkillDifficulty(GetSkillCategory(tt.skillName), tt.skillName),
Category: cat,
Difficulty: difficulty,
}
// Calculate normal costs first to get baseline EP
@@ -1009,7 +1011,7 @@ func TestFindBestCategoryForSkillLearning(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
category, difficulty, err := findBestCategoryForSkillLearning(tt.skillName, tt.characterClass)
category, difficulty, err := FindBestCategoryForSkillLearning(tt.skillName, tt.characterClass)
if tt.expectError {
if err == nil {
+41
View File
@@ -320,6 +320,34 @@ func GetSkillCategoryAndDifficulty(skillName string, classCode string) (*SkillLe
return &results[0], nil
}
// GetSkillInfoForCategoryAndDifficulty holt die Informationen für eine spezifische Kategorie/Schwierigkeit
func GetSkillInfoForCategoryAndDifficulty(skillName, category, difficulty, classCode string) (*SkillLearningInfo, error) {
var result SkillLearningInfo
err := database.DB.Raw(`
SELECT
scd.skill_id,
s.name as skill_name,
scd.skill_category as category_name,
scd.skill_difficulty as difficulty_name,
scd.learn_cost,
ccec.character_class as class_code,
ccec.character_class as class_name,
ccec.ep_per_te,
(scd.learn_cost * ccec.ep_per_te) as total_cost
FROM learning_skill_category_difficulties scd
JOIN learning_class_category_ep_costs ccec ON scd.skill_category = ccec.skill_category
JOIN gsm_skills s ON scd.skill_id = s.id
WHERE s.name = ? AND scd.skill_category = ? AND scd.skill_difficulty = ? AND ccec.character_class = ?
`, skillName, category, difficulty, classCode).Scan(&result).Error
if err != nil {
return nil, err
}
return &result, nil
}
// GetSpellLearningInfo holt alle Informationen für das Erlernen eines Zaubers
func GetSpellLearningInfo(spellName string, classCode string) (*SpellLearningInfo, error) {
var result SpellLearningInfo
@@ -366,6 +394,19 @@ func GetImprovementCost(skillName string, categoryName string, difficultyName st
return result.TERequired, nil
}
// GetSkillLearnCost holt die Lernkosten (LE) für eine Fertigkeit basierend auf Kategorie und Schwierigkeit
func GetSkillLearnCost(categoryName string, difficultyName string) (int, error) {
var result SkillCategoryDifficulty
err := database.DB.
Where("skill_category = ? AND skill_difficulty = ?", categoryName, difficultyName).
First(&result).Error
if err != nil {
return 0, err
}
return result.LearnCost, nil
}
// Quellenmanagement-Funktionen
// GetActiveSourceCodes gibt alle aktiven Quellencodes zurück