Lernkosten aus DB
This commit is contained in:
@@ -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())
|
||||
})
|
||||
}
|
||||
@@ -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"])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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,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,
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
@@ -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 = ?",
|
||||
|
||||
Reference in New Issue
Block a user