Learning lists are filled with right values
This commit is contained in:
@@ -2089,14 +2089,28 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Berechne Lernkosten mit calculateSkillLearnCostNewSystem
|
||||
// For character creation (CharId = 0), use learning costs instead of improvement costs
|
||||
var epCost, goldCost int
|
||||
if baseRequest.CharId == 0 {
|
||||
// Character creation: use basic learning costs from skillLearningInfo
|
||||
learnCost := skillLearningInfo.LearnCost
|
||||
if learnCost == 0 {
|
||||
learnCost = 50 // Default learning cost
|
||||
}
|
||||
|
||||
// For character creation, costs are much lower - just the basic learning cost
|
||||
epCost = learnCost * 2 // Simple formula: learning cost * 2 for EP
|
||||
goldCost = learnCost * 5 // Simple formula: learning cost * 5 for gold
|
||||
} else {
|
||||
// Existing character improvement: use the full system
|
||||
err = calculateSkillLearnCostNewSystem(&request, &levelResult, &remainingPP, &remainingGold, skillLearningInfo)
|
||||
epCost := 10000 // Fallback-Wert
|
||||
goldCost := 50000 // Fallback-Wert
|
||||
epCost = 10000 // Fallback-Wert for improvements
|
||||
goldCost = 50000 // Fallback-Wert for improvements
|
||||
if err == nil {
|
||||
epCost = levelResult.EP
|
||||
goldCost = levelResult.GoldCost
|
||||
}
|
||||
}
|
||||
|
||||
skillInfo := gin.H{
|
||||
"name": skill.Name,
|
||||
@@ -2117,6 +2131,226 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetAvailableSkillsForCreation returns skills with learning costs for character creation
|
||||
func GetAvailableSkillsForCreation(c *gin.Context) {
|
||||
var request struct {
|
||||
CharacterClass string `json:"characterClass" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
logger.Warn("HTTP Fehler 400: Ungültige Anfrageparameter: %v", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "Ungültige Anfrageparameter",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("GetAvailableSkillsForCreation - CharacterClass: %s", request.CharacterClass)
|
||||
|
||||
// Get all available skills with their learning costs
|
||||
skillsByCategory, err := GetAllSkillsWithLE()
|
||||
if err != nil {
|
||||
logger.Error("Fehler beim Abrufen der Fertigkeiten: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Fehler beim Abrufen der Fertigkeiten",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("GetAvailableSkillsForCreation - Gefundene Kategorien: %d", len(skillsByCategory))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"skills_by_category": skillsByCategory,
|
||||
})
|
||||
}
|
||||
func GetAllSkillsWithLE() (map[string][]gin.H, error) {
|
||||
// Get all skill categories from database
|
||||
var skillCategories []models.SkillCategory
|
||||
if err := database.DB.Find(&skillCategories).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skillsByCategory := make(map[string][]gin.H)
|
||||
|
||||
// For each category, find all skills that can be learned in that category
|
||||
for _, category := range skillCategories {
|
||||
skillsByCategory[category.Name] = []gin.H{}
|
||||
|
||||
// Query all skill-category-difficulty combinations for this category
|
||||
var skillCategoryDifficulties []models.SkillCategoryDifficulty
|
||||
err := database.DB.Preload("Skill").Preload("SkillDifficulty").
|
||||
Where("skill_category_id = ?", category.ID).
|
||||
Find(&skillCategoryDifficulties).Error
|
||||
|
||||
if err != nil {
|
||||
continue // Skip this category if there's an error
|
||||
}
|
||||
|
||||
// For each skill in this category, add it with its LE cost and difficulty
|
||||
for _, scd := range skillCategoryDifficulties {
|
||||
// Skip Placeholder skills
|
||||
if category.Name == "Unbekannt" || scd.Skill.Name == "Placeholder" || scd.Skill.InnateSkill {
|
||||
continue
|
||||
}
|
||||
|
||||
skillInfo := gin.H{
|
||||
"name": scd.Skill.Name,
|
||||
"leCost": scd.LearnCost,
|
||||
"difficulty": scd.SkillDifficulty.Name,
|
||||
}
|
||||
|
||||
skillsByCategory[category.Name] = append(skillsByCategory[category.Name], skillInfo)
|
||||
}
|
||||
}
|
||||
|
||||
return skillsByCategory, nil
|
||||
}
|
||||
|
||||
// GetAllSkillsWithLearningCosts returns all skills with their basic learning costs for all possible categories
|
||||
func GetAllSkillsWithLearningCosts(characterClass string) (map[string][]gin.H, error) {
|
||||
skills, err := models.SelectSkills("", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
skillsByCategory := make(map[string][]gin.H)
|
||||
|
||||
// Define all possible categories for skills
|
||||
allCategories := []string{"Alltag", "Kampf", "Körper", "Sozial", "Wissen", "Halbwelt", "Unterwelt", "Freiland", "Sonstige"}
|
||||
|
||||
for _, skill := range skills {
|
||||
// Skip Placeholder skills
|
||||
if skill.Name == "Placeholder" {
|
||||
continue
|
||||
}
|
||||
|
||||
// First, always add to the skill's original category
|
||||
originalCategory := skill.Category
|
||||
if originalCategory == "" {
|
||||
originalCategory = "Sonstige"
|
||||
}
|
||||
|
||||
// Try to get the best category and learning cost for this skill and character class
|
||||
bestCategory, difficulty, err := gsmaster.FindBestCategoryForSkillLearningOld(skill.Name, characterClass)
|
||||
|
||||
var learnCost int
|
||||
if err == nil && bestCategory != "" {
|
||||
// Use the difficulty as a basis for learning cost
|
||||
switch difficulty {
|
||||
case "Leicht":
|
||||
learnCost = 1
|
||||
case "Normal":
|
||||
learnCost = 2
|
||||
case "Schwer":
|
||||
learnCost = 4
|
||||
case "Sehr Schwer":
|
||||
learnCost = 10
|
||||
default:
|
||||
learnCost = 50 // Default fallback
|
||||
}
|
||||
|
||||
// Add to the best category
|
||||
skillInfo := gin.H{
|
||||
"name": skill.Name,
|
||||
"learnCost": learnCost,
|
||||
}
|
||||
skillsByCategory[bestCategory] = append(skillsByCategory[bestCategory], skillInfo)
|
||||
|
||||
// If the best category is different from original, also add to original with higher cost
|
||||
if bestCategory != originalCategory {
|
||||
skillInfoOriginal := gin.H{
|
||||
"name": skill.Name,
|
||||
"learnCost": learnCost * 2, // Higher cost for non-optimal category
|
||||
}
|
||||
skillsByCategory[originalCategory] = append(skillsByCategory[originalCategory], skillInfoOriginal)
|
||||
}
|
||||
} else {
|
||||
// Fallback: add to original category only
|
||||
skillInfo := gin.H{
|
||||
"name": skill.Name,
|
||||
"learnCost": 50, // Default learning cost
|
||||
}
|
||||
skillsByCategory[originalCategory] = append(skillsByCategory[originalCategory], skillInfo)
|
||||
}
|
||||
|
||||
// Try to add skill to other logical categories with higher costs
|
||||
// This allows more flexibility in character creation
|
||||
for _, category := range allCategories {
|
||||
if category == bestCategory || category == originalCategory {
|
||||
continue // Already added
|
||||
}
|
||||
|
||||
// Only add to certain categories if it makes sense
|
||||
if shouldSkillBeInCategory(skill.Name, category) {
|
||||
higherCost := learnCost
|
||||
if higherCost == 0 {
|
||||
higherCost = 50
|
||||
}
|
||||
higherCost = higherCost * 3 // Much higher cost for cross-category learning
|
||||
|
||||
skillInfo := gin.H{
|
||||
"name": skill.Name,
|
||||
"learnCost": higherCost,
|
||||
}
|
||||
skillsByCategory[category] = append(skillsByCategory[category], skillInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return skillsByCategory, nil
|
||||
}
|
||||
|
||||
// shouldSkillBeInCategory determines if a skill should be available in a given category
|
||||
func shouldSkillBeInCategory(skillName, category string) bool {
|
||||
// Define which skills can appear in which categories
|
||||
skillCategoryMap := map[string][]string{
|
||||
// Physical skills can appear in multiple categories
|
||||
"Athletik": {"Körper", "Kampf", "Freiland"},
|
||||
"Klettern": {"Körper", "Freiland", "Alltag"},
|
||||
"Schwimmen": {"Körper", "Freiland", "Alltag"},
|
||||
"Laufen": {"Körper", "Kampf", "Freiland"},
|
||||
"Akrobatik": {"Körper", "Kampf"},
|
||||
|
||||
// Combat skills
|
||||
"Dolch": {"Kampf", "Halbwelt"},
|
||||
"Schwert": {"Kampf"},
|
||||
"Bogen": {"Kampf", "Freiland"},
|
||||
|
||||
// Social skills
|
||||
"Menschenkenntnis": {"Sozial", "Halbwelt"},
|
||||
"Verführen": {"Sozial", "Halbwelt"},
|
||||
"Anführen": {"Sozial", "Kampf"},
|
||||
|
||||
// Knowledge skills
|
||||
"Schreiben": {"Wissen", "Alltag"},
|
||||
"Sprache": {"Wissen", "Sozial"},
|
||||
"Naturkunde": {"Wissen", "Freiland"},
|
||||
|
||||
// Stealth and underworld
|
||||
"Schleichen": {"Halbwelt", "Freiland", "Kampf"},
|
||||
"Tarnen": {"Halbwelt", "Freiland", "Kampf"},
|
||||
"Stehlen": {"Halbwelt"},
|
||||
|
||||
// Survival and wilderness
|
||||
"Überleben": {"Freiland", "Alltag"},
|
||||
"Spurensuche": {"Freiland", "Halbwelt"},
|
||||
"Orientierung": {"Freiland", "Alltag"},
|
||||
}
|
||||
|
||||
categories, exists := skillCategoryMap[skillName]
|
||||
if !exists {
|
||||
return false // Only add skills we explicitly define
|
||||
}
|
||||
|
||||
for _, cat := range categories {
|
||||
if cat == category {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAvailableSpellsNewSystem gibt alle verfügbaren Zauber mit Lernkosten zurück (POST mit LernCostRequest)
|
||||
func GetAvailableSpellsNewSystem(c *gin.Context) {
|
||||
//characterID := c.Param("id")
|
||||
|
||||
@@ -256,3 +256,296 @@ func TestImproveSkillHandler(t *testing.T) {
|
||||
assert.Contains(t, response["error"], "Charakter nicht gefunden")
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetAvailableSkillsNewSystem(t *testing.T) {
|
||||
// Setup test environment
|
||||
original := os.Getenv("ENVIRONMENT")
|
||||
os.Setenv("ENVIRONMENT", "test")
|
||||
t.Cleanup(func() {
|
||||
if original != "" {
|
||||
os.Setenv("ENVIRONMENT", original)
|
||||
} else {
|
||||
os.Unsetenv("ENVIRONMENT")
|
||||
}
|
||||
})
|
||||
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
err := models.MigrateStructure()
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("GetAvailableSkillsForCharacterCreation", func(t *testing.T) {
|
||||
// Test data - the exact request format for character creation
|
||||
requestData := map[string]interface{}{
|
||||
"CharId": 0,
|
||||
"name": "",
|
||||
"current_level": 0,
|
||||
"target_level": 1,
|
||||
"type": "skill",
|
||||
"action": "learn",
|
||||
"use_pp": 0,
|
||||
"use_gold": 0,
|
||||
"reward": "default",
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(requestData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create test request
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-skills-new", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Call the handler
|
||||
GetAvailableSkillsNewSystem(c)
|
||||
|
||||
// Verify response
|
||||
assert.Equal(t, http.StatusOK, w.Code, "Should return 200 OK for character creation request")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check response structure
|
||||
assert.Contains(t, response, "skills_by_category", "Response should contain skills_by_category")
|
||||
|
||||
skillsByCategory, ok := response["skills_by_category"].(map[string]interface{})
|
||||
assert.True(t, ok, "skills_by_category should be a map")
|
||||
|
||||
// Verify we have reasonable costs for character creation
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should have at least some skill categories")
|
||||
|
||||
// Check that costs are reasonable for character creation (not the high fallback values)
|
||||
for _, skills := range skillsByCategory {
|
||||
if skillsList, ok := skills.([]interface{}); ok {
|
||||
for _, skill := range skillsList {
|
||||
if skillMap, ok := skill.(map[string]interface{}); ok {
|
||||
epCost := skillMap["epCost"].(float64)
|
||||
goldCost := skillMap["goldCost"].(float64)
|
||||
|
||||
// Verify costs are reasonable for character creation (not fallback values)
|
||||
assert.Less(t, epCost, 1000.0, "EP cost should be reasonable for character creation")
|
||||
assert.Less(t, goldCost, 1000.0, "Gold cost should be reasonable for character creation")
|
||||
assert.Greater(t, epCost, 0.0, "EP cost should be positive")
|
||||
assert.Greater(t, goldCost, 0.0, "Gold cost should be positive")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should return at least some skill categories")
|
||||
|
||||
// Check that each category contains skills with proper structure
|
||||
for categoryName, categorySkills := range skillsByCategory {
|
||||
assert.NotEmpty(t, categoryName, "Category name should not be empty")
|
||||
|
||||
skillsArray, ok := categorySkills.([]interface{})
|
||||
assert.True(t, ok, "Category skills should be an array")
|
||||
|
||||
if len(skillsArray) > 0 {
|
||||
// Check first skill structure
|
||||
firstSkill, ok := skillsArray[0].(map[string]interface{})
|
||||
assert.True(t, ok, "Skill should be a map")
|
||||
|
||||
// Verify skill has required fields
|
||||
assert.Contains(t, firstSkill, "name", "Skill should have name field")
|
||||
assert.Contains(t, firstSkill, "epCost", "Skill should have epCost field")
|
||||
assert.Contains(t, firstSkill, "goldCost", "Skill should have goldCost field")
|
||||
|
||||
// Verify field types
|
||||
assert.IsType(t, "", firstSkill["name"], "Name should be string")
|
||||
assert.IsType(t, float64(0), firstSkill["epCost"], "epCost should be numeric")
|
||||
assert.IsType(t, float64(0), firstSkill["goldCost"], "goldCost should be numeric")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GetAvailableSkillsInvalidRequest", func(t *testing.T) {
|
||||
// Test with missing required fields (type, action, reward)
|
||||
requestData := map[string]interface{}{
|
||||
"CharId": 0, // CharId 0 is valid for character creation
|
||||
"name": "",
|
||||
"current_level": 0,
|
||||
"target_level": 1,
|
||||
// Missing "type", "action", and "reward" - should fail
|
||||
"use_pp": 0,
|
||||
"use_gold": 0,
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(requestData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-skills-new", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
GetAvailableSkillsNewSystem(c)
|
||||
|
||||
// Should return 400 Bad Request for missing required fields
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code, "Should return 400 for missing required fields")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "error")
|
||||
assert.True(t, len(response["error"].(string)) > 0, "Error message should not be empty")
|
||||
})
|
||||
|
||||
t.Run("GetAvailableSkillsInvalidRewardType", func(t *testing.T) {
|
||||
// Test with invalid reward type
|
||||
requestData := map[string]interface{}{
|
||||
"CharId": 0,
|
||||
"name": "",
|
||||
"current_level": 0,
|
||||
"target_level": 1,
|
||||
"type": "skill",
|
||||
"action": "learn",
|
||||
"use_pp": 0,
|
||||
"use_gold": 0,
|
||||
"reward": "invalid",
|
||||
}
|
||||
|
||||
requestBody, err := json.Marshal(requestData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-skills-new", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
GetAvailableSkillsNewSystem(c)
|
||||
|
||||
// Should return 400 Bad Request for invalid reward type
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code, "Should return 400 for invalid reward type")
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "error")
|
||||
})
|
||||
}
|
||||
|
||||
// TestGetAvailableSkillsForCreation tests the character creation skills endpoint
|
||||
func TestGetAvailableSkillsForCreation(t *testing.T) {
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
characterClass string
|
||||
expectStatus int
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "ValidCharacterClass",
|
||||
characterClass: "As",
|
||||
expectStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "MagierCharacterClass",
|
||||
characterClass: "Ma",
|
||||
expectStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "EmptyCharacterClass",
|
||||
characterClass: "",
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create request
|
||||
requestData := gin.H{
|
||||
"characterClass": tt.characterClass,
|
||||
}
|
||||
requestBody, _ := json.Marshal(requestData)
|
||||
|
||||
// Create HTTP request
|
||||
req, _ := http.NewRequest("POST", "/api/characters/available-skills-creation", bytes.NewBuffer(requestBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
|
||||
// Create response recorder
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Call the handler directly (it will handle JSON parsing internally)
|
||||
GetAvailableSkillsForCreation(c)
|
||||
|
||||
// Verify response
|
||||
assert.Equal(t, tt.expectStatus, w.Code)
|
||||
|
||||
if !tt.expectError {
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check response structure
|
||||
assert.Contains(t, response, "skills_by_category")
|
||||
|
||||
skillsByCategory, ok := response["skills_by_category"].(map[string]interface{})
|
||||
assert.True(t, ok)
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should have at least some skill categories")
|
||||
|
||||
// Verify skills have learnCost field
|
||||
hasNonDefaultCost := false
|
||||
for categoryName, skills := range skillsByCategory {
|
||||
if skillsList, ok := skills.([]interface{}); ok {
|
||||
for _, skill := range skillsList {
|
||||
if skillMap, ok := skill.(map[string]interface{}); ok {
|
||||
assert.Contains(t, skillMap, "name", "Skill should have name")
|
||||
assert.Contains(t, skillMap, "learnCost", "Skill should have learnCost")
|
||||
|
||||
learnCost := skillMap["learnCost"].(float64)
|
||||
assert.Greater(t, learnCost, 0.0, "Learn cost should be positive")
|
||||
assert.Less(t, learnCost, 500.0, "Learn cost should be reasonable for character creation")
|
||||
|
||||
// Check if we have skills with non-default costs
|
||||
if learnCost != 50 {
|
||||
hasNonDefaultCost = true
|
||||
}
|
||||
|
||||
// Log individual skill costs for debugging
|
||||
if tt.characterClass == "Ma" {
|
||||
t.Logf("Skill: %s, Category: %s, LearnCost: %.0f", skillMap["name"], categoryName, learnCost)
|
||||
}
|
||||
}
|
||||
}
|
||||
if tt.characterClass == "Ma" {
|
||||
break // Only log first category for Ma
|
||||
}
|
||||
}
|
||||
_ = categoryName // Mark as used
|
||||
}
|
||||
|
||||
// For Ma class, we should get some skills with different costs than the default 50
|
||||
if tt.characterClass == "Ma" {
|
||||
// This is more of an informational test - we want to see what costs we get
|
||||
t.Logf("Ma class - has skills with non-default costs: %v", hasNonDefaultCost)
|
||||
} // Log some sample data for verification
|
||||
t.Logf("Character creation skills loaded for class %s: %d categories", tt.characterClass, len(skillsByCategory))
|
||||
} else {
|
||||
// For error cases, verify error response
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
// Fertigkeiten-Information
|
||||
charGrp.GET("/:id/available-skills", GetAvailableSkillsOld) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
||||
charGrp.POST("/available-skills-new", GetAvailableSkillsNewSystem) // Verfügbare Fertigkeiten mit Kosten (bereits gelernte ausgeschlossen)
|
||||
charGrp.POST("/available-skills-creation", GetAvailableSkillsForCreation) // Verfügbare Fertigkeiten mit Lernkosten für Charaktererstellung
|
||||
charGrp.POST("/available-spells-new", GetAvailableSpellsNewSystem) // Verfügbare Zauber mit Kosten (bereits gelernte ausgeschlossen)
|
||||
charGrp.GET("/spell-details", GetSpellDetails) // Detaillierte Informationen zu einem bestimmten Zauber
|
||||
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"bamort/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestGetAllSkillsWithLearningCosts tests the helper function directly
|
||||
func TestGetAllSkillsWithLearningCosts(t *testing.T) {
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
characterClass string
|
||||
expectError bool
|
||||
expectSkills bool
|
||||
}{
|
||||
{
|
||||
name: "ValidCharacterClassAs",
|
||||
characterClass: "As",
|
||||
expectError: false,
|
||||
expectSkills: true,
|
||||
},
|
||||
{
|
||||
name: "ValidCharacterClassMagier",
|
||||
characterClass: "Magier",
|
||||
expectError: false,
|
||||
expectSkills: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyCharacterClass",
|
||||
characterClass: "",
|
||||
expectError: false,
|
||||
expectSkills: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
skillsByCategory, err := GetAllSkillsWithLearningCosts(tt.characterClass)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
assert.NoError(t, err, "GetAllSkillsWithLearningCosts should not return error")
|
||||
|
||||
if tt.expectSkills {
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should return at least some skill categories")
|
||||
|
||||
// Verify structure of returned data
|
||||
for categoryName, skills := range skillsByCategory {
|
||||
assert.NotEmpty(t, categoryName, "Category name should not be empty")
|
||||
|
||||
skillsArray := skills
|
||||
assert.Greater(t, len(skillsArray), 0, "Category should have at least one skill")
|
||||
|
||||
// Check first skill structure
|
||||
if len(skillsArray) > 0 {
|
||||
skill := skillsArray[0]
|
||||
assert.Contains(t, skill, "name", "Skill should have name")
|
||||
assert.Contains(t, skill, "learnCost", "Skill should have learnCost")
|
||||
|
||||
name, nameOk := skill["name"].(string)
|
||||
assert.True(t, nameOk, "Skill name should be string")
|
||||
assert.NotEmpty(t, name, "Skill name should not be empty")
|
||||
|
||||
learnCost, costOk := skill["learnCost"].(int)
|
||||
assert.True(t, costOk, "Learn cost should be int")
|
||||
assert.Greater(t, learnCost, 0, "Learn cost should be positive")
|
||||
assert.LessOrEqual(t, learnCost, 500, "Learn cost should be reasonable")
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Character class %s: Found %d skill categories", tt.characterClass, len(skillsByCategory))
|
||||
|
||||
// Log some sample skills for verification
|
||||
count := 0
|
||||
for categoryName, skills := range skillsByCategory {
|
||||
skillsArray := skills
|
||||
for _, skill := range skillsArray {
|
||||
skillMap := skill
|
||||
t.Logf(" %s -> %s: %v LP", categoryName, skillMap["name"], skillMap["learnCost"])
|
||||
count++
|
||||
if count >= 5 { // Only log first 5 skills
|
||||
break
|
||||
}
|
||||
}
|
||||
if count >= 5 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAvailableSkillsForCreationHandler tests the HTTP handler directly
|
||||
func TestGetAvailableSkillsForCreationHandler(t *testing.T) {
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
requestBody interface{}
|
||||
expectStatus int
|
||||
expectError bool
|
||||
validateFunc func(t *testing.T, response map[string]interface{})
|
||||
}{
|
||||
{
|
||||
name: "ValidMagierRequest",
|
||||
requestBody: gin.H{
|
||||
"characterClass": "Magier",
|
||||
},
|
||||
expectStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
validateFunc: func(t *testing.T, response map[string]interface{}) {
|
||||
assert.Contains(t, response, "skills_by_category")
|
||||
skillsByCategory := response["skills_by_category"].(map[string]interface{})
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should have skill categories")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ValidAsRequest",
|
||||
requestBody: gin.H{
|
||||
"characterClass": "As",
|
||||
},
|
||||
expectStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
validateFunc: func(t *testing.T, response map[string]interface{}) {
|
||||
assert.Contains(t, response, "skills_by_category")
|
||||
skillsByCategory := response["skills_by_category"].(map[string]interface{})
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should have skill categories")
|
||||
|
||||
// Verify that skills have reasonable learning costs
|
||||
hasReasonableCosts := false
|
||||
for _, skills := range skillsByCategory {
|
||||
if skillsList, ok := skills.([]interface{}); ok {
|
||||
for _, skill := range skillsList {
|
||||
if skillMap, ok := skill.(map[string]interface{}); ok {
|
||||
if learnCost, exists := skillMap["learnCost"]; exists {
|
||||
if cost, ok := learnCost.(float64); ok {
|
||||
assert.Greater(t, cost, 0.0, "Learn cost should be positive")
|
||||
assert.LessOrEqual(t, cost, 500.0, "Learn cost should be reasonable")
|
||||
hasReasonableCosts = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.True(t, hasReasonableCosts, "Should find skills with reasonable costs")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyCharacterClass",
|
||||
requestBody: gin.H{
|
||||
"characterClass": "",
|
||||
},
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectError: true,
|
||||
validateFunc: func(t *testing.T, response map[string]interface{}) {
|
||||
assert.Contains(t, response, "error")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MissingCharacterClass",
|
||||
requestBody: gin.H{
|
||||
"someOtherField": "value",
|
||||
},
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectError: true,
|
||||
validateFunc: func(t *testing.T, response map[string]interface{}) {
|
||||
assert.Contains(t, response, "error")
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
requestBody: "invalid json string",
|
||||
expectStatus: http.StatusBadRequest,
|
||||
expectError: true,
|
||||
validateFunc: func(t *testing.T, response map[string]interface{}) {
|
||||
assert.Contains(t, response, "error")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Prepare request body
|
||||
var requestBody []byte
|
||||
var err error
|
||||
|
||||
if str, ok := tt.requestBody.(string); ok {
|
||||
requestBody = []byte(str)
|
||||
} else {
|
||||
requestBody, err = json.Marshal(tt.requestBody)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", "/api/characters/available-skills-creation", bytes.NewBuffer(requestBody))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create response recorder
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Call the handler
|
||||
GetAvailableSkillsForCreation(c)
|
||||
|
||||
// Verify response status
|
||||
assert.Equal(t, tt.expectStatus, w.Code, "HTTP status should match expected")
|
||||
|
||||
// Parse response body
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err, "Response should be valid JSON")
|
||||
|
||||
// Run custom validation if provided
|
||||
if tt.validateFunc != nil {
|
||||
tt.validateFunc(t, response)
|
||||
}
|
||||
|
||||
// Log response for debugging
|
||||
if !tt.expectError {
|
||||
t.Logf("Response for %s: %v", tt.name, response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetAllSkillsWithLearningCosts benchmarks the skills loading function
|
||||
func BenchmarkGetAllSkillsWithLearningCosts(b *testing.B) {
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
characterClass := "Magier"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GetAllSkillsWithLearningCosts(characterClass)
|
||||
if err != nil {
|
||||
b.Fatalf("GetAllSkillsWithLearningCosts failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSkillsCreationEndpointIntegration tests the full integration
|
||||
func TestSkillsCreationEndpointIntegration(t *testing.T) {
|
||||
// Setup test database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
// Test different character classes
|
||||
characterClasses := []string{"As", "Magier", "Krieger", "Spitzbube"}
|
||||
|
||||
for _, class := range characterClasses {
|
||||
t.Run("CharacterClass_"+class, func(t *testing.T) {
|
||||
// Create request
|
||||
requestData := gin.H{
|
||||
"characterClass": class,
|
||||
}
|
||||
requestBody, err := json.Marshal(requestData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create HTTP request
|
||||
req, err := http.NewRequest("POST", "/api/characters/available-skills-creation", bytes.NewBuffer(requestBody))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create response recorder
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
// Call the handler
|
||||
GetAvailableSkillsForCreation(c)
|
||||
|
||||
// Verify response
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify structure
|
||||
assert.Contains(t, response, "skills_by_category")
|
||||
skillsByCategory := response["skills_by_category"].(map[string]interface{})
|
||||
assert.Greater(t, len(skillsByCategory), 0, "Should have at least one skill category")
|
||||
|
||||
// Count total skills available
|
||||
totalSkills := 0
|
||||
for categoryName, skills := range skillsByCategory {
|
||||
if skillsList, ok := skills.([]interface{}); ok {
|
||||
totalSkills += len(skillsList)
|
||||
t.Logf("Category %s has %d skills", categoryName, len(skillsList))
|
||||
}
|
||||
}
|
||||
|
||||
assert.Greater(t, totalSkills, 0, "Should have at least some skills available")
|
||||
t.Logf("Character class %s: %d total skills in %d categories", class, totalSkills, len(skillsByCategory))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bamort/database"
|
||||
"bamort/gsmaster"
|
||||
"bamort/models"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize config and database
|
||||
database.SetupTestDB(true, true)
|
||||
defer database.ResetTestDB()
|
||||
|
||||
// Test some skills for Magier character class
|
||||
testSkills := []string{"Schreiben", "Athletik", "Lesen von Zauberformeln", "Zaubern"}
|
||||
characterClass := "Ma" // Magier abbreviation
|
||||
|
||||
fmt.Printf("Testing learning costs for character class: %s\n", characterClass)
|
||||
fmt.Println(strings.Repeat("=", 50))
|
||||
|
||||
for _, skillName := range testSkills {
|
||||
// Test the same logic as our handler
|
||||
fmt.Printf("\n--- Testing skill: %s ---\n", skillName)
|
||||
|
||||
// Try gsmaster.FindBestCategoryForSkillLearningOld
|
||||
bestCategory, difficulty, err := gsmaster.FindBestCategoryForSkillLearningOld(skillName, characterClass)
|
||||
if err == nil && bestCategory != "" {
|
||||
fmt.Printf("✅ FindBestCategory: Category=%s, Difficulty=%s\n", bestCategory, difficulty)
|
||||
} else {
|
||||
fmt.Printf("❌ FindBestCategory: ERROR: %v\n", err)
|
||||
}
|
||||
|
||||
// Try models.GetSkillCategoryAndDifficultyNewSystem
|
||||
skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, characterClass)
|
||||
if err == nil {
|
||||
fmt.Printf("✅ GetSkillCategory: Category=%s, LearnCost=%d\n",
|
||||
skillLearningInfo.CategoryName, skillLearningInfo.LearnCost)
|
||||
} else {
|
||||
fmt.Printf("❌ GetSkillCategory: ERROR: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Also test with empty character class
|
||||
fmt.Println("\nTesting with empty character class:")
|
||||
for _, skillName := range testSkills {
|
||||
skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, "")
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Skill: %s (empty class) - ERROR: %v\n", skillName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Skill: %s (empty class) - Category: %s, LearnCost: %d\n",
|
||||
skillName,
|
||||
skillLearningInfo.CategoryName,
|
||||
skillLearningInfo.LearnCost)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
type LernCostRequest struct {
|
||||
CharId uint `json:"char_id" binding:"required"` // Charakter-ID
|
||||
CharId uint `json:"char_id" binding:"omitempty"` // Charakter-ID
|
||||
Name string `json:"name" binding:"omitempty"` // 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 )
|
||||
|
||||
@@ -23,32 +23,122 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<div v-else class="skills-content">
|
||||
<!-- Typical Skills Info -->
|
||||
<div v-if="typicalSkills.length > 0" class="card" style="margin-bottom: 30px;">
|
||||
<div class="section-header">
|
||||
<h3>Empfohlene Fertigkeiten für {{ characterClass }}</h3>
|
||||
<!-- Three Column Layout -->
|
||||
<div class="three-column-grid">
|
||||
|
||||
<!-- Left Column: Available Skills for Selected Category -->
|
||||
<div>
|
||||
<div v-if="selectedCategory" class="skills-content">
|
||||
<div class="list-container">
|
||||
<div class="section-header" style="padding: 20px 20px 10px;">
|
||||
<h3>{{ getSelectedCategoryName() }} - Verfügbare Fertigkeiten</h3>
|
||||
</div>
|
||||
<p style="color: #6c757d; margin-bottom: 15px;">
|
||||
Die folgenden Fertigkeiten werden häufig von Charakteren Ihrer Klasse erlernt:
|
||||
</p>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<span
|
||||
v-for="skill in typicalSkills"
|
||||
|
||||
<!-- Loading skills -->
|
||||
<div v-if="isLoadingSkills" class="loading-message">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Lade Fertigkeiten...</p>
|
||||
</div>
|
||||
|
||||
<!-- Available skills -->
|
||||
<div v-else-if="availableSkillsForSelectedCategory.length > 0">
|
||||
<div
|
||||
v-for="skill in availableSkillsForSelectedCategory"
|
||||
:key="skill.name"
|
||||
class="badge badge-info"
|
||||
:title="`${skill.name} (${skill.attribute}) - Bonus: +${skill.bonus}`"
|
||||
class="list-item"
|
||||
:class="{ 'opacity-50': !canAffordSkillInCategory(skill) }"
|
||||
>
|
||||
{{ skill.name }} ({{ skill.attribute }})
|
||||
</span>
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-title">{{ skill.name }}</div>
|
||||
<div class="list-item-details">
|
||||
<span class="badge badge-primary">{{ skill.cost }} LE</span>
|
||||
<span v-if="skill.attribute" class="badge badge-secondary">({{ skill.attribute }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item-actions">
|
||||
<button
|
||||
@click="selectSkillForLearning(skill)"
|
||||
:disabled="!canAffordSkillInCategory(skill)"
|
||||
class="btn btn-primary"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No skills found -->
|
||||
<div v-else class="empty-state">
|
||||
<p>Keine Fertigkeiten für diese Kategorie gefunden.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No category selected -->
|
||||
<div v-else-if="learningCategories.length > 0" class="empty-state">
|
||||
<p>Wählen Sie eine Lernpunkte-Kategorie aus, um verfügbare Fertigkeiten zu sehen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center Column: Selected Skills -->
|
||||
<div>
|
||||
<div class="list-container">
|
||||
<div class="section-header" style="padding: 20px 20px 10px;">
|
||||
<h3>Gewählte Fertigkeiten</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedSkills.length > 0">
|
||||
<div
|
||||
v-for="skill in selectedSkills"
|
||||
:key="skill.name"
|
||||
class="list-item"
|
||||
>
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-title">{{ skill.name }}</div>
|
||||
<div class="list-item-details">
|
||||
<span class="badge badge-primary">{{ skill.cost }} LE</span>
|
||||
<span class="badge badge-info">{{ skill.categoryDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item-actions">
|
||||
<button
|
||||
@click="removeSkill(skill)"
|
||||
class="btn btn-danger"
|
||||
style="width: 30px; height: 30px; border-radius: 50%; padding: 0;"
|
||||
title="Fertigkeit entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="card" style="margin: 15px 20px;">
|
||||
<div class="resource-card" style="justify-content: center;">
|
||||
<div class="resource-info" style="text-align: center;">
|
||||
<div class="resource-label">Gesamt verbrauchte LE:</div>
|
||||
<div class="resource-amount">{{ totalUsedPoints }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<p>Noch keine Fertigkeiten gewählt.</p>
|
||||
<p style="font-style: italic; margin-top: 10px; font-size: 14px; color: #6c757d;">Wählen Sie eine Kategorie und fügen Sie Fertigkeiten hinzu.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Learning Points + Typical Skills -->
|
||||
<div>
|
||||
<!-- Learning Points Overview -->
|
||||
<div v-if="learningCategories.length > 0" style="margin-bottom: 30px;">
|
||||
<div class="section-header">
|
||||
<h3>Verfügbare Lernpunkte</h3>
|
||||
</div>
|
||||
<div class="grid-container grid-3-columns">
|
||||
<div style="display: flex; flex-direction: column; gap: 15px;">
|
||||
<div
|
||||
v-for="category in learningCategories"
|
||||
:key="category.name"
|
||||
@@ -69,7 +159,7 @@
|
||||
<span style="color: #28a745; font-size: 16px;">{{ category.remainingPoints }}</span>
|
||||
<span style="color: #6c757d;">/</span>
|
||||
<span style="color: #6c757d;">{{ category.totalPoints }}</span>
|
||||
<span style="color: #6c757d; font-size: 12px;">LP</span>
|
||||
<span style="color: #6c757d; font-size: 12px;">LE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,174 +168,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Skills Selection -->
|
||||
<div v-if="selectedCategory" class="skills-content">
|
||||
<div class="list-container">
|
||||
<div class="section-header" style="padding: 20px 20px 10px;">
|
||||
<h3>{{ getSelectedCategoryName() }} - Verfügbare Fertigkeiten</h3>
|
||||
</div>
|
||||
|
||||
<!-- Loading skills -->
|
||||
<div v-if="isLoadingSkills" class="loading-message">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Lade Fertigkeiten...</p>
|
||||
</div>
|
||||
|
||||
<!-- Available skills -->
|
||||
<div v-else-if="availableSkills.length > 0">
|
||||
<div
|
||||
v-for="skill in availableSkills"
|
||||
:key="skill.name"
|
||||
class="list-item"
|
||||
:class="{ 'opacity-50': !canAddSkill(skill) }"
|
||||
>
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-title">{{ skill.name }}</div>
|
||||
<div class="list-item-details">
|
||||
<span class="badge badge-primary">{{ skill.cost }} LP</span>
|
||||
<span v-if="skill.attribute" class="badge badge-secondary">({{ skill.attribute }})</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item-actions">
|
||||
<button
|
||||
@click="addSkill(skill)"
|
||||
:disabled="!canAddSkill(skill)"
|
||||
class="btn btn-primary"
|
||||
style="font-size: 12px;"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No skills found -->
|
||||
<div v-else class="empty-state">
|
||||
<p>Keine Fertigkeiten für diese Kategorie gefunden.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Selected Skills -->
|
||||
<div class="list-container">
|
||||
<div class="section-header" style="padding: 20px 20px 10px;">
|
||||
<h3>Gewählte Fertigkeiten</h3>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedSkills.length > 0">
|
||||
<div
|
||||
v-for="skill in selectedSkills"
|
||||
:key="skill.name"
|
||||
class="list-item"
|
||||
>
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-title">{{ skill.name }}</div>
|
||||
<div class="list-item-details">
|
||||
<span class="badge badge-primary">{{ skill.cost }} LP</span>
|
||||
<span class="badge badge-info">{{ skill.categoryDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-item-actions">
|
||||
<button
|
||||
@click="removeSkill(skill)"
|
||||
class="btn btn-danger"
|
||||
style="width: 30px; height: 30px; border-radius: 50%; padding: 0;"
|
||||
title="Fertigkeit entfernen"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="card" style="margin: 15px 20px;">
|
||||
<div class="resource-card" style="justify-content: center;">
|
||||
<div class="resource-info" style="text-align: center;">
|
||||
<div class="resource-label">Gesamt verbrauchte LP:</div>
|
||||
<div class="resource-amount">{{ totalUsedPoints }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="empty-state">
|
||||
<p>Noch keine Fertigkeiten gewählt.</p>
|
||||
<p style="font-style: italic; margin-top: 10px; font-size: 14px; color: #6c757d;">Wählen Sie eine Kategorie und fügen Sie Fertigkeiten hinzu.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Skills Section -->
|
||||
<div v-if="!isLoadingSkills && learningCategories.length > 0" style="margin-top: 30px;">
|
||||
<!-- Typical Skills Info -->
|
||||
<div v-if="typicalSkills.length > 0" class="card">
|
||||
<div class="section-header">
|
||||
<h3>Verfügbare Fertigkeiten</h3>
|
||||
<h3>Empfohlene Fertigkeiten für {{ characterClass }}</h3>
|
||||
</div>
|
||||
<p style="color: #6c757d; margin-bottom: 20px;">
|
||||
Wählen Sie aus den verfügbaren Fertigkeiten für jede Kategorie:
|
||||
<p style="color: #6c757d; margin-bottom: 15px;">
|
||||
Die folgenden Fertigkeiten werden häufig von Charakteren Ihrer Klasse erlernt:
|
||||
</p>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<div class="form-row" style="margin-bottom: 20px; gap: 10px; flex-wrap: wrap;">
|
||||
<button
|
||||
@click="setSkillCategoryFilter(null)"
|
||||
class="btn"
|
||||
:class="selectedSkillCategoryFilter === null ? 'btn-primary' : 'btn-secondary'"
|
||||
>
|
||||
Alle Kategorien
|
||||
</button>
|
||||
<button
|
||||
v-for="category in availableSkillCategories"
|
||||
:key="category"
|
||||
@click="setSkillCategoryFilter(category)"
|
||||
class="btn"
|
||||
:class="selectedSkillCategoryFilter === category ? 'btn-primary' : 'btn-secondary'"
|
||||
>
|
||||
{{ category }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Skills Loading State -->
|
||||
<div v-if="isLoadingSkills" class="loading-message">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Lade verfügbare Fertigkeiten...</p>
|
||||
</div>
|
||||
|
||||
<!-- Skills List -->
|
||||
<div v-else-if="filteredAvailableSkills.length > 0" class="list-container">
|
||||
<div
|
||||
v-for="skill in filteredAvailableSkills"
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||||
<span
|
||||
v-for="skill in typicalSkills"
|
||||
:key="skill.name"
|
||||
class="list-item"
|
||||
style="display: flex; justify-content: space-between; align-items: center;"
|
||||
:class="{ 'opacity-50': !canAffordSkill(skill) }"
|
||||
class="badge badge-info"
|
||||
:title="`${skill.name} (${skill.attribute}) - Bonus: +${skill.bonus}`"
|
||||
>
|
||||
<div>
|
||||
<div style="font-weight: 600; color: #2c3e50;">{{ skill.name }}</div>
|
||||
<div style="display: flex; gap: 15px; font-size: 14px; color: #6c757d; margin-top: 4px;">
|
||||
<span>{{ skill.category }}</span>
|
||||
<span style="color: #007acc; font-weight: 500;">{{ skill.epCost }} EP</span>
|
||||
<span style="color: #28a745; font-weight: 500;">{{ skill.goldCost }} GS</span>
|
||||
{{ skill.name }} ({{ skill.attribute }})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="selectSkillForLearning(skill)"
|
||||
class="btn btn-primary"
|
||||
:disabled="!canAffordSkill(skill) || isSkillSelected(skill)"
|
||||
>
|
||||
{{ isSkillSelected(skill) ? '✓' : '→' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Skills Available -->
|
||||
<div v-else-if="!isLoadingSkills" class="empty-state">
|
||||
<p>Keine Fertigkeiten verfügbar für die gewählte Kategorie.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No category selected -->
|
||||
<div v-else-if="learningCategories.length > 0" class="empty-state">
|
||||
<p>Wählen Sie eine Lernpunkte-Kategorie aus, um verfügbare Fertigkeiten zu sehen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
@@ -308,7 +251,6 @@ export default {
|
||||
|
||||
// Available skills fetching
|
||||
availableSkillsByCategory: null,
|
||||
selectedSkillCategoryFilter: null,
|
||||
|
||||
// Debug
|
||||
showDebug: false // Set to true for debugging
|
||||
@@ -345,44 +287,56 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
availableSkillCategories() {
|
||||
if (!this.availableSkillsByCategory) return []
|
||||
return Object.keys(this.availableSkillsByCategory)
|
||||
},
|
||||
|
||||
filteredAvailableSkills() {
|
||||
if (!this.availableSkillsByCategory) return []
|
||||
|
||||
let allSkills = []
|
||||
|
||||
// Collect all skills from all categories
|
||||
Object.keys(this.availableSkillsByCategory).forEach(category => {
|
||||
this.availableSkillsByCategory[category].forEach(skill => {
|
||||
allSkills.push({
|
||||
...skill,
|
||||
category: category
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Apply category filter
|
||||
if (this.selectedSkillCategoryFilter) {
|
||||
allSkills = allSkills.filter(skill => skill.category === this.selectedSkillCategoryFilter)
|
||||
// Skills for the selected category (from Learning Points section)
|
||||
availableSkillsForSelectedCategory() {
|
||||
if (!this.selectedCategory || !this.availableSkillsByCategory) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Try to find the matching category key
|
||||
const categoryKey = this.findCategoryKey(this.selectedCategory)
|
||||
if (!categoryKey) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Get skills from the selected category
|
||||
const categorySkills = this.availableSkillsByCategory[categoryKey] || []
|
||||
|
||||
const filteredSkills = categorySkills.map(skill => ({
|
||||
...skill,
|
||||
cost: this.getSkillCost(skill),
|
||||
category: categoryKey, // Use the actual category key from availableSkillsByCategory
|
||||
categoryDisplay: this.getCategoryDisplayName(this.selectedCategory)
|
||||
}))
|
||||
.filter(skill => {
|
||||
// Remove already selected skills
|
||||
const selectedSkillNames = this.selectedSkills.map(s => s.name)
|
||||
allSkills = allSkills.filter(skill => !selectedSkillNames.includes(skill.name))
|
||||
return !selectedSkillNames.includes(skill.name)
|
||||
})
|
||||
.filter(skill => this.canAffordSkillInCategory(skill))
|
||||
.sort((a, b) => {
|
||||
const aLeCost = this.getSkillCost(a)
|
||||
const bLeCost = this.getSkillCost(b)
|
||||
|
||||
return allSkills
|
||||
// First sort by LE cost (ascending)
|
||||
if (aLeCost !== bLeCost) {
|
||||
return aLeCost - bLeCost
|
||||
}
|
||||
|
||||
// If costs are equal, sort alphabetically
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
|
||||
return filteredSkills
|
||||
},
|
||||
|
||||
totalSelectedEP() {
|
||||
return this.selectedSkills.reduce((total, skill) => total + (skill.epCost || 0), 0)
|
||||
return this.selectedSkills.reduce((total, skill) => total + this.getSkillCost(skill), 0)
|
||||
},
|
||||
|
||||
totalSelectedGold() {
|
||||
return this.selectedSkills.reduce((total, skill) => total + (skill.goldCost || 0), 0)
|
||||
// For character creation, we only track learning costs, not gold costs
|
||||
return 0
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -457,12 +411,8 @@ export default {
|
||||
'Alltag': 'Alltag',
|
||||
'Kampf': 'Kampf',
|
||||
'Körper': 'Körper',
|
||||
'Gesellschaft': 'Gesellschaft',
|
||||
'Sozial': 'Sozial',
|
||||
'Natur': 'Natur',
|
||||
'Wissen': 'Wissen',
|
||||
'Handwerk': 'Handwerk',
|
||||
'Gaben': 'Gaben',
|
||||
'Halbwelt': 'Halbwelt',
|
||||
'Unterwelt': 'Unterwelt',
|
||||
'Freiland': 'Freiland'
|
||||
@@ -489,73 +439,14 @@ export default {
|
||||
cat.name === skill.category?.toLowerCase()
|
||||
)
|
||||
if (category && skill.cost) {
|
||||
category.remainingPoints = Math.max(0, category.remainingPoints - skill.cost)
|
||||
category.remainingPoints = Math.max(0, category.remainingPoints - this.getSkillCost(skill))
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async selectCategory(categoryName) {
|
||||
this.selectedCategory = categoryName
|
||||
await this.loadSkillsForCategory(categoryName)
|
||||
},
|
||||
|
||||
async loadSkillsForCategory(categoryName) {
|
||||
try {
|
||||
this.isLoadingSkills = true
|
||||
|
||||
// Create request for skills in this category
|
||||
const request = {
|
||||
char_id: 0, // New character - send as number, not string
|
||||
name: '', // Will be set for each skill individually
|
||||
current_level: 0,
|
||||
target_level: 1,
|
||||
type: 'skill',
|
||||
action: 'learn',
|
||||
use_pp: 0,
|
||||
use_gold: 0,
|
||||
reward: 'default',
|
||||
characterClass: this.characterClass,
|
||||
category: categoryName
|
||||
}
|
||||
|
||||
console.log('Loading skills for category:', categoryName, 'with request:', request)
|
||||
|
||||
const response = await API.post('/api/characters/available-skills-new', request, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
this.availableSkills = (response.data.skills || []).map(skill => ({
|
||||
...skill,
|
||||
category: categoryName,
|
||||
categoryDisplay: this.getCategoryDisplayName(categoryName)
|
||||
}))
|
||||
|
||||
console.log('Loaded skills for category:', this.availableSkills)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading skills:', error)
|
||||
// Provide fallback dummy data for development
|
||||
this.availableSkills = [
|
||||
{
|
||||
name: 'Beispiel Fertigkeit 1',
|
||||
cost: 30,
|
||||
category: categoryName,
|
||||
categoryDisplay: this.getCategoryDisplayName(categoryName),
|
||||
attribute: 'In'
|
||||
},
|
||||
{
|
||||
name: 'Beispiel Fertigkeit 2',
|
||||
cost: 40,
|
||||
category: categoryName,
|
||||
categoryDisplay: this.getCategoryDisplayName(categoryName),
|
||||
attribute: 'Gs'
|
||||
}
|
||||
]
|
||||
} finally {
|
||||
this.isLoadingSkills = false
|
||||
}
|
||||
// Skills are now provided by computed property availableSkillsForSelectedCategory
|
||||
},
|
||||
|
||||
getSelectedCategoryName() {
|
||||
@@ -563,27 +454,55 @@ export default {
|
||||
return category ? category.displayName : this.selectedCategory
|
||||
},
|
||||
|
||||
canAddSkill(skill) {
|
||||
// Check if skill is already selected
|
||||
const alreadySelected = this.selectedSkills.some(s => s.name === skill.name)
|
||||
if (alreadySelected) return false
|
||||
findCategoryKey(selectedCategoryName) {
|
||||
if (!this.availableSkillsByCategory) return null
|
||||
|
||||
// Check if category has enough points
|
||||
const category = this.learningCategories.find(cat =>
|
||||
cat.name === skill.category?.toLowerCase()
|
||||
// Try to find by learning category mapping first (most likely scenario)
|
||||
const learningCategory = this.learningCategories?.find(cat => cat.name === selectedCategoryName)
|
||||
if (learningCategory && this.availableSkillsByCategory[learningCategory.displayName]) {
|
||||
return learningCategory.displayName
|
||||
}
|
||||
|
||||
// Try direct match
|
||||
if (this.availableSkillsByCategory[selectedCategoryName]) {
|
||||
return selectedCategoryName
|
||||
}
|
||||
|
||||
// Try case-insensitive search
|
||||
const availableKeys = Object.keys(this.availableSkillsByCategory)
|
||||
const foundKey = availableKeys.find(key =>
|
||||
key.toLowerCase() === selectedCategoryName.toLowerCase()
|
||||
)
|
||||
if (!category) return false
|
||||
|
||||
return category.remainingPoints >= (skill.cost || 0)
|
||||
return foundKey || null
|
||||
},
|
||||
|
||||
addSkill(skill) {
|
||||
if (!this.canAddSkill(skill)) return
|
||||
getSkillCost(skill) {
|
||||
// Unified method to get skill cost from various possible properties
|
||||
return skill.cost || skill.learnCost || skill.leCost || 0
|
||||
},
|
||||
|
||||
this.selectedSkills.push({ ...skill })
|
||||
this.updateRemainingPoints()
|
||||
canAffordSkillInCategory(skill) {
|
||||
// Check if character has enough learning points in the skill's category
|
||||
// Handle both displayName format (e.g., "Sozial") and internal name format (e.g., "sozial")
|
||||
let category = this.learningCategories.find(cat =>
|
||||
cat.displayName === skill.category
|
||||
)
|
||||
|
||||
console.log('Added skill:', skill.name, 'Selected skills:', this.selectedSkills)
|
||||
// If not found by displayName, try by internal name
|
||||
if (!category) {
|
||||
category = this.learningCategories.find(cat =>
|
||||
cat.name === skill.category?.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
if (!category) {
|
||||
console.warn('No learning category found for skill:', skill.name, 'category:', skill.category)
|
||||
return false
|
||||
}
|
||||
|
||||
const skillCost = this.getSkillCost(skill)
|
||||
return category.remainingPoints >= skillCost
|
||||
},
|
||||
|
||||
removeSkill(skill) {
|
||||
@@ -627,22 +546,15 @@ export default {
|
||||
|
||||
this.isLoadingSkills = true
|
||||
try {
|
||||
// Create request similar to SkillLearnDialog
|
||||
// Use the new simplified endpoint for character creation
|
||||
// Make sure to use the character class abbreviation
|
||||
const requestData = {
|
||||
char_id: 0, // New character - send as number, not string
|
||||
name: '', // Will be set for each skill individually
|
||||
current_level: 0,
|
||||
target_level: 1,
|
||||
type: 'skill',
|
||||
action: 'learn',
|
||||
use_pp: 0,
|
||||
use_gold: 0,
|
||||
reward: 'default'
|
||||
characterClass: this.characterClass // Should already be the abbreviation like "Ma", "As", etc.
|
||||
}
|
||||
|
||||
console.log('Loading skills with request:', requestData)
|
||||
|
||||
const response = await API.post('/api/characters/available-skills-new', requestData)
|
||||
const response = await API.post('/api/characters/available-skills-creation', requestData)
|
||||
|
||||
if (response.data && response.data.skills_by_category) {
|
||||
this.availableSkillsByCategory = response.data.skills_by_category
|
||||
@@ -665,35 +577,24 @@ export default {
|
||||
// Fallback for testing
|
||||
this.availableSkillsByCategory = {
|
||||
'Körperliche Fertigkeiten': [
|
||||
{ name: 'Klettern', epCost: 100, goldCost: 50 },
|
||||
{ name: 'Schwimmen', epCost: 80, goldCost: 40 },
|
||||
{ name: 'Springen', epCost: 60, goldCost: 30 }
|
||||
{ name: 'Klettern', learnCost: 50 },
|
||||
{ name: 'Schwimmen', learnCost: 40 },
|
||||
{ name: 'Springen', learnCost: 30 }
|
||||
],
|
||||
'Geistige Fertigkeiten': [
|
||||
{ name: 'Erste Hilfe', epCost: 120, goldCost: 60 },
|
||||
{ name: 'Naturkunde', epCost: 150, goldCost: 75 },
|
||||
{ name: 'Menschenkenntnis', epCost: 130, goldCost: 65 }
|
||||
{ name: 'Erste Hilfe', learnCost: 60 },
|
||||
{ name: 'Naturkunde', learnCost: 75 },
|
||||
{ name: 'Menschenkenntnis', learnCost: 65 }
|
||||
],
|
||||
'Handwerkliche Fertigkeiten': [
|
||||
{ name: 'Bogenbau', epCost: 200, goldCost: 100 },
|
||||
{ name: 'Schmieden', epCost: 250, goldCost: 125 }
|
||||
{ name: 'Bogenbau', learnCost: 100 },
|
||||
{ name: 'Schmieden', learnCost: 125 }
|
||||
]
|
||||
}
|
||||
|
||||
console.log('Generated sample skills:', this.availableSkillsByCategory)
|
||||
},
|
||||
|
||||
setSkillCategoryFilter(categoryName) {
|
||||
this.selectedSkillCategoryFilter = categoryName
|
||||
console.log('Skill category filter set to:', categoryName)
|
||||
},
|
||||
|
||||
canAffordSkill(skill) {
|
||||
// For character creation, we don't have actual EP/Gold yet
|
||||
// This is more of a placeholder for the UI
|
||||
return true
|
||||
},
|
||||
|
||||
isSkillSelected(skill) {
|
||||
return this.selectedSkills.some(s => s.name === skill.name)
|
||||
},
|
||||
@@ -703,16 +604,38 @@ export default {
|
||||
return // Already selected
|
||||
}
|
||||
|
||||
// Add skill to selected list
|
||||
this.selectedSkills.push({ ...skill })
|
||||
console.log('Skill selected for learning:', skill.name)
|
||||
// Check if the skill can be afforded
|
||||
if (!this.canAffordSkillInCategory(skill)) {
|
||||
console.log('Cannot afford skill:', skill.name)
|
||||
return
|
||||
}
|
||||
|
||||
// Add skill to selected list with proper cost
|
||||
const skillToAdd = {
|
||||
...skill,
|
||||
cost: this.getSkillCost(skill), // Ensure cost is properly set
|
||||
categoryDisplay: skill.category // Set category for display
|
||||
}
|
||||
|
||||
this.selectedSkills.push(skillToAdd)
|
||||
|
||||
// Update remaining points for all categories
|
||||
this.updateRemainingPoints()
|
||||
|
||||
console.log('Skill selected for learning:', skill.name, 'Cost:', skillToAdd.cost)
|
||||
console.log('Updated remaining points')
|
||||
},
|
||||
|
||||
removeSkillFromSelection(skill) {
|
||||
const index = this.selectedSkills.findIndex(s => s.name === skill.name)
|
||||
if (index !== -1) {
|
||||
this.selectedSkills.splice(index, 1)
|
||||
|
||||
// Update remaining points for all categories after removal
|
||||
this.updateRemainingPoints()
|
||||
|
||||
console.log('Skill removed from selection:', skill.name)
|
||||
console.log('Updated remaining points after removal')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -722,6 +645,38 @@ export default {
|
||||
<style scoped>
|
||||
/* Minimal custom styles - most styling comes from main.css */
|
||||
|
||||
/* Override global fullwidth-page padding to achieve true full-width */
|
||||
.fullwidth-page {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
|
||||
/* Add minimal padding only where needed */
|
||||
.page-header {
|
||||
padding: 15px 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Full-width three column grid layout */
|
||||
.three-column-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 30px;
|
||||
margin: 0 20px 30px 20px; /* Add horizontal margins for content readability */
|
||||
width: calc(100vw - 40px); /* Use viewport width minus margins */
|
||||
max-width: calc(100vw - 40px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Ensure grid takes full available width */
|
||||
.skills-content {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Utility classes for dynamic styling that can't be expressed in main.css */
|
||||
.opacity-50 {
|
||||
opacity: 0.6;
|
||||
@@ -742,4 +697,19 @@ export default {
|
||||
border-color: #dc3545 !important;
|
||||
background-color: #ffebee;
|
||||
}
|
||||
|
||||
/* Responsive behavior for smaller screens */
|
||||
@media (max-width: 1200px) {
|
||||
.three-column-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.three-column-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user