Merge branch 'Lernkosten-Fortsetzung'

This commit is contained in:
2025-07-25 07:46:19 +02:00
12 changed files with 1468 additions and 248 deletions
+16 -16
View File
@@ -237,6 +237,7 @@ func GetLearnSpellCost(c *gin.Context) {
c.JSON(http.StatusOK, sd)
}
/*
func GetSkillNextLevelCosts(c *gin.Context) {
// Get the character ID from the request
charID := c.Param("id")
@@ -267,7 +268,8 @@ func GetSkillNextLevelCosts(c *gin.Context) {
// Return the updated character
c.JSON(http.StatusOK, character.Fertigkeiten)
}
*/
/*
func GetSkillAllLevelCosts(c *gin.Context) {
// Get the character ID from the request
charID := c.Param("id")
@@ -322,7 +324,7 @@ func GetSkillAllLevelCosts(c *gin.Context) {
// Return the updated character
c.JSON(http.StatusOK, costArr)
}
*/
// ExperienceAndWealthResponse repräsentiert die Antwort für EP und Vermögen
type ExperienceAndWealthResponse struct {
ExperiencePoints int `json:"experience_points"`
@@ -892,6 +894,8 @@ func LearnSpell(c *gin.Context) {
}
// ImproveSpell verbessert einen bestehenden Zauber und erstellt Audit-Log-Einträge
// Zauber können nicht verbessert werden
/*
func ImproveSpell(c *gin.Context) {
charID := c.Param("id")
var character Char
@@ -971,7 +975,7 @@ func ImproveSpell(c *gin.Context) {
"remaining_ep": newEP,
})
}
*/
// GetRewardTypes liefert verfügbare Belohnungsarten für ein bestimmtes Lernszenario
func GetRewardTypes(c *gin.Context) {
characterID := c.Param("id")
@@ -985,28 +989,24 @@ func GetRewardTypes(c *gin.Context) {
// Je nach Lerntyp verschiedene Belohnungsarten anbieten
switch learningType {
case "learn":
// Neue Fertigkeit lernen - meist nur EP oder Gold
// Neue Fertigkeit lernen - noGold Belohnung verfügbar
rewardTypes = append(rewardTypes,
gin.H{"value": "ep", "label": "Erfahrungspunkte verwenden", "description": "Verwende EP zum Lernen"},
gin.H{"value": "gold", "label": "Gold verwenden", "description": "Bezahle einen Lehrer mit Gold"},
gin.H{"value": "default", "label": "Standard (EP + Gold)", "description": "Normale EP- und Goldkosten"},
gin.H{"value": "noGold", "label": "Ohne Gold (nur EP)", "description": "Keine Goldkosten, nur EP als Belohnung"},
)
case "spell":
// Zauber - mehr Optionen including Ritual
// Zauber lernen - halveepnoGold verfügbar
rewardTypes = append(rewardTypes,
gin.H{"value": "ep", "label": "Erfahrungspunkte verwenden", "description": "Verwende EP zum Verbessern"},
gin.H{"value": "gold", "label": "Gold verwenden", "description": "Bezahle einen Zauberlehrer"},
gin.H{"value": "pp", "label": "Praxispunkte verwenden", "description": "Nutze gesammelte Praxis"},
gin.H{"value": "mixed", "label": "Gemischt (EP + PP)", "description": "Kombiniere EP und PP für reduzierten Aufwand"},
gin.H{"value": "default", "label": "Standard (EP)", "description": "Normale EP-Kosten"},
gin.H{"value": "halveepnoGold", "label": "Halbe EP ohne Gold", "description": "Halbe EP-Kosten, kein Gold als Belohnung"},
)
case "improve":
// Fertigkeit verbessern - Standard-Optionen
// Fertigkeit verbessern - halveepnoGold verfügbar
rewardTypes = append(rewardTypes,
gin.H{"value": "ep", "label": "Erfahrungspunkte verwenden", "description": "Verwende EP zum Verbessern"},
gin.H{"value": "gold", "label": "Gold verwenden", "description": "Bezahle einen Lehrer"},
gin.H{"value": "pp", "label": "Praxispunkte verwenden", "description": "Nutze gesammelte Praxis"},
gin.H{"value": "mixed", "label": "Gemischt (EP + PP)", "description": "Kombiniere EP und PP für reduzierten Aufwand"},
gin.H{"value": "default", "label": "Standard (EP + Gold)", "description": "Normale EP- und Goldkosten"},
gin.H{"value": "halveepnoGold", "label": "Halbe EP ohne Gold", "description": "Halbe EP-Kosten, kein Gold als Belohnung"},
)
// Spezielle Optionen für bestimmte Fertigkeiten
@@ -65,25 +65,10 @@ type MultiLevelCostResponse struct {
CanAffordTotal bool `json:"can_afford_total"`
}
type LernCostRequest struct {
CharId uint `json:"char_id" binding:"required"` // Charakter-ID
Name string `json:"name" binding:"required"` // Name der Fertigkeit / des Zaubers
CurrentLevel int `json:"current_level,omitempty"` // Aktueller Wert (nur für Verbesserung)
Type string `json:"type" binding:"required,oneof=skill spell weapon"` // 'skill', 'spell' oder 'weapon' 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'
TargetLevel int `json:"target_level,omitempty"` // Zielwert (optional, für Kostenberechnung bis zu einem bestimmten Level)
UsePP int `json:"use_pp,omitempty"` // Anzahl der zu verwendenden Praxispunkte
// Belohnungsoptionen
Reward *string `json:"reward" binding:"required,oneof=default noGold halveep"` // Belohnungsoptionen Lernen als Belohnung
// default
// learn: ohne Gold
// improve/spell: halbe EP kein Gold
}
// GetLernCost
func GetLernCost(c *gin.Context) {
// Request-Parameter abrufen
var request LernCostRequest
var request gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&request); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
@@ -96,44 +81,37 @@ func GetLernCost(c *gin.Context) {
}
var costResult gsmaster.SkillCostResultNew
costResult.CharacterID = charID
costResult.CharacterClass = character.Typ
// Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist
if len(character.Typ) > 3 {
costResult.CharacterClass = gsmaster.GetClassAbbreviation(character.Typ)
} else {
costResult.CharacterClass = character.Typ
}
// 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)
switch {
case request.Action == "learn" && request.Type == "skill":
err := gsmaster.CalcSkillLernCost(&costResult, request.Reward)
var response []gsmaster.SkillCostResultNew
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
}
// extrakosten für elfen
if character.Typ == "Elf" {
costResult.EP += 6
}
case request.Action == "learn" && request.Type == "spell":
err := gsmaster.CalcSpellLernCost(&costResult, request.Reward)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
// extrakosten für elfen
if character.Typ == "Elf" {
costResult.EP += 6
}
case request.Action == "improve" && request.Type == "skill":
err := gsmaster.CalcSkillImproveCost(&costResult, request.CurrentLevel, request.Reward)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Fehler bei der Kostenberechnung: "+err.Error())
return
}
default:
response = append(response, levelResult)
}
c.JSON(http.StatusOK, costResult)
c.JSON(http.StatusOK, response)
}
// GetSkillCost berechnet die Kosten zum Erlernen oder Verbessern einer Fertigkeit
@@ -645,7 +623,7 @@ func applyPPReduction(request *SkillCostRequest, cost *gsmaster.LearnCost, avail
return finalEP, finalLE, reduction
}
func CalcSkillLearnCost(req *LernCostRequest, skillCostInfo *gsmaster.SkillCostResultNew) error {
func CalcSkillLearnCost(req *gsmaster.LernCostRequest, skillCostInfo *gsmaster.SkillCostResultNew) error {
// Fallback-Werte für Skills ohne definierte Kategorie/Schwierigkeit
result, err := gsmaster.CalculateSkillLearningCosts(skillCostInfo.CharacterClass, skillCostInfo.Category, skillCostInfo.Difficulty)
@@ -4,7 +4,6 @@ import (
"bamort/database"
"bamort/equipment"
"bamort/gsmaster"
"bamort/models"
"bamort/skills"
"bytes"
"encoding/json"
@@ -354,6 +353,7 @@ func TestGSMasterIntegration(t *testing.T) {
}
// Test the GetSkillAllLevelCosts endpoint (GET /:id/improve/skill)
/*
func TestGetSkillAllLevelCostsEndpoint(t *testing.T) {
// Setup test database
database.SetupTestDB()
@@ -594,3 +594,226 @@ func TestGetSkillAllLevelCostsEndpoint(t *testing.T) {
assert.Contains(t, response, "error", "Response should contain error message")
})
}
*/
// Test GetLernCost endpoint specifically with gsmaster.LernCostRequest structure
func TestGetLernCostEndpoint(t *testing.T) {
// Setup test database
database.SetupTestDB(true, true)
defer database.ResetTestDB()
// Migrate the schema
err := MigrateStructure()
assert.NoError(t, err)
// Also migrate skills and equipment to avoid preload errors
err = skills.MigrateStructure()
assert.NoError(t, err)
err = equipment.MigrateStructure()
assert.NoError(t, err)
err = gsmaster.MigrateStructure()
assert.NoError(t, err)
/*
// Create test skill data
err = createTestSkillData()
assert.NoError(t, err)
defer cleanupTestSkillData()
// Create test character with ID 20 and class "Krieger"
testChar := createChar()
testChar.ID = 20
testChar.Typ = "Krieger" // Set character class to "Krieger"
// Add Athletik skill at level 9
skillName := "Athletik"
skill := skills.Fertigkeit{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: skillName,
},
CharacterID: 20,
},
Fertigkeitswert: 9,
}
testChar.Fertigkeiten = append(testChar.Fertigkeiten, skill)
err = testChar.Create()
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
Reward: &[]string{"default"}[0], // Default reward type
}
requestBody, _ := json.Marshal(requestData)
// Create HTTP request
req, _ := http.NewRequest("POST", "/api/characters/lerncost", bytes.NewBuffer(requestBody))
req.Header.Set("Content-Type", "application/json")
// Create response recorder
w := httptest.NewRecorder()
// Create Gin context
c, _ := gin.CreateTestContext(w)
c.Request = req
c.Params = []gin.Param{{Key: "id", Value: "20"}}
fmt.Printf("Test: GetLernCost for Athletik improvement for Krieger character ID 20\n")
fmt.Printf("Request: CharId=%d, SkillName=%s, CurrentLevel=%d, TargetLevel=%d\n",
requestData.CharId, requestData.Name, requestData.CurrentLevel, requestData.TargetLevel)
// Call the actual handler function
GetLernCost(c)
// Print the actual response to see what we get
fmt.Printf("Response Status: %d\n", w.Code)
fmt.Printf("Response Body: %s\n", w.Body.String())
// Check if we got an error response first
if w.Code != http.StatusOK {
var errorResponse map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
if err == nil {
fmt.Printf("Error Response: %+v\n", errorResponse)
}
assert.Fail(t, "Expected successful response but got error: %s", w.Body.String())
return
}
// Parse and validate response for success case
var response []gsmaster.SkillCostResultNew
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err, "Response should be valid JSON array of SkillCostResultNew")
// Should have costs for levels 10, 11, 12, ... up to 18 (from current level 9)
assert.Greater(t, len(response), 0, "Should return learning costs for multiple levels")
assert.LessOrEqual(t, len(response), 9, "Should not return more than 9 levels (10-18)")
// Validate the first entry (level 10)
if len(response) > 0 {
firstResult := response[0]
assert.Equal(t, "20", firstResult.CharacterID, "Character ID should match")
assert.Equal(t, "Athletik", firstResult.SkillName, "Skill name should match")
assert.Equal(t, 10, firstResult.TargetLevel, "First target level should be 10")
// Character class should be "Kr" (abbreviation for "Krieger")
assert.Equal(t, "Kr", firstResult.CharacterClass, "Character class should be abbreviated to 'Kr'")
// Should have valid costs
assert.Greater(t, firstResult.EP, 0, "EP cost should be greater than 0")
assert.GreaterOrEqual(t, firstResult.GoldCost, 0, "Gold cost should be 0 or greater")
fmt.Printf("Level 10 cost: EP=%d, GoldCost=%d, LE=%d\n",
firstResult.EP, firstResult.GoldCost, firstResult.LE)
fmt.Printf("Category=%s, Difficulty=%s\n",
firstResult.Category, firstResult.Difficulty)
}
// Find cost for level 12 specifically to test mid-range
var level12Cost *gsmaster.SkillCostResultNew
for i := range response {
if response[i].TargetLevel == 12 {
level12Cost = &response[i]
break
}
}
if level12Cost != nil {
assert.Equal(t, 12, level12Cost.TargetLevel, "Target level should be 12")
assert.Greater(t, level12Cost.EP, 0, "EP cost should be greater than 0 for level 12")
fmt.Printf("Level 12 cost: EP=%d, GoldCost=%d, LE=%d\n",
level12Cost.EP, level12Cost.GoldCost, level12Cost.LE)
} else {
fmt.Printf("No cost found for level 12. Available levels: ")
for _, cost := range response {
fmt.Printf("%d ", cost.TargetLevel)
}
fmt.Println()
}
// Verify all target levels are sequential and start from current level + 1
expectedLevel := 10 // Current level 9 + 1
for _, cost := range response {
assert.Equal(t, expectedLevel, cost.TargetLevel,
"Target levels should be sequential starting from %d", expectedLevel)
assert.Equal(t, "Athletik", cost.SkillName, "All entries should have correct skill name")
assert.Equal(t, "Kr", cost.CharacterClass, "All entries should have correct character class")
expectedLevel++
}
})
t.Run("GetLernCost 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"])
})
}
+4 -4
View File
@@ -24,15 +24,15 @@ func RegisterRoutes(r *gin.RouterGroup) {
charGrp.POST("/lerncost", GetLernCost) // neuer Hauptendpunkt für alle Kostenberechnungen
// Kostenberechnung (konsolidiert)
charGrp.POST("/:id/skill-cost", GetSkillCost) // Hauptendpunkt für alle Kostenberechnungen
charGrp.GET("/:id/improve", GetSkillNextLevelCosts) // Legacy - für nächste Stufe
charGrp.GET("/:id/improve/skill", GetSkillAllLevelCosts) // Legacy - für alle Stufen
//charGrp.POST("/:id/skill-cost", GetSkillCost) // Hauptendpunkt für alle Kostenberechnungen
//charGrp.GET("/:id/improve", GetSkillNextLevelCosts) // Legacy - für nächste Stufe
//charGrp.GET("/:id/improve/skill", GetSkillAllLevelCosts) // Legacy - für alle Stufen
// Lernen und Verbessern (mit automatischem Audit-Log)
charGrp.POST("/:id/learn-skill", LearnSkill) // Fertigkeit lernen
charGrp.POST("/:id/improve-skill", ImproveSkill) // Fertigkeit verbessern
charGrp.POST("/:id/learn-spell", LearnSpell) // Zauber lernen
charGrp.POST("/:id/improve-spell", ImproveSpell) // Zauber verbessern
//charGrp.POST("/:id/improve-spell", ImproveSpell) // Zauber verbessern
// Belohnungsarten für verschiedene Lernszenarien
charGrp.GET("/:id/reward-types", GetRewardTypes) // Verfügbare Belohnungsarten je nach Kontext
+3
View File
@@ -0,0 +1,3 @@
package database
// Empty test helper file
+26
View File
@@ -1,6 +1,7 @@
package database
import (
"fmt"
"io"
"os"
"path/filepath"
@@ -47,6 +48,25 @@ func copyFile(src, dst string) error {
return err
}
// LoadCopyOfPredefinedTestData loads predefined test data from a specific file into the provided database
func LoadCopyOfPredefinedTestData(targetDB *gorm.DB, dataFile string) error {
// Check if file exists
if _, err := os.Stat(dataFile); os.IsNotExist(err) {
return fmt.Errorf("predefined test data file not found: %s", dataFile)
}
testpath := filepath.Join(filepath.Dir(dataFile), "tmp_test_data.db")
if _, err := os.Stat(testpath); os.IsExist(err) {
os.Remove(testpath) // delete existing test data file
}
copyFile(dataFile, testpath)
targetDB, err := gorm.Open(sqlite.Open(testpath), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to open temporary test database: %w", err)
}
DB = targetDB
return nil
}
// SetupTestDB creates an in-memory SQLite database for testing
// Parameters:
// - opts[0]: isTestDb (bool) - whether to use in-memory SQLite (true) or persistent MariaDB (false)
@@ -134,6 +154,12 @@ func SetupTestDB(opts ...bool) {
if err != nil {
panic("failed to load test data: " + err.Error())
}
} else if isTestDb {
// If no test data loader is set, we can still use the default test data loading
err := LoadCopyOfPredefinedTestData(DB, "../testdata/prepared_test_data.db")
if err != nil {
panic("failed to load predefined test data: " + err.Error())
}
}
migrationDone = true
}
+6 -4
View File
@@ -328,7 +328,7 @@ func CalculateSkillLearningCosts(characterClass, category, difficulty string) (*
}
// Konvertiere Vollnamen der Charakterklasse zu Abkürzungen falls nötig
classKey := getClassAbbreviation(characterClass)
classKey := GetClassAbbreviation(characterClass)
// Hole die EP-Kosten pro TE für die angegebene Charakterklasse
classData, exists := learningCosts.EPPerTE[classKey]
@@ -378,7 +378,7 @@ func CalculateSpellLearningCosts(characterClass, spellSchool string, leNeeded in
}
// Konvertiere Vollnamen zu Abkürzungen falls nötig
classKey := getClassAbbreviation(characterClass)
classKey := GetClassAbbreviation(characterClass)
classData, exists := learningCosts.SpellEPPerLE[classKey]
if !exists {
@@ -430,6 +430,8 @@ type SkillCostResultNew struct {
EP int `json:"ep"`
LE int `json:"le"`
GoldCost int `json:"gold_cost"`
PPUsed int `json:"pp_used"`
TargetLevel int `json:"target_level"`
Details map[string]interface{} `json:"details"`
}
@@ -810,8 +812,8 @@ func getDefaultSpellSchool(spellName string) string {
return "Verändern"
}
// getClassAbbreviation konvertiert Charakterklassen-Vollnamen zu Abkürzungen
func getClassAbbreviation(characterClass string) string {
// GetClassAbbreviation konvertiert Charakterklassen-Vollnamen zu Abkürzungen
func GetClassAbbreviation(characterClass string) string {
// Mapping von Vollnamen zu Abkürzungen
classMap := map[string]string{
// Abenteurer-Klassen
+302 -23
View File
@@ -1,6 +1,23 @@
package gsmaster
import "fmt"
import (
"fmt"
)
type LernCostRequest struct {
CharId uint `json:"char_id" binding:"required"` // Charakter-ID
Name string `json:"name" binding:"required"` // Name der Fertigkeit / des Zaubers
CurrentLevel int `json:"current_level,omitempty"` // Aktueller Wert (nur für Verbesserung)
Type string `json:"type" binding:"required,oneof=skill spell weapon"` // 'skill', 'spell' oder 'weapon' 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'
TargetLevel int `json:"target_level,omitempty"` // Zielwert (optional, für Kostenberechnung bis zu einem bestimmten Level)
UsePP int `json:"use_pp,omitempty"` // Anzahl der zu verwendenden Praxispunkte
// Belohnungsoptionen
Reward *string `json:"reward" binding:"required,oneof=default noGold halveep halveepnoGold"` // Belohnungsoptionen Lernen als Belohnung
// default
// learn: ohne Gold
// improve/spell: halbe EP kein Gold
}
// DifficultyData enthält Skills und Trainingskosten für eine Schwierigkeitsstufe
type DifficultyData struct {
@@ -15,7 +32,8 @@ type LearningCostsTable2 struct {
EPPerTE map[string]map[string]int
// EP-Kosten für 1 Lerneinheit (LE) für Zauber pro Charakterklasse und Zauberschule
SpellEPPerLE map[string]map[string]int
SpellEPPerLE map[string]map[string]int
SpellLEPerLevel map[int]int
// LE-Kosten für Fertigkeiten basierend auf Schwierigkeit
BaseLearnCost map[string]map[string]int
@@ -282,6 +300,9 @@ var learningCostsData = &LearningCostsTable2{
"Lied": 30,
},
},
// Lernen von Zaubern
// LE pro Stufe des Zaubers
SpellLEPerLevel: map[int]int{1: 1, 2: 1, 3: 2, 4: 3, 5: 5, 6: 10, 7: 15, 8: 20, 9: 30, 10: 40, 11: 60, 12: 90},
// TE-Kosten für Verbesserungen basierend auf Kategorie, Schwierigkeit und aktuellem Wert
ImprovementCost: map[string]map[string]DifficultyData{
@@ -506,11 +527,155 @@ func GetSkillDifficulty(category string, skillName string) string {
return "Unbekannt"
}
// contains checks if a slice contains a specific string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
//### End of Helper functions ###
// GetSpellInfo returns the school and level of a spell from the database
func GetSpellInfo(spellName string) (string, int, error) {
// Create a Spell instance to search in the database
var spell Spell
// Search for the spell in the database
err := spell.First(spellName)
if err != nil {
return "", 0, fmt.Errorf("spell '%s' not found in database: %w", spellName, err)
}
return spell.Category, spell.Stufe, nil
}
// GetSpecialization returns the specialization school for a character (placeholder)
// This should be implemented to get the actual specialization from character data
func GetSpecialization(characterID string) string {
// TODO: Implement actual character specialization lookup
// For now, return a default specialization
return "Beherrschen"
}
// findBestCategoryForSkillImprovement findet die Kategorie mit den niedrigsten EP-Kosten für eine Fertigkeit
func findBestCategoryForSkillImprovement(skillName, characterClass string, level int) (string, string, error) {
classKey := characterClass
// Sammle alle Kategorien und Schwierigkeiten, in denen die Fertigkeit verfügbar ist
type categoryOption struct {
category string
difficulty string
epCost int
}
var options []categoryOption
for category, difficulties := range learningCostsData.ImprovementCost {
for difficulty, data := range difficulties {
if contains(data.Skills, skillName) {
// Prüfe ob EP-Kosten für diese Kategorie und Klasse existieren
epPerTE, exists := learningCostsData.EPPerTE[classKey][category]
if exists {
// Hole die Trainingskosten für level
trainCost, hasCost := data.TrainCosts[level]
if hasCost {
totalEP := epPerTE * trainCost
options = append(options, categoryOption{
category: category,
difficulty: difficulty,
epCost: totalEP,
})
}
}
}
}
}
if len(options) == 0 {
return "", "", fmt.Errorf("keine verfügbare Kategorie für Fertigkeit '%s' und Klasse '%s' auf Level %d gefunden", skillName, characterClass, level)
}
// Finde die Option mit den niedrigsten EP-Kosten
bestOption := options[0]
for _, option := range options[1:] {
if option.epCost < bestOption.epCost {
bestOption = option
}
}
return bestOption.category, bestOption.difficulty, nil
}
// 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
type categoryOption struct {
category string
difficulty string
epCost int
}
var options []categoryOption
for category, difficulties := range learningCostsData.ImprovementCost {
for difficulty, data := range difficulties {
if contains(data.Skills, skillName) {
// Prüfe ob EP-Kosten für diese Kategorie und Klasse existieren
epPerTE, exists := learningCostsData.EPPerTE[classKey][category]
if exists {
// Für das Lernen verwenden wir LearnCost * 3
learnCost := data.LearnCost
totalEP := epPerTE * learnCost * 3
options = append(options, categoryOption{
category: category,
difficulty: difficulty,
epCost: totalEP,
})
}
}
}
}
if len(options) == 0 {
return "", "", fmt.Errorf("keine verfügbare Kategorie für Fertigkeit '%s' und Klasse '%s' gefunden", skillName, characterClass)
}
// Finde die Option mit den niedrigsten EP-Kosten
bestOption := options[0]
for _, option := range options[1:] {
if option.epCost < bestOption.epCost {
bestOption = option
}
}
return bestOption.category, bestOption.difficulty, nil
}
func CalcSkillLernCost(costResult *SkillCostResultNew, reward *string) error {
// Berechne die Lernkosten basierend auf den aktuellen Werten im costResult
// Hier sollte die Logik zur Berechnung der Lernkosten implementiert werden
//Finde EP kosten für die Kategorie für die Charakterklasse aus learningCostsData.EPPerTE
epPerTE, ok := learningCostsData.EPPerTE[costResult.CharacterClass][costResult.Category]
// Konvertiere Vollnamen der Charakterklasse zu Abkürzungen falls nötig
//classKey := getClassAbbreviation(costResult.CharacterClass)
classKey := costResult.CharacterClass
// Wenn Kategorie und Schwierigkeit noch nicht gesetzt sind, finde die beste Option
if costResult.Category == "" || costResult.Difficulty == "" {
bestCategory, bestDifficulty, err := findBestCategoryForSkillLearning(costResult.SkillName, classKey)
if err != nil {
return err
}
costResult.Category = bestCategory
costResult.Difficulty = bestDifficulty
}
epPerTE, ok := learningCostsData.EPPerTE[classKey][costResult.Category]
if !ok {
return fmt.Errorf("EP-Kosten für Kategorie '%s' und Klasse '%s' nicht gefunden", costResult.Category, costResult.CharacterClass)
}
@@ -524,8 +689,68 @@ func CalcSkillLernCost(costResult *SkillCostResultNew, reward *string) error {
costResult.GoldCost = costResult.LE * 200 // Beispiel: 200 Gold pro LE
// Apply reward logic
if reward != nil && *reward == "noGold" {
costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung
if reward != nil {
switch *reward {
case "noGold":
costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung
case "halveep":
costResult.EP = costResult.EP / 2 // Halbe EP-Kosten
costResult.GoldCost = 0 // Keine Goldkosten bei halven EP
case "halveepnoGold":
costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung
costResult.EP = costResult.EP / 2 // Halbe EP-Kosten
case "default":
// Keine Änderungen, normale Kosten
}
}
return nil
}
// CalcSkillImproveCost berechnet die Kosten für die Verbesserung einer Fertigkeit
func CalcSkillImproveCost(costResult *SkillCostResultNew, currentLevel int, reward *string) error {
// Für Skill-Verbesserung könnten die Kosten vom aktuellen Level abhängen
//Finde EP kosten für die Kategorie für die Charakterklasse aus learningCostsData.EPPerTE
//classKey := getClassAbbreviation(costResult.CharacterClass)
classKey := costResult.CharacterClass
if costResult.TargetLevel > 0 {
currentLevel = costResult.TargetLevel - 1 // Wenn ein Ziellevel angegeben ist, verwende dieses
}
// Wenn Kategorie und Schwierigkeit noch nicht gesetzt sind, finde die beste Option
if costResult.Category == "" || costResult.Difficulty == "" {
bestCategory, bestDifficulty, err := findBestCategoryForSkillImprovement(costResult.SkillName, classKey, currentLevel+1)
if err != nil {
return err
}
costResult.Category = bestCategory
costResult.Difficulty = bestDifficulty
}
epPerTE, ok := learningCostsData.EPPerTE[classKey][costResult.Category]
if !ok {
return fmt.Errorf("EP-Kosten für Kategorie '%s' und Klasse '%s' nicht gefunden", costResult.Category, costResult.CharacterClass)
}
diffData := learningCostsData.ImprovementCost[costResult.Category][costResult.Difficulty]
trainCost := diffData.TrainCosts[currentLevel+1]
if costResult.PPUsed > 0 {
trainCost -= costResult.PPUsed // Wenn PP verwendet werden, setze die Kosten auf die PP
}
// Apply reward logic
costResult.LE = trainCost
costResult.EP = epPerTE * trainCost
costResult.GoldCost = trainCost * 20 // Beispiel: 20 Gold pro TE
if reward != nil && *reward == "halveep" {
costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung
}
if reward != nil && *reward == "halveepnoGold" {
costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung
costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung
}
return nil
@@ -535,24 +760,78 @@ func CalcSkillLernCost(costResult *SkillCostResultNew, reward *string) error {
func CalcSpellLernCost(costResult *SkillCostResultNew, reward *string) error {
// Für Zauber verwenden wir eine ähnliche Logik wie für Skills
// TODO: Implementiere spezifische Zauber-Kostenlogik wenn verfügbar
// Für jetzt verwenden wir die gleiche Logik wie für Skills
return CalcSkillLernCost(costResult, reward)
}
// CalcSkillImproveCost berechnet die Kosten für die Verbesserung einer Fertigkeit
func CalcSkillImproveCost(costResult *SkillCostResultNew, currentLevel int, reward *string) error {
// Für Skill-Verbesserung könnten die Kosten vom aktuellen Level abhängen
// TODO: Implementiere spezifische Verbesserungslogik
// Für jetzt verwenden wir die gleiche Logik wie für das Lernen
return CalcSkillLernCost(costResult, reward)
}
// contains checks if a slice contains a specific string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
classKey := costResult.CharacterClass
spellCategory, spellLevel, err := GetSpellInfo(costResult.SkillName)
if err != nil {
return fmt.Errorf("failed to get spell info: %w", err)
}
SpellEPPerLE, ok := learningCostsData.SpellEPPerLE[classKey][spellCategory]
if !ok {
return fmt.Errorf("EP-Kosten für Zauber '%s' und Klasse '%s' nicht gefunden", costResult.SkillName, classKey)
}
if classKey == "Ma" {
spezialgebiet := GetSpecialization(costResult.CharacterID)
if spellCategory == spezialgebiet {
SpellEPPerLE = 30 // Spezialgebiet für Magier
}
}
return false
trainCost := learningCostsData.SpellLEPerLevel[spellLevel] // LE pro Stufe des Zaubers
if costResult.PPUsed > 0 {
trainCost -= costResult.PPUsed // Wenn PP verwendet werden, setze die Kosten auf die PP
}
costResult.EP = trainCost * SpellEPPerLE // EP-Kosten für das Lernen des Zaubers
costResult.GoldCost = trainCost * 100 // Beispiel: 200 Gold pro LE
costResult.Category = spellCategory
costResult.Difficulty = fmt.Sprintf("Stufe %d", spellLevel) // Zauber haben keine Schwierigkeit, sondern eine Stufe
if reward != nil && *reward == "spruchrolle" {
costResult.GoldCost = 20 // 20 Gold für Jeden Versuch
costResult.EP = costResult.EP / 3 // 1/3 EP-Kosten bei Erfolg
} else {
if reward != nil && *reward == "halveep" {
costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung
}
if reward != nil && *reward == "halveepnoGold" {
costResult.EP = costResult.EP / 2 // Halbiere die EP-Kosten für diese Belohnung
costResult.GoldCost = 0 // Keine Goldkosten für diese Belohnung
}
}
return nil
}
func GetLernCostNextLevel(request *LernCostRequest, costResult *SkillCostResultNew, reward *string, level int, characterTyp string) error {
// Diese Funktion berechnet die Kosten für das Erlernen oder Verbessern einer Fertigkeit oder eines Zaubers
// abhängig von der Aktion (learn/improve) und der Belohnung.
// die Berechnung erfolgt immer für genau 1 Level
// Diese Funktion wird in GetLernCost aufgerufen.
switch {
case request.Action == "learn" && request.Type == "skill":
err := CalcSkillLernCost(costResult, request.Reward)
if err != nil {
return fmt.Errorf("fehler bei der Kostenberechnung: %w", err)
}
// extrakosten für elfen
if characterTyp == "Elf" {
costResult.EP += 6
}
case request.Action == "learn" && request.Type == "spell":
err := CalcSpellLernCost(costResult, request.Reward)
if err != nil {
return fmt.Errorf("fehler bei der Kostenberechnung: %w", err)
}
// extrakosten für elfen
if characterTyp == "Elf" {
costResult.EP += 6
}
case request.Action == "improve" && request.Type == "skill":
err := CalcSkillImproveCost(costResult, request.CurrentLevel, request.Reward)
if err != nil {
return fmt.Errorf("fehler bei der Kostenberechnung: %w", err)
}
default:
}
return nil
}
+608 -19
View File
@@ -1,9 +1,15 @@
package gsmaster
import (
"bamort/database"
"testing"
)
// Helper function to create string pointers
func stringPtr(s string) *string {
return &s
}
// TestGetSkillCategory tests the GetSkillCategory function
func TestGetSkillCategory(t *testing.T) {
tests := []struct {
@@ -452,6 +458,77 @@ func TestSkillCoverage(t *testing.T) {
t.Logf("Tested coverage for %d unique skills", len(skillsFound))
}
// TestFindBestCategoryForSkill tests the findBestCategoryForSkill function
func TestFindBestCategoryForSkill(t *testing.T) {
tests := []struct {
name string
skillName string
characterClass string
currentLevel int
expectedCategory string
expectError bool
}{
{
name: "Klettern - should choose cheapest category",
skillName: "Klettern",
characterClass: "Kr", // Krieger
currentLevel: 13, // Level 13->14
// Klettern ist in: Alltag (leicht), Halbwelt (leicht), Körper (leicht)
// Für Kr: Alltag=20 EP/TE, Halbwelt=30 EP/TE, Körper=20 EP/TE
// Level 13->14 kostet in allen 1 TE, also 20*1=20 EP für Alltag und Körper, 30*1=30 EP für Halbwelt
// Sollte Alltag oder Körper wählen (beide gleich günstig)
expectedCategory: "Alltag", // oder "Körper" - beide sind gleich günstig
expectError: false,
},
{
name: "Non-existent skill",
skillName: "NichtExistierendeFertigkeit",
characterClass: "Kr",
currentLevel: 10,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
category, difficulty, err := findBestCategoryForSkillImprovement(tt.skillName, tt.characterClass, tt.currentLevel)
if tt.expectError {
if err == nil {
t.Errorf("Expected error but got none")
}
return
}
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
// Für Klettern sind mehrere Kategorien gleich günstig, also akzeptieren wir alle
if tt.skillName == "Klettern" {
validCategories := []string{"Alltag", "Körper"} // Beide haben 20 EP/TE für Kr
found := false
for _, validCat := range validCategories {
if category == validCat {
found = true
break
}
}
if !found {
t.Errorf("Expected category to be one of %v, got %s", validCategories, category)
}
} else {
if category != tt.expectedCategory {
t.Errorf("Expected category %s, got %s", tt.expectedCategory, category)
}
}
t.Logf("Skill %s for class %s at level %d: category=%s, difficulty=%s",
tt.skillName, tt.characterClass, tt.currentLevel, category, difficulty)
})
}
}
// TestCalcSkillLernCostWithRewards tests the reward logic in CalcSkillLernCost
func TestCalcSkillLernCostWithRewards(t *testing.T) {
tests := []struct {
@@ -535,11 +612,6 @@ func TestCalcSkillLernCostWithRewards(t *testing.T) {
}
}
// Helper function to create string pointers
func stringPtr(s string) *string {
return &s
}
// TestCalcSpellLernCostWithRewards tests the reward logic in CalcSpellLernCost
/*
func TestCalcSpellLernCostWithRewards(t *testing.T) {
@@ -563,23 +635,540 @@ func TestCalcSpellLernCostWithRewards(t *testing.T) {
*/
// TestCalcSkillImproveCostWithRewards tests the reward logic in CalcSkillImproveCost
/*
func TestCalcSkillImproveCostWithRewards(t *testing.T) {
costResult := &SkillCostResultNew{
CharacterClass: "Kr", // Use abbreviation
SkillName: "Klettern",
Category: GetSkillCategory("Klettern"),
Difficulty: GetSkillDifficulty(GetSkillCategory("Klettern"), "Klettern"),
tests := []struct {
name string
skillName string
characterClass string
currentLevel int // represents the level the character currently has must be incremented by 1 when calculating the costs
ppUsed int
reward *string
expectedEP int
expectedGold int
}{
{
name: "Normal improvement to 13 without reward",
skillName: "Klettern",
characterClass: "Kr",
currentLevel: 12,
ppUsed: 0,
reward: nil,
expectedEP: 20, // Kr has 20 EP/TE for Alltag, level 12->13 costs 0 TE, so 20*0=0
expectedGold: 20, // 0 TE * 20 Gold per TE
},
{
name: "Normal improvement to 14 without reward",
skillName: "Klettern",
characterClass: "Kr",
currentLevel: 13,
ppUsed: 0,
reward: nil,
expectedEP: 40, // Kr has 20 EP/TE for Alltag, level 13->14 costs 1 TE, so 20*1=20
expectedGold: 40, // 1 TE * 20 Gold per TE
},
{
name: "Improvement with halveep reward",
skillName: "Klettern",
characterClass: "Kr",
currentLevel: 13,
ppUsed: 0,
reward: stringPtr("halveep"),
expectedEP: 20, // Kr has 20 EP/TE for Alltag, level 13->14 costs 1 TE, so 20*1=20, halved = 10
expectedGold: 40, // Gold cost not affected by halveep
},
{
name: "Improvement to 15 without reward",
skillName: "Klettern",
characterClass: "Kr",
currentLevel: 14,
ppUsed: 0,
reward: nil,
expectedEP: 100, // Kr has 20 EP/TE for Alltag, level 14->15 costs 2 TE, minus 1 PP = 1 TE, so 20*1=20
expectedGold: 100, // 1 TE * 20 Gold per TE
},
{
name: "Improvement to 15 with PP used",
skillName: "Klettern",
characterClass: "Kr",
currentLevel: 14,
ppUsed: 1,
reward: nil,
expectedEP: 80, // Kr has 20 EP/TE for Alltag, level 14->15 costs 2 TE, minus 1 PP = 1 TE, so 20*1=20
expectedGold: 80, // 1 TE * 20 Gold per TE
},
{
name: "Improvement with halveepnoGold reward",
skillName: "Klettern",
characterClass: "Kr",
currentLevel: 15,
ppUsed: 0,
reward: stringPtr("halveepnoGold"),
expectedEP: 100, // Kr has 20 EP/TE for Alltag, level 15->16 costs 5 TE, so 20*5=100, halved = 50
expectedGold: 0, // Should be 0 with halveepnoGold reward
},
}
// Test with halveep reward
err := CalcSkillImproveCost(costResult, 5, stringPtr("halveep"))
if err != nil {
t.Fatalf("Failed to calculate improvement costs: %v", err)
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
costResult := &SkillCostResultNew{
CharacterClass: tt.characterClass,
SkillName: tt.skillName,
PPUsed: tt.ppUsed,
// Lassen Sie Kategorie und Schwierigkeit leer, damit die Funktion die beste auswählt
}
if costResult.GoldCost != 0 {
t.Errorf("Expected gold cost 0 with halveep reward, got %d", costResult.GoldCost)
err := CalcSkillImproveCost(costResult, tt.currentLevel, tt.reward)
if err != nil {
t.Fatalf("Failed to calculate improvement costs: %v", err)
}
// Log the chosen category for debugging
t.Logf("Skill: %s, Class %s, Chosen category: %s, difficulty: %s", costResult.SkillName, costResult.CharacterClass, costResult.Category, costResult.Difficulty)
if costResult.EP != tt.expectedEP {
t.Errorf("Expected EP %d, got %d", tt.expectedEP, costResult.EP)
}
if costResult.GoldCost != tt.expectedGold {
t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, costResult.GoldCost)
}
})
}
}
// TestGetSpellInfo tests the GetSpellInfo function
func TestGetSpellInfo(t *testing.T) {
// Initialize test database with migration (but no test data since we don't have the preparedTestDB file)
database.SetupTestDB(true, false) // Use in-memory SQLite, no test data loading
defer database.ResetTestDB()
MigrateStructure()
// Create minimal test spell data for our test
testSpells := []Spell{
{
LookupList: LookupList{
GameSystem: "midgard",
Name: "Schlummer",
Beschreibung: "Test spell for GetSpellInfo",
Quelle: "Test",
},
Stufe: 1,
Category: "Beherrschen",
},
{
LookupList: LookupList{
GameSystem: "midgard",
Name: "Erkennen von Krankheit",
Beschreibung: "Test spell for GetSpellInfo",
Quelle: "Test",
},
Stufe: 2,
Category: "Dweomerzauber",
},
{
LookupList: LookupList{
GameSystem: "midgard",
Name: "Das Loblied",
Beschreibung: "Test spell for GetSpellInfo",
Quelle: "Test",
},
Stufe: 3,
Category: "Zauberlied",
},
}
// Insert test data directly
for _, spell := range testSpells {
if err := database.DB.Create(&spell).Error; err != nil {
t.Fatalf("Failed to create test spell: %v", err)
}
}
tests := []struct {
spellName string
expectedSchool string
expectedLevel int
expectError bool
}{
{
spellName: "Schlummer",
expectedSchool: "Beherrschen",
expectedLevel: 1,
expectError: false,
},
{
spellName: "Erkennen von Krankheit",
expectedSchool: "Dweomerzauber",
expectedLevel: 2,
expectError: false,
},
{
spellName: "Das Loblied",
expectedSchool: "Zauberlied",
expectedLevel: 3,
expectError: false,
},
{
spellName: "Unknown Spell",
expectedSchool: "", // Should error for unknown spell
expectedLevel: 0, // Should error for unknown spell
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.spellName, func(t *testing.T) {
school, level, err := GetSpellInfo(tt.spellName)
if tt.expectError {
if err == nil {
t.Errorf("Expected error for unknown spell, but got none")
}
return
}
if err != nil {
t.Fatalf("Failed to get spell info: %v", err)
}
if school != tt.expectedSchool {
t.Errorf("Expected school %s, got %s", tt.expectedSchool, school)
}
if level != tt.expectedLevel {
t.Errorf("Expected level %d, got %d", tt.expectedLevel, level)
}
})
}
}
// TestCalcSpellLernCostWithRewards tests the reward logic in CalcSpellLernCost
func TestCalcSpellLernCostWithRewards(t *testing.T) {
// Initialize test database with migration (but no test data since we don't have the preparedTestDB file)
database.SetupTestDB(true, false) // Use in-memory SQLite, no test data loading
defer database.ResetTestDB()
MigrateStructure()
// Create minimal test spell data for our test
testSpells := []Spell{
{
LookupList: LookupList{
GameSystem: "midgard",
Name: "Schlummer",
Beschreibung: "Test spell for GetSpellInfo",
Quelle: "Test",
},
Stufe: 1,
Category: "Beherrschen",
},
{
LookupList: LookupList{
GameSystem: "midgard",
Name: "Erkennen von Krankheit",
Beschreibung: "Test spell for GetSpellInfo",
Quelle: "Test",
},
Stufe: 2,
Category: "Dweomer",
},
{
LookupList: LookupList{
GameSystem: "midgard",
Name: "Das Loblied",
Beschreibung: "Test spell for GetSpellInfo",
Quelle: "Test",
},
Stufe: 3,
Category: "Zauberlied",
},
}
// Insert test data directly
for _, spell := range testSpells {
if err := database.DB.Create(&spell).Error; err != nil {
t.Fatalf("Failed to create test spell: %v", err)
}
}
tests := []struct {
name string
spellName string
characterClass string
reward *string
expectedEP int
expectedGold int
}{
{
name: "Simple spell for Magier without but specialized",
spellName: "Schlummer",
characterClass: "Ma",
reward: nil,
expectedEP: 30, // Ma has 60 EP/LE for Beherrschen, Furcht is level 1 = 1 LE, so 1*60=60
expectedGold: 100, // 1 LE * 100 Gold per LE
},
{
name: "Spell with spruchrolle no reward",
spellName: "Erkennen von Krankheit",
characterClass: "Ma",
reward: nil,
expectedEP: 120, // 60/3 for spruchrolle
expectedGold: 100, // Fixed 20 Gold for spruchrolle
},
{
name: "Spell with spruchrolle reward",
spellName: "Erkennen von Krankheit",
characterClass: "Ma",
reward: stringPtr("spruchrolle"),
expectedEP: 40, // 60/3 for spruchrolle
expectedGold: 20, // Fixed 20 Gold for spruchrolle
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
costResult := &SkillCostResultNew{
CharacterClass: tt.characterClass,
SkillName: tt.spellName,
CharacterID: "test-character",
}
err := CalcSpellLernCost(costResult, tt.reward)
if err != nil {
t.Fatalf("Failed to calculate spell costs: %v", err)
}
if costResult.EP != tt.expectedEP {
t.Errorf("Expected EP %d, got %d", tt.expectedEP, costResult.EP)
}
if costResult.GoldCost != tt.expectedGold {
t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, costResult.GoldCost)
}
})
}
}
// TestGetSpecialization tests the GetSpecialization function
func TestGetSpecialization(t *testing.T) {
tests := []struct {
name string
characterID string
expectedSpec string
}{
{
name: "Default specialization",
characterID: "123",
expectedSpec: "Beherrschen",
},
{
name: "Another character",
characterID: "456",
expectedSpec: "Beherrschen", // Currently returns default
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GetSpecialization(tt.characterID)
if result != tt.expectedSpec {
t.Errorf("Expected specialization %s, got %s", tt.expectedSpec, result)
}
})
}
}
// TestFindBestCategoryForSkillLearning tests the findBestCategoryForSkillLearning function
func TestFindBestCategoryForSkillLearning(t *testing.T) {
tests := []struct {
name string
skillName string
characterClass string
expectedCat string
expectedDiff string
expectError bool
}{
{
name: "Klettern for Assassine - should find best category",
skillName: "Klettern",
characterClass: "As",
expectedCat: "Körper", // Should prefer Körper (10 EP/TE * 1 LE * 3 = 30 EP) over Alltag (20 EP/TE * 1 LE * 3 = 60 EP)
expectedDiff: "leicht",
expectError: false,
},
{
name: "Schleichen for Spitzbube - should find Unterwelt",
skillName: "Schleichen",
characterClass: "Sp",
expectedCat: "Unterwelt", // Sp has 10 EP/TE for Unterwelt vs 30 EP/TE for Freiland
expectedDiff: "normal",
expectError: false,
},
{
name: "Invalid skill",
skillName: "NonExistentSkill",
characterClass: "As",
expectError: true,
},
{
name: "Invalid character class",
skillName: "Klettern",
characterClass: "InvalidClass",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
category, difficulty, err := findBestCategoryForSkillLearning(tt.skillName, tt.characterClass)
if tt.expectError {
if err == nil {
t.Error("Expected an error but got none")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
if category != tt.expectedCat {
t.Errorf("Expected category %s, got %s", tt.expectedCat, category)
}
if difficulty != tt.expectedDiff {
t.Errorf("Expected difficulty %s, got %s", tt.expectedDiff, difficulty)
}
})
}
}
// TestGetLernCostNextLevel tests the GetLernCostNextLevel function
func TestGetLernCostNextLevel(t *testing.T) {
tests := []struct {
name string
request *LernCostRequest
costResult *SkillCostResultNew
reward *string
level int
characterTyp string
expectError bool
expectedEP int
expectedGold int
expectedElfEP int // Expected EP bonus for Elves
}{
{
name: "Learn skill as Human",
request: &LernCostRequest{
Action: "learn",
Type: "skill",
Reward: stringPtr("default"),
},
costResult: &SkillCostResultNew{
CharacterClass: "As",
SkillName: "Klettern",
Category: "Körper",
Difficulty: "leicht",
},
level: 1,
characterTyp: "Mensch",
expectError: false,
expectedEP: 30, // 10 * 1 * 3
expectedGold: 200, // 1 * 200
},
{
name: "Learn skill as Elf - should have EP bonus",
request: &LernCostRequest{
Action: "learn",
Type: "skill",
Reward: stringPtr("default"),
},
costResult: &SkillCostResultNew{
CharacterClass: "As",
SkillName: "Klettern",
Category: "Körper",
Difficulty: "leicht",
},
level: 1,
characterTyp: "Elf",
expectError: false,
expectedEP: 30,
expectedElfEP: 6, // Additional 6 EP for Elves
expectedGold: 200,
},
{
name: "Improve skill as human",
request: &LernCostRequest{
Action: "improve",
Type: "skill",
CurrentLevel: 12,
Reward: stringPtr("default"),
},
costResult: &SkillCostResultNew{
CharacterClass: "As",
SkillName: "Klettern",
Category: "Körper",
Difficulty: "leicht",
},
level: 13,
characterTyp: "Mensch",
expectError: false,
expectedEP: 10, // 10 * 1 (TE cost for level 13)
expectedGold: 20, // 1 * 20
},
{
name: "Improve skill as Elf",
request: &LernCostRequest{
Action: "improve",
Type: "skill",
CurrentLevel: 12,
Reward: stringPtr("default"),
},
costResult: &SkillCostResultNew{
CharacterClass: "As",
SkillName: "Klettern",
Category: "Körper",
Difficulty: "leicht",
},
level: 13,
characterTyp: "Elf",
expectError: false,
expectedEP: 10, // 10 * 1 (TE cost for level 13)
expectedGold: 20, // 1 * 20
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := GetLernCostNextLevel(tt.request, tt.costResult, tt.reward, tt.level, tt.characterTyp)
if tt.expectError {
if err == nil {
t.Error("Expected an error but got none")
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
return
}
expectedTotalEP := tt.expectedEP
if tt.characterTyp == "Elf" && (tt.request.Action == "learn") {
expectedTotalEP += tt.expectedElfEP
}
if tt.costResult.EP != expectedTotalEP {
t.Errorf("Expected EP %d, got %d", expectedTotalEP, tt.costResult.EP)
}
if tt.costResult.GoldCost != tt.expectedGold {
t.Errorf("Expected gold cost %d, got %d", tt.expectedGold, tt.costResult.GoldCost)
}
})
}
}
*/
@@ -0,0 +1,90 @@
package integration_test
import (
"bamort/database"
"bamort/gsmaster"
_ "bamort/maintenance" // Import for init() function that sets up test data callbacks
"testing"
)
// TestGetSpellInfoIntegration tests the GetSpellInfo function with full database integration
func TestGetSpellInfoIntegration(t *testing.T) {
// The maintenance package init() already sets up:
// database.SetTestDataLoader(LoadPredefinedTestDataFromFile)
// database.SetMigrationCallback(migrateAllStructures)
// Initialize test database with test data loading
database.SetupTestDB(true, true) // Use in-memory SQLite with test data
defer database.ResetTestDB()
// Test with real spell names that should exist in the prepared test database
// If the prepared test database doesn't exist, these tests will fail gracefully
tests := []struct {
spellName string
expectedSchool string
expectedLevel int
expectError bool
description string
}{
{
spellName: "Licht",
expectedSchool: "Erschaffen", // Common light spell in Midgard
expectedLevel: 1,
expectError: false,
description: "Basic light spell should exist in prepared database",
},
{
spellName: "Erkennen von Krankheit",
expectedSchool: "Erkennen",
expectedLevel: 2,
expectError: false,
description: "Common diagnostic spell should exist in prepared database",
},
{
spellName: "Furcht",
expectedSchool: "Beherrschen",
expectedLevel: 1,
expectError: false,
description: "Fear spell should exist in prepared database",
},
{
spellName: "Unknown Spell That Should Not Exist",
expectedSchool: "", // Should error for unknown spell
expectedLevel: 0, // Should error for unknown spell
expectError: true,
description: "Non-existent spell should return error",
},
}
for _, tt := range tests {
t.Run(tt.spellName, func(t *testing.T) {
school, level, err := gsmaster.GetSpellInfo(tt.spellName)
if tt.expectError {
if err == nil {
t.Errorf("Expected error for %s, but got none. %s", tt.spellName, tt.description)
}
return
}
if err != nil {
// If we get an error for a spell we expect to exist, it might mean
// the prepared test database doesn't exist or doesn't contain this spell
t.Logf("Warning: Failed to get spell info for %s: %v. %s", tt.spellName, err, tt.description)
t.Logf("This might indicate that the prepared test database is missing or incomplete.")
t.Skip("Skipping test due to missing prepared test data")
return
}
if school != tt.expectedSchool {
t.Errorf("Expected school %s, got %s for spell %s", tt.expectedSchool, school, tt.spellName)
}
if level != tt.expectedLevel {
t.Errorf("Expected level %d, got %d for spell %s", tt.expectedLevel, level, tt.spellName)
}
t.Logf("✅ Successfully found spell %s: school=%s, level=%d", tt.spellName, school, level)
})
}
}
+166 -136
View File
@@ -34,28 +34,50 @@
<div class="resource-info">
<div class="resource-label">Praxispunkte</div>
<div class="resource-amount">{{ skill?.pp || 0 }} PP</div>
<div class="resource-remaining">
<small>
Verwendet: {{ ppUsed || 0 }} PP |
Verbleibend: {{ Math.max(0, (skill?.pp || 0) - (ppUsed || 0)) }} PP
</small>
</div>
<div v-if="selectedLevel && selectedPPCost > 0" class="resource-remaining">
<small :class="{ 'text-warning': remainingPP < 5, 'text-danger': remainingPP <= 0 }">
Verbleibend: {{ remainingPP }} PP
Nach Lernen: {{ remainingPP }} PP
</small>
</div>
</div>
</div>
</div> <!-- Belohnungsart auswählen -->
<div class="form-group">
<label>Belohnungsart:</label>
<select v-model="selectedRewardType" :disabled="isLoadingRewardTypes">
<option value="" disabled>
{{ isLoadingRewardTypes ? 'Lade Belohnungsarten...' : 'Belohnungsart wählen' }}
</option>
<option
v-for="rewardType in availableRewardTypes"
:key="rewardType.value"
:value="rewardType.value"
>
{{ rewardType.label }}
</option>
</select>
</div> <!-- Belohnungsart und PP-Eingabe nebeneinander -->
<div class="form-group form-row">
<div class="form-col">
<label>Lernen als Belohnung:</label>
<select v-model="selectedRewardType" :disabled="isLoadingRewardTypes">
<option value="" disabled>
{{ isLoadingRewardTypes ? 'Lade Belohnungsarten...' : 'Belohnungsart wählen' }}
</option>
<option
v-for="rewardType in availableRewardTypes"
:key="rewardType.value"
:value="rewardType.value"
>
{{ rewardType.label }}
</option>
</select>
</div>
<div class="form-col">
<label>Praxispunkte verwenden:</label>
<input
v-model.number="ppUsed"
type="number"
min="0"
:max="skill?.pp || 0"
placeholder="PP verwenden"
@input="updatePPUsage"
/>
<small class="help-text">
{{ ppUsed || 0 }} / {{ skill?.pp || 0 }} PP
</small>
</div>
</div>
<!-- Lernbare Stufen -->
@@ -71,10 +93,12 @@
>
<div class="level-header">
<span class="level-target">{{ skill?.fertigkeitswert || 0 }} {{ level.targetLevel }}</span>
<span class="level-cost" v-if="selectedRewardType === 'ep'">{{ level.epCost }} EP</span>
<span class="level-cost" v-else-if="selectedRewardType === 'gold'">{{ level.goldCost }} GS</span>
<span class="level-cost" v-if="selectedRewardType === 'default'">{{ level.epCost }} EP + {{ level.goldCost }} GS</span>
<span class="level-cost" v-else-if="selectedRewardType === 'noGold'">{{ level.epCost }} EP</span>
<span class="level-cost" v-else-if="selectedRewardType === 'halveepnoGold'">{{ Math.floor(level.epCost / 2) }} EP</span>
<span class="level-cost" v-else-if="selectedRewardType === 'pp'">{{ level.ppCost }} PP</span>
<span class="level-cost" v-else>{{ level.epCost }} EP + {{ level.ppUsed }} PP</span>
<span class="level-cost" v-else-if="selectedRewardType === 'mixed'">{{ level.epCost }} EP + {{ level.ppUsed || 0 }} PP</span>
<span class="level-cost" v-else>{{ level.epCost }} EP + {{ level.goldCost }} GS</span>
</div>
<div class="level-details" v-if="selectedRewardType === 'mixed'">
<small>PP verwenden: {{ level.ppUsed }} / {{ skill?.pp || 0 }}</small>
@@ -83,19 +107,6 @@
</div>
</div>
<!-- PP Eingabe für gemischte Belohnung -->
<div v-if="selectedRewardType === 'mixed'" class="form-group">
<label>Praxispunkte verwenden (optional):</label>
<input
v-model.number="ppUsed"
type="number"
min="0"
:max="skill?.pp || 0"
placeholder="Anzahl PP verwenden"
@input="updateMixedCosts"
/>
</div>
<!-- Notizen -->
<div class="form-group">
<label>Notizen (optional):</label>
@@ -286,6 +297,21 @@
margin-bottom: 15px;
}
.form-row {
display: flex;
gap: 15px;
align-items: flex-start;
}
.form-col {
flex: 1;
min-width: 0;
}
.form-col:last-child {
flex: 0 0 180px;
}
.form-group label {
display: block;
margin-bottom: 5px;
@@ -309,6 +335,14 @@
resize: vertical;
}
.help-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #6c757d;
font-style: italic;
}
.modal-actions {
display: flex;
justify-content: flex-end;
@@ -403,20 +437,11 @@ export default {
if (!this.selectedLevel) return 0;
const level = this.availableLevels.find(l => l.targetLevel === this.selectedLevel);
if (!level) return 0;
if (this.selectedRewardType === 'ep') {
return level.epCost;
} else if (this.selectedRewardType === 'mixed') {
const adjustedEPCost = Math.max(level.epCost - (this.ppUsed * 10), level.epCost * 0.5);
return Math.ceil(adjustedEPCost);
}
return 0;
return level ? level.epCost : 0;
},
selectedGoldCost() {
if (!this.selectedLevel || this.selectedRewardType !== 'gold') return 0;
if (!this.selectedLevel) return 0;
const level = this.availableLevels.find(l => l.targetLevel === this.selectedLevel);
return level ? level.goldCost : 0;
@@ -426,15 +451,7 @@ export default {
if (!this.selectedLevel) return 0;
const level = this.availableLevels.find(l => l.targetLevel === this.selectedLevel);
if (!level) return 0;
if (this.selectedRewardType === 'pp') {
return level.ppCost;
} else if (this.selectedRewardType === 'mixed') {
return this.ppUsed;
}
return 0;
return level ? (level.ppUsed || level.ppCost) : 0;
},
remainingEP() {
@@ -457,7 +474,7 @@ export default {
handler(newSkill) {
if (newSkill) {
this.loadRewardTypes();
this.loadLearningCosts(); // Verwende die neue Methode
// loadLearningCosts wird durch selectedRewardType watcher ausgelöst
}
},
immediate: true
@@ -466,13 +483,16 @@ export default {
handler() {
if (this.skill) {
this.loadRewardTypes();
this.loadLearningCosts(); // Verwende die neue Methode
// loadLearningCosts wird durch selectedRewardType watcher ausgelöst
}
},
immediate: true
},
selectedRewardType() {
this.updateAffordability();
// Nur hier die Lernkosten laden, wenn Belohnungsart geändert wird
if (this.selectedRewardType && this.skill) {
this.loadLearningCosts();
}
this.selectedLevel = null; // Reset selection when reward type changes
},
isVisible(newValue) {
@@ -498,7 +518,7 @@ export default {
this.availableRewardTypes = [];
if (this.skill) {
this.loadRewardTypes();
this.loadLearningCosts(); // Verwende die neue Methode
// loadLearningCosts wird automatisch durch selectedRewardType watcher ausgelöst
}
},
@@ -512,6 +532,7 @@ export default {
this.availableRewardTypes = this.getDefaultRewardTypes();
if (this.availableRewardTypes.length > 0 && !this.selectedRewardType) {
this.selectedRewardType = this.availableRewardTypes[0].value;
// loadLearningCosts wird automatisch durch selectedRewardType watcher ausgelöst
}
return;
}
@@ -546,11 +567,13 @@ export default {
if (this.availableRewardTypes.length > 0 && !this.selectedRewardType) {
this.selectedRewardType = this.availableRewardTypes[0].value;
console.log('Set default reward type to:', this.selectedRewardType);
// loadLearningCosts wird automatisch durch selectedRewardType watcher ausgelöst
} else if (this.availableRewardTypes.length === 0) {
console.warn('No reward types received from API, using fallback');
this.availableRewardTypes = this.getDefaultRewardTypes();
if (this.availableRewardTypes.length > 0 && !this.selectedRewardType) {
this.selectedRewardType = this.availableRewardTypes[0].value;
// loadLearningCosts wird automatisch durch selectedRewardType watcher ausgelöst
}
}
@@ -584,6 +607,7 @@ export default {
this.availableRewardTypes = this.getDefaultRewardTypes();
if (this.availableRewardTypes.length > 0 && !this.selectedRewardType) {
this.selectedRewardType = this.availableRewardTypes[0].value;
// loadLearningCosts wird automatisch durch selectedRewardType watcher ausgelöst
}
console.log('Using fallback reward types:', this.availableRewardTypes);
} finally {
@@ -600,13 +624,13 @@ export default {
switch (this.learningType) {
case 'learn':
rewardTypes = [
{ value: 'ep', label: 'Erfahrungspunkte verwenden' },
{ value: 'default', label: 'Erfahrungspunkte verwenden' },
{ value: 'gold', label: 'Gold verwenden' }
];
break;
case 'spell':
rewardTypes = [
{ value: 'ep', label: 'Erfahrungspunkte verwenden' },
{ value: 'default', label: 'Erfahrungspunkte verwenden' },
{ value: 'gold', label: 'Gold verwenden' },
{ value: 'pp', label: 'Praxispunkte verwenden' },
{ value: 'mixed', label: 'Gemischt (EP + PP)' },
@@ -616,7 +640,7 @@ export default {
case 'improve':
default:
rewardTypes = [
{ value: 'ep', label: 'Erfahrungspunkte verwenden' },
{ value: 'default', label: 'Erfahrungspunkte verwenden' },
{ value: 'gold', label: 'Gold verwenden' },
{ value: 'pp', label: 'Praxispunkte verwenden' },
{ value: 'mixed', label: 'Gemischt (EP + PP)' }
@@ -631,12 +655,13 @@ export default {
calculateAvailableLevels() {
if (!this.skill) return;
// Verwende die API für echte Kostenberechnung
this.loadLearningCosts();
// Diese Methode ist jetzt redundant, da loadLearningCosts()
// automatisch durch den selectedRewardType watcher aufgerufen wird
console.warn('calculateAvailableLevels() ist veraltet - verwende selectedRewardType watcher');
},
async loadLearningCosts() {
if (!this.skill) return;
if (!this.skill || !this.selectedRewardType) return;
this.isLoading = true;
try {
@@ -652,55 +677,64 @@ export default {
skill_name: this.skill.name,
skill_type: this.skill.type || 'skill',
learning_type: this.learningType,
current_level: this.skill.fertigkeitswert || 0
current_level: this.skill.fertigkeitswert || 0,
reward_type: this.selectedRewardType
});
// Verwende die bestehende Route /:id/improve/skill mit POST
// Verwende den neuen /lerncost Endpunkt mit gsmaster.LernCostRequest Struktur
const requestData = {
skillType: this.skill.type || 'skill',
char_id: parseInt(this.character.id),
name: this.skill.name,
stufe: this.skill.fertigkeitswert || 0
current_level: this.skill.fertigkeitswert || 0,
type: this.skill.type || 'skill',
action: this.learningType === 'learn' ? 'learn' : 'improve',
target_level: 0, // Wird vom Backend automatisch bis Level 18 berechnet
use_pp: this.selectedRewardType === 'mixed' ? this.ppUsed : 0,
reward: this.selectedRewardType
};
const response = await this.$api.post(`/api/characters/${this.character.id}/improve/skill`, requestData);
const response = await this.$api.post(`/api/characters/lerncost`, requestData);
console.log('Learning costs API response:', response.data);
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
// Konvertiere gsmaster.LearnCost Array zu unserem internen Format
// Konvertiere gsmaster.SkillCostResultNew Array zu unserem internen Format
const availableEP = this.character.erfahrungsschatz?.value || 0;
const availableGold = this.character.vermoegen?.goldstücke || 0;
const availablePP = this.skill?.pp || 0;
let cumulativeEP = 0;
let cumulativeGold = 0;
let cumulativePP = 0;
this.availableLevels = response.data.map(cost => {
cumulativeEP += cost.ep;
cumulativeGold += cost.money;
cumulativePP += cost.le; // LE als PP-Äquivalent
// Backend liefert bereits die korrekten Kosten basierend auf dem Belohnungstyp
let canAfford = false;
switch (this.selectedRewardType) {
case 'noGold':
case 'halveepnoGold':
canAfford = availableEP >= cost.ep;
break;
case 'pp':
canAfford = availablePP >= cost.le;
break;
case 'mixed':
canAfford = availableEP >= cost.ep && availablePP >= (cost.pp_used || 0);
break;
case 'default':
default:
canAfford = availableEP >= cost.ep && availableGold >= cost.gold_cost;
break;
}
return {
targetLevel: cost.stufe,
targetLevel: cost.target_level,
epCost: cost.ep,
goldCost: cost.money,
goldCost: cost.gold_cost,
ppCost: cost.le,
totalEpCost: cumulativeEP,
totalGoldCost: cumulativeGold,
totalPpCost: cumulativePP,
canAfford: {
ep: availableEP >= cumulativeEP,
gold: availableGold >= cumulativeGold,
pp: availablePP >= cumulativePP
}
ppUsed: cost.pp_used || 0,
canAfford: canAfford
};
});
// Aktualisiere Verfügbarkeit basierend auf dem gewählten Belohnungstyp
this.updateAffordability();
console.log('Converted level costs:', this.availableLevels);
console.log('Processed level costs for reward type', this.selectedRewardType, ':', this.availableLevels);
} else {
console.warn('No level costs returned from API, using fallback');
this.generateFallbackLevels();
@@ -728,7 +762,7 @@ export default {
},
generateFallbackLevels() {
// Fallback-Methode für Kostenberechnung
// Einfache Fallback-Methode für Kostenberechnung (nur für Notfälle)
const currentLevel = this.skill.fertigkeitswert || 0;
const maxLevel = 20;
const availableEP = this.character.erfahrungsschatz?.value || 0;
@@ -740,60 +774,39 @@ export default {
for (let targetLevel = currentLevel + 1; targetLevel <= Math.min(currentLevel + 5, maxLevel); targetLevel++) {
const levelDiff = targetLevel - currentLevel;
// Basis-Kosten (vereinfacht)
const baseEPCost = levelDiff * 100;
const baseGoldCost = levelDiff * 50;
const ppReduction = Math.floor(levelDiff * 10);
// Sehr einfache Basis-Kosten (nur als Fallback)
const epCost = levelDiff * 100;
const goldCost = levelDiff * 50;
const ppCost = levelDiff * 20;
// Kosten berechnen
const epCost = Math.max(baseEPCost - (availablePP * 10), baseEPCost * 0.5);
const goldCost = baseGoldCost;
const ppCost = Math.min(levelDiff * 2, availablePP);
// Verfügbarkeit basierend auf Belohnungstyp
let canAfford = false;
switch (this.selectedRewardType) {
case 'noGold':
case 'halveepnoGold':
canAfford = availableEP >= epCost;
break;
case 'pp':
canAfford = availablePP >= ppCost;
break;
case 'mixed':
canAfford = availableEP >= epCost && availablePP >= Math.min(levelDiff * 5, availablePP);
break;
case 'default':
default:
canAfford = availableEP >= epCost && availableGold >= goldCost;
break;
}
this.availableLevels.push({
targetLevel,
epCost: Math.ceil(epCost),
epCost,
goldCost,
ppCost,
ppUsed: 0,
canAfford: {
ep: availableEP >= epCost,
gold: availableGold >= goldCost,
pp: availablePP >= ppCost
}
canAfford
});
}
this.updateAffordability();
},
updateAffordability() {
const availableEP = this.character.erfahrungsschatz?.value || 0;
const availableGold = this.character.vermoegen?.goldstücke || 0;
const availablePP = this.skill?.pp || 0;
this.availableLevels.forEach(level => {
switch (this.selectedRewardType) {
case 'ep':
level.canAfford = level.canAfford?.ep || (availableEP >= (level.totalEpCost || level.epCost));
break;
case 'gold':
level.canAfford = level.canAfford?.gold || (availableGold >= (level.totalGoldCost || level.goldCost));
break;
case 'pp':
level.canAfford = level.canAfford?.pp || (availablePP >= (level.totalPpCost || level.ppCost));
break;
case 'mixed':
const adjustedEPCost = Math.max((level.epCost || 0) - (this.ppUsed * 10), (level.epCost || 0) * 0.5);
level.canAfford = availableEP >= adjustedEPCost && availablePP >= this.ppUsed;
level.ppUsed = this.ppUsed;
break;
default:
// Standardmäßig EP-Verfügbarkeit verwenden
level.canAfford = level.canAfford?.ep || (availableEP >= (level.totalEpCost || level.epCost));
break;
}
});
},
selectLevel(level) {
@@ -802,8 +815,25 @@ export default {
}
},
updatePPUsage() {
// Stelle sicher, dass PP-Verwendung die verfügbaren PP nicht überschreitet
const maxPP = this.skill?.pp || 0;
if (this.ppUsed > maxPP) {
this.ppUsed = maxPP;
}
if (this.ppUsed < 0) {
this.ppUsed = 0;
}
// Bei gemischten Kosten oder PP-Belohnung: Neue Kosten vom Backend laden
if (this.selectedRewardType === 'mixed' || this.selectedRewardType === 'pp') {
this.loadLearningCosts();
}
},
updateMixedCosts() {
this.updateAffordability();
// Diese Methode ist jetzt redundant, da updatePPUsage() alles übernimmt
this.updatePPUsage();
},
async executeDetailedLearning() {