Learning lists are filled with right values

This commit is contained in:
2025-08-21 20:57:55 +02:00
parent c82ec0aa23
commit 32f63a283d
8 changed files with 1248 additions and 371 deletions
+237 -3
View File
@@ -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")
+293
View File
@@ -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")
}
})
}
}
+1
View File
@@ -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
+320
View File
@@ -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))
})
}
}
+59
View File
@@ -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)
}
}
+1 -1
View File
@@ -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>