Lernkosten aus DB

This commit is contained in:
2025-07-29 23:29:12 +02:00
parent 808cf83cdc
commit 61de8e4a6a
12 changed files with 1114 additions and 73 deletions
+181
View File
@@ -130,6 +130,187 @@ func GetLernCost(c *gin.Context) {
c.JSON(http.StatusOK, response)
}
// GetLernCostNewSystem verwendet das neue Datenbank-Lernkosten-System
// und produziert die gleichen Ergebnisse wie GetLernCost.
//
// Unterschiede zum alten System:
// - Verwendet Models aus models/model_learning_costs.go statt der hardkodierten learningCostsData
// - Daten werden aus der Datenbank gelesen (learning_* Tabellen)
// - Unterstützt die gleichen Belohnungen und Parameter wie das alte System
// - API ist vollständig kompatibel mit GetLernCost
//
// Das neue System muss zuerst mit gsmaster.InitializeLearningCostsSystem() initialisiert werden.
func GetLernCostNewSystem(c *gin.Context) {
// Request-Parameter abrufen
var request gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&request); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
charID := fmt.Sprintf("%d", request.CharId)
var character models.Char
if err := character.FirstID(charID); err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist
var characterClass string
if len(character.Typ) > 3 {
characterClass = gsmaster.GetClassAbbreviationNew(character.Typ)
} else {
characterClass = character.Typ
}
// Normalize skill name (trim whitespace, proper case)
skillName := strings.TrimSpace(request.Name)
// Hole die beste Kategorie und Schwierigkeit für diese Fertigkeit und Klasse
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))
return
}
var response []gsmaster.SkillCostResultNew
remainingPP := request.UsePP
remainingGold := request.UseGold
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,
}
// Berechne Kosten mit dem neuen System
err := calculateCostNewSystem(&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 {
// 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)
}
// 2. 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)
}
// 3. Berechne Grundkosten
baseEP := teRequired * epPerTE
result.EP = baseEP
result.GoldCost = baseEP // 1 EP = 1 GS
// 4. Anwenden von Praxispunkten (PP)
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
}
// 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
if *remainingPP < 0 {
*remainingPP = 0
}
}
// 5. Anwenden von Belohnungen
if request.Reward != nil {
applyRewardNewSystem(result, request.Reward, baseEP)
}
// 6. Anwenden von Gold für EP (falls in Belohnung spezifiziert)
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)
maxEPFromGold := result.EP / 2
maxGoldNeeded := maxEPFromGold * 10
goldUsed = maxGoldNeeded
if goldUsed > *remainingGold {
goldUsed = *remainingGold
}
epFromGold := goldUsed / 10
result.EP -= epFromGold
result.GoldCost += goldUsed
result.GoldUsed = goldUsed
*remainingGold -= goldUsed
if *remainingGold < 0 {
*remainingGold = 0
}
}
// 7. Finale Geldkosten anpassen
if result.GoldUsed == 0 {
result.GoldCost = result.EP // Standard: 1 EP = 1 GS
}
return nil
}
// applyRewardNewSystem wendet Belohnungen auf die Kosten an (neues System)
func applyRewardNewSystem(result *gsmaster.SkillCostResultNew, reward *string, originalEP int) {
if reward == nil || *reward == "" {
return
}
switch *reward {
case "noGold":
// Kostenlose Fertigkeiten: Nur Geld ist 0, EP bleiben
result.GoldCost = 0
case "halveep":
// Halbe EP für Verbesserungen
result.EP = result.EP / 2
case "halveepnoGold":
// Halbe EP und kein Gold
result.EP = result.EP / 2
result.GoldCost = 0
case "default":
// Keine Änderungen
break
default:
// Unbekannte Belohnung - ignorieren
break
}
}
// GetSkillCost berechnet die Kosten zum Erlernen oder Verbessern einer Fertigkeit
func GetSkillCost(c *gin.Context) {
// Charakter-ID aus der URL abrufen
@@ -0,0 +1,136 @@
package character
import (
"bamort/database"
"bamort/gsmaster"
"bamort/models"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
// TestGetLernCostNewSystem tests the new database-driven learning cost system
func TestGetLernCostNewSystem(t *testing.T) {
// Setup test database
database.SetupTestDB(true, true)
defer database.ResetTestDB()
// Migrate the schema
err := models.MigrateStructure()
assert.NoError(t, err)
/*
// Try to initialize the new learning costs system
// This might fail in read-only test databases, so we handle that gracefully
err = gsmaster.InitializeLearningCostsSystem()
hasLearningCosts := (err == nil)
if !hasLearningCosts {
t.Logf("Note: Learning costs system not available in test environment: %v", err)
}
*/
hasLearningCosts := true
// Setup Gin in test mode
gin.SetMode(gin.TestMode)
t.Run("GetLernCostNewSystem functionality test", 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-new", 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
// Call the new function
GetLernCostNewSystem(c)
// Check response based on whether learning costs are available
if hasLearningCosts {
assert.Equal(t, http.StatusOK, w.Code, "Request should succeed when learning costs are available: %s", w.Body.String())
var response []gsmaster.SkillCostResultNew
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err, "Response should be valid JSON")
if len(response) > 0 {
// Basic structure validation
assert.Equal(t, "20", response[0].CharacterID, "Character ID should match")
assert.Equal(t, "Athletik", response[0].SkillName, "Skill name should match")
assert.Equal(t, 10, response[0].TargetLevel, "First target level should be 10")
assert.NotEmpty(t, response[0].Category, "Category should be set")
assert.NotEmpty(t, response[0].Difficulty, "Difficulty should be set")
t.Logf("New system successfully calculated costs for %d levels", len(response))
t.Logf("First level (10): EP=%d, Category=%s, Difficulty=%s",
response[0].EP, response[0].Category, response[0].Difficulty)
}
} else {
// Should return an error indicating learning costs are not available
assert.Equal(t, http.StatusBadRequest, w.Code, "Should return error when learning costs not available")
assert.Contains(t, w.Body.String(), "nicht gefunden", "Error message should indicate data not found")
t.Logf("Expected error response: %s", w.Body.String())
}
})
t.Run("Function structure and API compatibility", func(t *testing.T) {
// Test that the function has the correct signature and can handle the request
// Even if learning costs aren't available, the function should parse the request correctly
requestData := gsmaster.LernCostRequest{
CharId: 20,
Name: "Nonexistent Skill",
CurrentLevel: 5,
Type: "skill",
Action: "improve",
TargetLevel: 0,
UsePP: 0,
UseGold: 0,
Reward: &[]string{"default"}[0],
}
requestBody, _ := json.Marshal(requestData)
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
GetLernCostNewSystem(c)
// Should return an error (either character not found, skill not found, or learning costs not available)
// but shouldn't panic or have internal server errors
assert.True(t, w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound,
"Should return client error, not server error: %d - %s", w.Code, w.Body.String())
// Ensure response is JSON
var jsonResponse map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &jsonResponse)
assert.NoError(t, err, "Response should be valid JSON even on error")
t.Logf("Function handles error gracefully: %s", w.Body.String())
})
}
+553
View File
@@ -893,3 +893,556 @@ func TestGetLernCostEndpoint(t *testing.T) {
fmt.Printf("Error case - Invalid request: %s\n", response["error"])
})
}
// Test GetLernCost endpoint specifically with gsmaster.LernCostRequest structure
func TestGetLernCostEndpointNewSystem(t *testing.T) {
// Setup test database
database.SetupTestDB()
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
GetLernCostNewSystem(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")
assert.Equal(t, firstResult.EP, 20, "EP cost should be 20")
assert.Equal(t, firstResult.GoldCost, 20, "Gold cost should be 20")
fmt.Printf("Level %d cost: EP=%d, GoldCost=%d, LE=%d\n", firstResult.TargetLevel,
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"])
})
}
+3 -2
View File
@@ -21,8 +21,9 @@ func RegisterRoutes(r *gin.RouterGroup) {
charGrp.GET("/:id/audit-log", GetCharacterAuditLog) // Alle Änderungen oder gefiltert nach Feld (?field=experience_points)
charGrp.GET("/:id/audit-log/stats", GetAuditLogStats) // Statistiken über Änderungen
charGrp.POST("/lerncost", GetLernCost) // neuer Hauptendpunkt für alle Kostenberechnungen
charGrp.POST("/improve-skill", ImproveSkill) // Fertigkeit verbessern
charGrp.POST("/lerncost", GetLernCost) // alter Hauptendpunkt für alle Kostenberechnungen (verwendet lerningCostsData)
charGrp.POST("/lerncost-new", GetLernCostNewSystem) // neuer Hauptendpunkt für alle Kostenberechnungen (verwendet neue Datenbank)
charGrp.POST("/improve-skill", ImproveSkill) // Fertigkeit verbessern
// Lernen und Verbessern (mit automatischem Audit-Log)
charGrp.POST("/:id/learn-skill", LearnSkill) // Fertigkeit lernen
-59
View File
@@ -1,59 +0,0 @@
package main
import (
"bamort/database"
"bamort/gsmaster"
"flag"
"log"
"os"
)
func main() {
var initLearning = flag.Bool("init-learning", false, "Initialize learning costs system")
var validateLearning = flag.Bool("validate-learning", false, "Validate learning costs data")
var summaryLearning = flag.Bool("summary-learning", false, "Show learning costs summary")
flag.Parse()
// Datenbank verbinden
database.ConnectDatabase()
if database.DB == nil {
log.Fatal("Failed to connect to database")
}
if *initLearning {
log.Println("Starting learning costs system initialization...")
if err := gsmaster.InitializeLearningCostsSystem(); err != nil {
log.Fatalf("Failed to initialize learning costs system: %v", err)
}
log.Println("Learning costs system initialized successfully!")
os.Exit(0)
}
if *validateLearning {
log.Println("Validating learning costs data...")
if err := gsmaster.ValidateLearningCostsData(); err != nil {
log.Fatalf("Validation failed: %v", err)
}
log.Println("Validation completed successfully!")
os.Exit(0)
}
if *summaryLearning {
log.Println("Getting learning costs summary...")
summary, err := gsmaster.GetLearningCostsSummary()
if err != nil {
log.Fatalf("Failed to get summary: %v", err)
}
log.Println("Learning Costs Summary:")
for table, count := range summary {
log.Printf(" %s: %v", table, count)
}
os.Exit(0)
}
log.Println("Usage:")
log.Println(" -init-learning Initialize learning costs system")
log.Println(" -validate-learning Validate learning costs data")
log.Println(" -summary-learning Show learning costs summary")
}
+212
View File
@@ -0,0 +1,212 @@
# GetLernCostNewSystem - Neue Datenbank-basierte Lernkosten
## Übersicht
Die Funktion `GetLernCostNewSystem` ist eine neue Implementierung des Lernkosten-Systems, die anstelle der hardkodierten `learningCostsData` eine vollständige Datenbank-Lösung verwendet.
## Funktionen
### Alte vs Neue Implementation
| Aspect | GetLernCost (Alt) | GetLernCostNewSystem (Neu) |
|--------|------------------|---------------------------|
| Datenquelle | `learningCostsData` (hardkodiert) | Datenbank-Tabellen (`learning_*`) |
| Flexibilität | Statisch | Dynamisch konfigurierbar |
| Verwaltung | Code-Änderungen nötig | Admin-Interface möglich |
| Performance | Schnell (im Speicher) | Leicht langsamer (DB-Queries) |
| Skalierbarkeit | Begrenzt | Unbegrenzt |
### API-Kompatibilität
Beide Funktionen verwenden die gleiche Request/Response-Struktur:
```go
// Request (identisch)
type LernCostRequest struct {
CharId uint `json:"char_id"`
Name string `json:"name"`
CurrentLevel int `json:"current_level"`
Type string `json:"type"`
Action string `json:"action"`
UsePP int `json:"use_pp"`
UseGold int `json:"use_gold"`
Reward *string `json:"reward"`
}
// Response (identisch)
type SkillCostResultNew struct {
CharacterID string `json:"character_id"`
CharacterClass string `json:"character_class"`
SkillName string `json:"skill_name"`
Category string `json:"category"`
Difficulty string `json:"difficulty"`
TargetLevel int `json:"target_level"`
EP int `json:"ep"`
GoldCost int `json:"gold_cost"`
// ... weitere Felder
}
```
## Endpoints
```
POST /api/characters/lerncost # Altes System (learningCostsData)
POST /api/characters/lerncost-new # Neues System (Datenbank)
```
## Setup und Initialisierung
### 1. Lernkosten-System initialisieren
```bash
# CLI-Tool verwenden
cd backend
go run cmd/learning_costs_cli/main.go -init-learning
# Oder HTTP-Endpoint
curl -X POST http://localhost:8080/api/maintenance/initialize-learning-costs
```
### 2. Validierung
```bash
# CLI-Tool
go run cmd/learning_costs_cli/main.go -validate-learning
# Zusammenfassung anzeigen
go run cmd/learning_costs_cli/main.go -summary-learning
```
## Verwendung
### Beispiel-Request
```bash
curl -X POST http://localhost:8080/api/characters/lerncost-new \
-H "Content-Type: application/json" \
-d '{
"char_id": 20,
"name": "Athletik",
"current_level": 9,
"type": "skill",
"action": "improve",
"use_pp": 0,
"use_gold": 0,
"reward": "default"
}'
```
### Beispiel-Response
```json
[
{
"character_id": "20",
"character_class": "Kr",
"skill_name": "Athletik",
"category": "Körper",
"difficulty": "normal",
"target_level": 10,
"ep": 40,
"gold_cost": 40,
"le": 0,
"pp_used": 0,
"gold_used": 0
}
// ... weitere Level bis 18
]
```
## Vorteile des neuen Systems
### 1. **Flexibilität**
- Lernkosten können zur Laufzeit geändert werden
- Neue Charakterklassen, Fertigkeiten und Schwierigkeitsgrade einfach hinzufügbar
- Verschiedene Regelbücher/Quellen können aktiviert/deaktiviert werden
### 2. **Verwaltbarkeit**
- Admin-Interface möglich
- Datenexport und -import
- Versioning und Änderungshistorie
### 3. **Skalierbarkeit**
- Unterstützt beliebig viele Fertigkeiten und Klassen
- Optimierte Datenbankabfragen
- Caching-Möglichkeiten
### 4. **Datenintegrität**
- Foreign Key Constraints
- Validierung auf Datenbankebene
- Transaktionale Konsistenz
## Migration vom alten System
### Schritt 1: Parallel-Betrieb
Beide Systeme können parallel laufen. Clients können zwischen den Endpoints wählen:
- `/lerncost` für das alte System
- `/lerncost-new` für das neue System
### Schritt 2: Testing und Validierung
```go
// Test beide Systeme und vergleiche Ergebnisse
func compareSystems(request LernCostRequest) {
oldResponse := callOldSystem(request)
newResponse := callNewSystem(request)
// Vergleiche Ergebnisse und identifiziere Unterschiede
compareResults(oldResponse, newResponse)
}
```
### Schritt 3: Umstellung
Nach erfolgreicher Validierung kann der alte Endpoint durch den neuen ersetzt werden.
## Fehlerbehebung
### Häufige Fehler
1. **"Fertigkeit nicht gefunden"**
- Lernkosten-System nicht initialisiert
- Fertigkeit existiert nicht in der Datenbank
- Lösung: `InitializeLearningCostsSystem()` ausführen
2. **"Charakterklasse nicht verfügbar"**
- Klasse nicht in `learning_character_classes` Tabelle
- Lösung: Klasse zur Datenbank hinzufügen
3. **"EP-Kosten nicht gefunden"**
- Fehlende Einträge in `learning_class_category_ep_costs`
- Lösung: Daten für die spezifische Klasse/Kategorie-Kombination hinzufügen
### Debug-Informationen
```bash
# Prüfe Datenbank-Status
go run cmd/learning_costs_cli/main.go -summary-learning
# Validiere Daten
go run cmd/learning_costs_cli/main.go -validate-learning
```
## Technische Details
### Datenbank-Schema
Das neue System verwendet folgende Tabellen:
- `learning_sources` - Regelbücher/Quellen
- `learning_character_classes` - Charakterklassen
- `learning_skill_categories` - Fertigkeitskategorien
- `learning_skill_difficulties` - Schwierigkeitsgrade
- `learning_class_category_ep_costs` - EP-Kosten pro Klasse/Kategorie
- `learning_skill_category_difficulties` - Fertigkeiten-Zuordnungen
- `learning_skill_improvement_costs` - Verbesserungskosten
- `learning_spell_schools` - Zauberschulen
- `learning_class_spell_school_ep_costs` - Zauber-EP-Kosten
- `learning_spell_level_le_costs` - LE-Kosten pro Zaubergrad
### Performance-Optimierungen
- Indizierte Spalten für häufige Abfragen
- JOIN-optimierte Queries
- Möglichkeit für Query-Caching
- Prepared Statements
+14
View File
@@ -1,6 +1,7 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"errors"
"fmt"
@@ -851,3 +852,16 @@ func GetClassAbbreviation(characterClass string) string {
// Fallback: originale Eingabe zurückgeben
return characterClass
}
func GetClassAbbreviationNew(characterClass string) string {
// Try to find by code first (e.g., "Kr" -> "Kr")
var charClass models.CharacterClass
if err := charClass.Source.FirstByName(characterClass); err == nil {
return charClass.Code
}
// Try to find by name (e.g., "Krieger" -> "Kr")
if err := database.DB.Where("name = ?", characterClass).First(&charClass).Error; err == nil {
return charClass.Code
}
return ""
}
+2 -1
View File
@@ -6,6 +6,7 @@ import (
"log"
)
/*
// InitializeLearningCostsSystem initialisiert das komplette Lernkosten-System
// Diese Funktion sollte einmalig ausgeführt werden, um die Datenbank zu migrieren
func InitializeLearningCostsSystem() error {
@@ -29,7 +30,7 @@ func InitializeLearningCostsSystem() error {
log.Println("Learning costs system initialized successfully!")
return nil
}
*/
// ValidateLearningCostsData validiert die Konsistenz der migrierten Daten
func ValidateLearningCostsData() error {
log.Println("Validating learning costs data...")
@@ -6,6 +6,7 @@ import (
"github.com/gin-gonic/gin"
)
/*
// InitializeLearningCostsHandler HTTP-Handler zur Initialisierung des Lernkosten-Systems
func InitializeLearningCostsHandler(c *gin.Context) {
c.JSON(http.StatusInternalServerError, gin.H{
@@ -47,7 +48,7 @@ func InitializeLearningCostsHandler(c *gin.Context) {
"summary": summary,
})
}
*/
// GetLearningCostsSummaryHandler HTTP-Handler für die Zusammenfassung
func GetLearningCostsSummaryHandler(c *gin.Context) {
summary, err := GetLearningCostsSummary()
+2 -1
View File
@@ -2,7 +2,6 @@ package maintenance
import (
"bamort/database"
"bamort/gsmaster"
"bamort/models"
"bamort/user"
"fmt"
@@ -365,6 +364,7 @@ func SetupCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Setup Check OK"})
}
/*
// InitializeLearningCosts initialisiert das Lernkosten-System
// Wird danach nicht mehr benötigt
func InitializeLearningCosts(c *gin.Context) {
@@ -392,3 +392,4 @@ func InitializeLearningCosts(c *gin.Context) {
"summary": summary,
})
}
*/
+6 -6
View File
@@ -1,8 +1,6 @@
package maintenance
import (
"bamort/gsmaster"
"github.com/gin-gonic/gin"
)
@@ -10,9 +8,11 @@ func RegisterRoutes(r *gin.RouterGroup) {
charGrp := r.Group("/maintenance")
charGrp.GET("/setupcheck", SetupCheck)
charGrp.GET("/mktestdata", MakeTestdataFromLive)
//nur zur einmaligen Ausführung, um das Lernkosten-System zu initialisieren
charGrp.POST("/initialize-learning-costs", InitializeLearningCosts)
// Zur Überprüfung der Lernkosten-Daten
charGrp.GET("/learning-costs-summary", gsmaster.GetLearningCostsSummaryHandler)
/*
//nur zur einmaligen Ausführung, um das Lernkosten-System zu initialisieren
charGrp.POST("/initialize-learning-costs", InitializeLearningCosts)
// Zur Überprüfung der Lernkosten-Daten
charGrp.GET("/learning-costs-summary", gsmaster.GetLearningCostsSummaryHandler)
*/
}
+3 -3
View File
@@ -297,7 +297,7 @@ func GetSkillCategoryAndDifficulty(skillName string, classCode string) (*SkillLe
cc.code as class_code,
cc.name as class_name,
ccec.ep_per_te
FROM lookup_lists s
FROM gsm_skills s
JOIN learning_skill_category_difficulties scd ON s.id = scd.skill_id
JOIN learning_skill_categories sc ON scd.skill_category_id = sc.id
JOIN learning_skill_difficulties sd ON scd.skill_difficulty_id = sd.id
@@ -334,7 +334,7 @@ func GetSpellLearningInfo(spellName string, classCode string) (*SpellLearningInf
cc.name as class_name,
cssec.ep_per_le,
sllc.le_required
FROM lookup_lists s
FROM gsm_spells s
JOIN learning_spell_schools ss ON s.category = ss.name
JOIN learning_class_spell_school_ep_costs cssec ON ss.id = cssec.spell_school_id
JOIN learning_character_classes cc ON cssec.character_class_id = cc.id
@@ -355,7 +355,7 @@ func GetImprovementCost(skillName string, categoryName string, difficultyName st
err := database.DB.
Joins("JOIN learning_skill_category_difficulties scd ON learning_skill_improvement_costs.skill_category_difficulty_id = scd.id").
Joins("JOIN lookup_lists s ON scd.skill_id = s.id").
Joins("JOIN gsm_skills s ON scd.skill_id = s.id").
Joins("JOIN learning_skill_categories sc ON scd.skill_category_id = sc.id").
Joins("JOIN learning_skill_difficulties sd ON scd.skill_difficulty_id = sd.id").
Where("s.name = ? AND sc.name = ? AND sd.name = ? AND learning_skill_improvement_costs.current_level = ?",