Files
bamort/backend/bmrt/character/handlers.go
T
Bardioc26 042a1d4773 Learncost frontend (#42)
* introduced central package  registry by package init function
* dynamic registration of routes, model, migrations and initializers.
* setting a docker compose project name to prevent shutdown of other containers with the same (composer)name
* ai documentation
* app template
* Create tests for ALL API entpoints in ALL packages Based on current data. Ensure that all API endpoints used in frontend are tested. These tests are crucial for the next refactoring tasks.
* adopting agent instructions for a more consistent coding style
* added desired module layout and debugging information
* Fix All Failing tests All failing tests are fixed now that makes the refactoring more easy since all tests must pass
* restored routes for maintenance
* added common translations
* added new tests for API Endpoint
* Merge branch 'separate_business_logic'
* added lern and skill improvement cost editing
* Set Docker image tag when building to prevent rebuild when nothing has changed
* add and remove PP for Weaponskill fixed
* add and remove PP for same named skills fixed
* add new task
2026-05-01 18:15:31 +02:00

3581 lines
113 KiB
Go

package character
import (
"bamort/database"
"bamort/bmrt/gsmaster"
"bamort/logger"
"bamort/bmrt/models"
"errors"
"sort"
"strconv"
"strings"
"time"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// Character Handlers
type LearnRequestStruct struct {
SkillType string `json:"skillType"`
Name string `json:"name"`
Stufe int `json:"stufe"`
}
func respondWithError(c *gin.Context, status int, message string) {
logger.Warn("HTTP Fehler %d: %s", status, message)
c.JSON(status, gin.H{"error": message})
}
// checkCharacterOwnership verifies that the logged-in user owns the character
func checkCharacterOwnership(c *gin.Context, character *models.Char) bool {
userID := c.GetUint("userID")
if character.UserID != userID {
logger.Warn("Unauthorized access attempt: user %d tried to modify character %d owned by user %d", userID, character.ID, character.UserID)
respondWithError(c, http.StatusForbidden, "You are not authorized to modify this character")
return false
}
return true
}
func ListCharacters(c *gin.Context) {
logger.Debug("ListCharacters aufgerufen")
type AllCharacters struct {
SelfOwned []models.CharList `json:"self_owned"`
Others []models.CharList `json:"others"`
}
allCharacters := AllCharacters{}
logger.Debug("Lade Charaktere aus der Datenbank...")
//if err := database.DB.Find(&characters).Error; err != nil {
listOfChars, err := models.FindCharListByUserID(c.GetUint("userID"))
if err != nil {
logger.Error("Fehler beim Laden der Charaktere: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve characters")
return
}
logger.Debug("Gefundene Charaktere: %d", len(listOfChars))
allCharacters.SelfOwned = listOfChars
listPublic, err := models.FindPublicCharList()
if err != nil {
logger.Error("Fehler beim Laden der öffentlichen Charaktere: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve public characters")
return
}
listShared, err := models.FindSharedCharList(c.GetUint("userID"))
if err != nil {
logger.Error("Fehler beim Laden der geteilten Charaktere: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve shared characters")
return
}
listPublic = append(listPublic, listShared...)
allCharacters.Others = listPublic
logger.Info("Charakterliste erfolgreich geladen: %d Charaktere", len(listOfChars))
c.JSON(http.StatusOK, allCharacters)
}
func CreateCharacter(c *gin.Context) {
var character models.Char
if err := c.ShouldBindJSON(&character); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
if err := database.DB.Create(&character).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create character")
return
}
c.JSON(http.StatusCreated, character)
}
func GetCharacter(c *gin.Context) {
id := c.Param("id")
var character models.Char
err := character.FirstID(id)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve character")
return
}
feChar := ToFeChar(&character)
c.JSON(http.StatusOK, feChar)
}
func UpdateCharacter(c *gin.Context) {
id := c.Param("id")
var character models.Char
// First, find the existing character
err := character.FirstID(id)
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Check ownership
if !checkCharacterOwnership(c, &character) {
return
}
// Store the original ID to preserve it
originalID := character.ID
originalGameSystem := character.GameSystem
originalGameSystemId := character.GameSystemId
// Bind the updated data
if err := c.ShouldBindJSON(&character); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// Restore the ID
character.ID = originalID
character.GameSystem = originalGameSystem
character.GameSystemId = originalGameSystemId
// Update all associations
if err := database.DB.Session(&gorm.Session{FullSaveAssociations: true}).Save(&character).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update character")
return
}
c.JSON(http.StatusOK, character)
}
func DeleteCharacter(c *gin.Context) {
id := c.Param("id")
var character models.Char
err := character.FirstID(id)
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Check ownership
if !checkCharacterOwnership(c, &character) {
return
}
err = character.Delete()
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to delete character")
return
}
c.JSON(http.StatusOK, gin.H{"message": "Character deleted successfully"})
}
// Add Fertigkeit by putting it directly to the DB
func AddFertigkeit(charID uint, fertigkeit *models.SkFertigkeit) error {
// Set the foreign key for the new Eigenschaft
fertigkeit.CharacterID = charID
// Save the new Eigenschaft to the database
if err := database.DB.Create(&fertigkeit).Error; err != nil {
return fmt.Errorf("failed to add Eigenschaft: %w", err)
}
return nil
}
// Append the new Fertigkeit to the slice of the characters property
//character.Fertigkeiten = append(character.Fertigkeiten, fertigkeit)
func ToFeChar(object *models.Char) *models.FeChar {
feC := &models.FeChar{
Char: *object,
}
for idx, fertigkeit := range object.Fertigkeiten {
fertigkeit.Bonus = GetSkillBonus(&object.Eigenschaften, &fertigkeit)
if fertigkeit.Bonus > 0 {
object.Fertigkeiten[idx].Bonus = fertigkeit.Bonus
}
}
skills, innateSkills, categories := splitSkills(object.Fertigkeiten)
feC.Fertigkeiten = skills
feC.InnateSkills = innateSkills
feC.CategorizedSkills = categories
feC.Git = object.GetGiftToleranz()
return feC
}
func GetSkillBonus(eigenschaften *[]models.Eigenschaft, skill *models.SkFertigkeit) int {
bonus := 0
gsmsk := skill.GetSkillByName()
if gsmsk.Bonuseigenschaft != "check" {
for _, eigenschaft := range *eigenschaften {
if eigenschaft.Name == gsmsk.Bonuseigenschaft {
if eigenschaft.Value < 6 {
bonus = -2
break
} else if eigenschaft.Value < 21 {
bonus = -1
break
} else if eigenschaft.Value > 81 && eigenschaft.Value < 96 {
bonus = 1
break
} else if eigenschaft.Value >= 96 {
bonus = 2
break
}
}
}
}
skill.Bonus = bonus
return bonus
}
func splitSkills(object []models.SkFertigkeit) ([]models.SkFertigkeit, []models.SkFertigkeit, map[string][]models.SkFertigkeit) {
var normSkills []models.SkFertigkeit
var innateSkills []models.SkFertigkeit
//var categories map[string][]models.Fertigkeit
categories := make(map[string][]models.SkFertigkeit)
for _, skill := range object {
gsmsk := skill.GetSkillByName()
if gsmsk != nil && gsmsk.Improvable {
// Use GetCategory() which fetches from learning_skill_category_difficulties table
// with lowest ID when multiple categories exist
category := skill.GetCategory()
normSkills = append(normSkills, skill)
if _, exists := categories[category]; !exists {
categories[category] = make([]models.SkFertigkeit, 0)
}
categories[category] = append(categories[category], skill)
} else if gsmsk == nil {
// Skill not found in gsmaster - could be custom skill
// Treat as improvable and use GetCategory which will return Unkategorisiert
category := skill.GetCategory()
normSkills = append(normSkills, skill)
if _, exists := categories[category]; !exists {
categories[category] = make([]models.SkFertigkeit, 0)
}
categories[category] = append(categories[category], skill)
} else {
innateSkills = append(innateSkills, skill)
}
}
return normSkills, innateSkills, categories
}
// ExperienceAndWealthResponse repräsentiert die Antwort für EP und Vermögen
type ExperienceAndWealthResponse struct {
ExperiencePoints int `json:"experience_points"`
Wealth struct {
Goldstuecke int `json:"gold_coins"` // GS
Silberstuecke int `json:"silver_coins"` // SS
Kupferstuecke int `json:"copper_coins"` // KS
TotalInGS int `json:"total_in_ss"` // Gesamt in Silberstücken
} `json:"wealth"`
}
// GetCharacterExperienceAndWealth gibt nur die EP und Vermögensdaten eines Charakters zurück
func GetCharacterExperienceAndWealth(c *gin.Context) {
id := c.Param("id")
var character models.Char
// Lade nur die benötigten Felder
err := database.DB.
Preload("Erfahrungsschatz").
Preload("Vermoegen").
First(&character, id).Error
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Berechne Gesamtvermögen in Silbergroschen
// Annahme: 1 GS = 10 SS, 1 SS = 10 KS (typische PRG Währung)
gs := character.Vermoegen.Goldstuecke
ss := character.Vermoegen.Silberstuecke
ks := character.Vermoegen.Kupferstuecke
totalInSS := (gs * 10) + ss + (ks / 10)
response := ExperienceAndWealthResponse{
ExperiencePoints: character.Erfahrungsschatz.EP,
}
response.Wealth.Goldstuecke = gs
response.Wealth.Silberstuecke = ss
response.Wealth.Kupferstuecke = ks
response.Wealth.TotalInGS = totalInSS
c.JSON(http.StatusOK, response)
}
// UpdateExperienceRequest repräsentiert die Anfrage für EP-Update
type UpdateExperienceRequest struct {
ExperiencePoints int `json:"experience_points" binding:"required,min=0"`
Reason string `json:"reason,omitempty"` // Grund der Änderung
Notes string `json:"notes,omitempty"` // Zusätzliche Notizen
}
// UpdateCharacterExperience aktualisiert die Erfahrungspunkte eines Charakters
// TODO Wenn EP verändert werden ändert sich auch ES
func UpdateCharacterExperience(c *gin.Context) {
id := c.Param("id")
var character models.Char
// Lade den Charakter
err := database.DB.
Preload("Erfahrungsschatz").
First(&character, id).Error
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Check ownership
if !checkCharacterOwnership(c, &character) {
return
}
// Parse Request
var req UpdateExperienceRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// Standard-Grund setzen, falls nicht angegeben
if req.Reason == "" {
req.Reason = string(ReasonManual)
}
// Alten Wert für Audit-Log speichern
oldValue := 0
if character.Erfahrungsschatz.ID != 0 {
oldValue = character.Erfahrungsschatz.EP
}
// Aktualisiere oder erstelle Erfahrungsschatz
if character.Erfahrungsschatz.ID == 0 {
// Erstelle neuen Erfahrungsschatz
character.Erfahrungsschatz = models.Erfahrungsschatz{
BamortCharTrait: models.BamortCharTrait{
CharacterID: character.ID,
},
EP: req.ExperiencePoints,
}
if err := database.DB.Create(&character.Erfahrungsschatz).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create experience record")
return
}
} else {
// Aktualisiere existierenden Erfahrungsschatz
character.Erfahrungsschatz.EP = req.ExperiencePoints
if err := database.DB.Save(&character.Erfahrungsschatz).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update experience")
return
}
}
// Audit-Log-Eintrag erstellen (nur wenn sich der Wert geändert hat)
if oldValue != req.ExperiencePoints {
// TODO: User-ID aus dem Authentifizierungs-Context holen
userID := uint(0) // Placeholder
err = CreateAuditLogEntry(
character.ID,
"experience_points",
oldValue,
req.ExperiencePoints,
AuditLogReason(req.Reason),
userID,
req.Notes,
)
if err != nil {
// Log-Fehler sollten die Hauptoperation nicht blockieren
// TODO: Proper logging implementieren
}
}
c.JSON(http.StatusOK, gin.H{
"message": "Experience updated successfully",
"experience_points": req.ExperiencePoints,
"audit_logged": oldValue != req.ExperiencePoints,
})
}
// UpdateWealthRequest repräsentiert die Anfrage für Vermögens-Update
type UpdateWealthRequest struct {
Goldstücke *int `json:"goldstücke,omitempty"`
Silberstücke *int `json:"silberstücke,omitempty"`
Kupferstücke *int `json:"kupferstücke,omitempty"`
Reason string `json:"reason,omitempty"` // Grund der Änderung
Notes string `json:"notes,omitempty"` // Zusätzliche Notizen
}
// UpdateCharacterWealth aktualisiert das Vermögen eines Charakters
func UpdateCharacterWealth(c *gin.Context) {
id := c.Param("id")
var character models.Char
// Lade den Charakter
err := database.DB.
Preload("Vermoegen").
First(&character, id).Error
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Check ownership
if !checkCharacterOwnership(c, &character) {
return
}
// Parse Request
var req UpdateWealthRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// Standard-Grund setzen, falls nicht angegeben
if req.Reason == "" {
req.Reason = string(ReasonManual)
}
// Alte Werte für Audit-Log speichern
oldGold := 0
oldSilver := 0
oldCopper := 0
if character.Vermoegen.ID != 0 {
oldGold = character.Vermoegen.Goldstuecke
oldSilver = character.Vermoegen.Silberstuecke
oldCopper = character.Vermoegen.Kupferstuecke
}
// Aktualisiere oder erstelle Vermögen
if character.Vermoegen.ID == 0 {
// Erstelle neues Vermögen
userID := c.GetUint("userID")
character.Vermoegen = models.Vermoegen{
BamortCharTrait: models.BamortCharTrait{
CharacterID: character.ID,
UserID: userID,
},
Goldstuecke: getValueOrDefault(req.Goldstücke, 0),
Silberstuecke: getValueOrDefault(req.Silberstücke, 0),
Kupferstuecke: getValueOrDefault(req.Kupferstücke, 0),
}
if err := database.DB.Create(&character.Vermoegen).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create wealth record")
return
}
} else {
// Aktualisiere existierendes Vermögen
if req.Goldstücke != nil {
character.Vermoegen.Goldstuecke = *req.Goldstücke
}
if req.Silberstücke != nil {
character.Vermoegen.Silberstuecke = *req.Silberstücke
}
if req.Kupferstücke != nil {
character.Vermoegen.Kupferstuecke = *req.Kupferstücke
}
if err := database.DB.Save(&character.Vermoegen).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update wealth")
return
}
}
// Audit-Log-Einträge erstellen (nur für geänderte Werte)
// TODO: User-ID aus dem Authentifizierungs-Context holen
userID := uint(0) // Placeholder
if req.Goldstücke != nil && oldGold != character.Vermoegen.Goldstuecke {
CreateAuditLogEntry(
character.ID,
"gold",
oldGold,
character.Vermoegen.Goldstuecke,
AuditLogReason(req.Reason),
userID,
req.Notes,
)
}
if req.Silberstücke != nil && oldSilver != character.Vermoegen.Silberstuecke {
CreateAuditLogEntry(
character.ID,
"silver",
oldSilver,
character.Vermoegen.Silberstuecke,
AuditLogReason(req.Reason),
userID,
req.Notes,
)
}
if req.Kupferstücke != nil && oldCopper != character.Vermoegen.Kupferstuecke {
CreateAuditLogEntry(
character.ID,
"copper",
oldCopper,
character.Vermoegen.Kupferstuecke,
AuditLogReason(req.Reason),
userID,
req.Notes,
)
}
c.JSON(http.StatusOK, gin.H{
"message": "Wealth updated successfully",
"wealth": gin.H{
"goldstücke": character.Vermoegen.Goldstuecke,
"silberstücke": character.Vermoegen.Silberstuecke,
"kupferstücke": character.Vermoegen.Kupferstuecke,
},
})
}
// getValueOrDefault gibt den Wert zurück oder einen Default-Wert falls nil
func getValueOrDefault(value *int, defaultValue int) int {
if value != nil {
return *value
}
return defaultValue
}
// updateOrCreateSkill aktualisiert eine vorhandene Fertigkeit oder erstellt eine neue
func updateOrCreateSkill(character *models.Char, skillName string, newLevel int) error {
// Suche erst in normalen Fertigkeiten
for i := range character.Fertigkeiten {
if character.Fertigkeiten[i].Name == skillName {
character.Fertigkeiten[i].Fertigkeitswert = newLevel
return database.DB.Save(&character.Fertigkeiten[i]).Error
}
}
// Suche in Waffenfertigkeiten
for i := range character.Waffenfertigkeiten {
if character.Waffenfertigkeiten[i].Name == skillName {
character.Waffenfertigkeiten[i].Fertigkeitswert = newLevel
return database.DB.Save(&character.Waffenfertigkeiten[i]).Error
}
}
// Fertigkeit nicht gefunden - erstelle neue normale Fertigkeit
newSkill := models.SkFertigkeit{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: skillName,
},
CharacterID: character.ID,
},
Fertigkeitswert: newLevel,
Improvable: true,
}
if err := database.DB.Create(&newSkill).Error; err != nil {
return err
}
// Füge zur Charakter-Liste hinzu
character.Fertigkeiten = append(character.Fertigkeiten, newSkill)
return nil
}
// addSpellToCharacter fügt einen neuen Zauber zum Charakter hinzu
func addSpellToCharacter(character *models.Char, spellName string) error {
// Prüfe, ob Zauber bereits existiert
for _, spell := range character.Zauber {
if spell.Name == spellName {
// Zauber bereits vorhanden, nichts zu tun
return nil
}
}
// Erstelle neuen Zauber
newSpell := models.SkZauber{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: spellName,
},
CharacterID: character.ID,
},
}
if err := database.DB.Create(&newSpell).Error; err != nil {
return err
}
// Füge zur Charakter-Liste hinzu
character.Zauber = append(character.Zauber, newSpell)
return nil
}
// Learn and Improve handlers with automatic audit logging
// LearnSpellRequest definiert die Struktur für das Lernen eines Zaubers
type LearnSpellRequest struct {
Name string `json:"name" binding:"required"`
Notes string `json:"notes,omitempty"`
}
// getCharacterClass is deprecated. Use character.Klasse directly or appropriate database lookups.
// This function provides backwards compatibility for character class access.
// getCharacterClass gibt die Charakterklassen-Abkürzung zurück
func getCharacterClass(character *models.Char) string {
if len(character.Typ) > 3 {
return gsmaster.GetClassAbbreviationNewSystem(character.Typ)
}
return character.Typ
}
// LearnSkill lernt eine neue Fertigkeit und erstellt Audit-Log-Einträge
func LearnSkill(c *gin.Context) {
charID := c.Param("id")
var character models.Char
if err := character.FirstID(charID); err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// Check ownership
if !checkCharacterOwnership(c, &character) {
return
}
// Verwende gsmaster.LernCostRequest direkt
var request gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&request); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
// Setze Charakter-ID und Action für learning
request.CharId = character.ID
request.Action = "learn"
if request.Type == "" {
request.Type = "skill" // Default zu skill für Learning
}
// 1. Charakter laden
char, err := loadCharacterForImprovement(request.CharId)
if err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// 2. Skill validieren (für Learning beginnen wir bei Level 0)
characterClass, skillInfo, currentLevel, err := validateSkillForLearning(char, &request)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// Bestimme das finale Level
finalLevel := request.TargetLevel
if finalLevel <= 0 {
finalLevel = 1 // Standard für neue Fertigkeit
}
// 3. Kosten berechnen (von Level 0 bis finalLevel)
response, totalEP, totalGold, totalPP, err := calculateLearningCosts(char, &request, characterClass, skillInfo, currentLevel, finalLevel)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// 4. Ressourcen validieren
err = validateResources(char, request.Name, totalEP, totalGold, totalPP)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// 5. Ressourcen abziehen
newEP, newGold, err := deductResourcesForLearning(char, request.Name, finalLevel, totalEP, totalGold, totalPP)
if err != nil {
respondWithError(c, http.StatusInternalServerError, err.Error())
return
}
// 6. Skill hinzufügen/erstellen
if err := updateOrCreateSkill(char, request.Name, finalLevel); err != nil {
respondWithError(c, http.StatusInternalServerError, "Fehler beim Hinzufügen der Fertigkeit: "+err.Error())
return
}
// 7. Charakter speichern
if err := database.DB.Save(char).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Charakters")
return
}
// 8. Response erstellen
responseData := gin.H{
"message": "Fertigkeit erfolgreich gelernt",
"skill_name": request.Name,
"final_level": finalLevel,
"ep_cost": totalEP,
"gold_cost": totalGold,
"remaining_ep": newEP,
"remaining_gold": newGold,
"cost_details": response,
}
// Füge Multi-Level-spezifische Informationen hinzu
if finalLevel > 1 {
// Erstelle Array der gelernten Level für Kompatibilität
var levelsLearned []int
for i := 1; i <= finalLevel; i++ {
levelsLearned = append(levelsLearned, i)
}
responseData["levels_learned"] = levelsLearned
responseData["level_count"] = finalLevel
responseData["multi_level"] = true
}
c.JSON(http.StatusOK, responseData)
}
// ImproveSkill verbessert eine bestehende Fertigkeit und erstellt Audit-Log-Einträge
// validateSkillForLearning validiert Skill-Namen für neue Fertigkeiten (Learning)
func validateSkillForLearning(char *models.Char, request *gsmaster.LernCostRequest) (string, *models.SkillLearningInfo, int, error) {
// Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist
var characterClass string
if len(char.Typ) > 3 {
characterClass = gsmaster.GetClassAbbreviationNewSystem(char.Typ)
} else {
characterClass = char.Typ
}
// Normalize skill/spell name (trim whitespace, proper case)
skillName := strings.TrimSpace(request.Name)
skillInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, characterClass)
if err != nil {
return "", nil, 0, fmt.Errorf("fertigkeit '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", skillName, characterClass, err)
}
// Für Learning starten wir bei Level 0
currentLevel := 0
// Prüfe, ob die Fertigkeit bereits existiert
existingLevel := getCurrentSkillLevel(char, request.Name, "skill")
if existingLevel > 0 {
return "", nil, 0, fmt.Errorf("fertigkeit '%s' ist bereits auf Level %d - verwende ImproveSkill stattdessen", request.Name, existingLevel)
}
return characterClass, skillInfo, currentLevel, nil
}
// calculateLearningCosts berechnet die Kosten für das Erlernen einer neuen Fertigkeit
func calculateLearningCosts(char *models.Char, request *gsmaster.LernCostRequest, characterClass string, skillInfo *models.SkillLearningInfo, currentLevel, finalLevel int) ([]gsmaster.SkillCostResultNew, int, int, int, error) {
var response []gsmaster.SkillCostResultNew
var totalEP, totalGold, totalPP int
// Loop für jeden Level von 0 bis finalLevel (für neue Fertigkeiten)
for tempLevel := currentLevel; tempLevel < finalLevel; tempLevel++ {
nextLevel := tempLevel + 1
// Erstelle temporären Request für diesen Level
tempRequest := *request
tempRequest.CurrentLevel = tempLevel
tempRequest.TargetLevel = nextLevel
// Für das erste Level (0->1) ist es ein "learn", für weitere Level "improve"
if tempLevel == 0 {
tempRequest.Action = "learn"
} else {
tempRequest.Action = "improve"
}
// Erstelle cost result structure
costResult := gsmaster.SkillCostResultNew{
CharacterID: fmt.Sprintf("%d", char.ID),
CharacterClass: characterClass,
SkillName: request.Name,
TargetLevel: nextLevel,
}
// Verwende die gleiche Kostenfunktion wie für Improvements
err := CalculateSkillImproveCostNewSystem(&tempRequest, &costResult, nextLevel, &tempRequest.UsePP, &tempRequest.UseGold, skillInfo)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("fehler bei der Kostenberechnung: %v", err)
}
// für die nächste Runde die PP und Gold reduzieren die zum Lernen genutzt werden sollen
if costResult.PPUsed > 0 {
request.UsePP -= costResult.PPUsed
if request.UsePP < 0 {
request.UsePP = 0
}
}
if costResult.GoldUsed > 0 {
request.UseGold -= costResult.GoldUsed
if request.UseGold < 0 {
request.UseGold = 0
}
}
response = append(response, costResult)
// Addiere die Kosten
totalEP += costResult.EP
totalGold += costResult.GoldCost
totalPP += costResult.PPUsed
}
return response, totalEP, totalGold, totalPP, nil
}
// deductResourcesForLearning zieht die Ressourcen für das Lernen ab und erstellt Audit-Log-Einträge
func deductResourcesForLearning(char *models.Char, skillName string, finalLevel, totalEP, totalGold, totalPP int) (int, int, error) {
return deductResourcesWithAuditReason(char, skillName, finalLevel, totalEP, totalGold, totalPP, ReasonSkillLearning)
}
// deductResourcesWithAuditReason zieht EP, Gold und PP ab und erstellt entsprechende Audit-Log-Einträge
func deductResourcesWithAuditReason(char *models.Char, itemName string, finalLevel, totalEP, totalGold, totalPP int, auditReason AuditLogReason) (int, int, error) {
currentEP := char.Erfahrungsschatz.EP
currentGold := char.Vermoegen.Goldstuecke
// EP abziehen und Audit-Log erstellen
newEP := currentEP - totalEP
if totalEP > 0 {
var notes string
if finalLevel > 1 {
notes = fmt.Sprintf("Fertigkeit '%s' bis Level %d gelernt", itemName, finalLevel)
} else if auditReason == ReasonSpellLearning {
notes = fmt.Sprintf("Zauber '%s' gelernt", itemName)
} else {
notes = fmt.Sprintf("Fertigkeit '%s' gelernt", itemName)
}
err := CreateAuditLogEntry(char.ID, "experience_points", currentEP, newEP, auditReason, 0, notes)
if err != nil {
return 0, 0, fmt.Errorf("fehler beim Erstellen des Audit-Log-Eintrags: %v", err)
}
char.Erfahrungsschatz.EP = newEP
if err := database.DB.Save(&char.Erfahrungsschatz).Error; err != nil {
return 0, 0, fmt.Errorf("fehler beim Speichern der Erfahrungspunkte: %v", err)
}
}
// Gold abziehen und Audit-Log erstellen
newGold := currentGold - totalGold
if totalGold > 0 {
var notes string
if auditReason == ReasonSpellLearning {
notes = fmt.Sprintf("Gold für Zauber '%s' ausgegeben", itemName)
} else {
notes = fmt.Sprintf("Gold für Fertigkeit '%s' ausgegeben", itemName)
}
err := CreateAuditLogEntry(char.ID, "gold", currentGold, newGold, auditReason, 0, notes)
if err != nil {
return 0, 0, fmt.Errorf("fehler beim Erstellen des Audit-Log-Eintrags: %v", err)
}
char.Vermoegen.Goldstuecke = newGold
if err := database.DB.Save(&char.Vermoegen).Error; err != nil {
return 0, 0, fmt.Errorf("fehler beim Speichern des Vermögens: %v", err)
}
}
// PP abziehen (falls vorhanden und erforderlich)
if totalPP > 0 {
// Suche die richtige Fertigkeit und ziehe PP ab
for i := range char.Fertigkeiten {
if char.Fertigkeiten[i].Name == itemName {
char.Fertigkeiten[i].Pp -= totalPP
if err := database.DB.Save(&char.Fertigkeiten[i]).Error; err != nil {
return 0, 0, fmt.Errorf("fehler beim Aktualisieren der Praxispunkte: %v", err)
}
break
}
}
// Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten
for i := range char.Waffenfertigkeiten {
if char.Waffenfertigkeiten[i].Name == itemName {
char.Waffenfertigkeiten[i].Pp -= totalPP
if err := database.DB.Save(&char.Waffenfertigkeiten[i]).Error; err != nil {
return 0, 0, fmt.Errorf("fehler beim Aktualisieren der Praxispunkte: %v", err)
}
break
}
}
}
return newEP, newGold, nil
}
// ImproveSkill verbessert eine bestehende Fertigkeit und erstellt Audit-Log-Einträge
// loadCharacterForImprovement lädt einen Charakter mit allen benötigten Beziehungen
func loadCharacterForImprovement(characterID uint) (*models.Char, error) {
var char models.Char
err := database.DB.
Preload("Fertigkeiten").
Preload("Waffenfertigkeiten").
Preload("Erfahrungsschatz").
Preload("Vermoegen").
Preload("Zauber").
First(&char, characterID).Error
return &char, err
}
// validateSkillForImprovement validiert Skill-Namen und ermittelt aktuelle Level
func validateSkillForImprovement(char *models.Char, request *gsmaster.LernCostRequest) (string, *models.SkillLearningInfo, int, error) {
// Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist
var characterClass string
if len(char.Typ) > 3 {
characterClass = gsmaster.GetClassAbbreviationNewSystem(char.Typ)
} else {
characterClass = char.Typ
}
// Normalize skill/spell name (trim whitespace, proper case)
skillName := strings.TrimSpace(request.Name)
skillInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skillName, characterClass)
if err != nil {
return "", nil, 0, fmt.Errorf("Fertigkeit '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", skillName, characterClass, err)
}
// Aktuellen Level ermitteln, falls nicht angegeben oder zu klein
currentLevel := request.CurrentLevel
if currentLevel <= 3 {
currentLevel = getCurrentSkillLevel(char, request.Name, "skill")
if currentLevel == -1 {
return "", nil, 0, fmt.Errorf("Fertigkeit nicht bei diesem Charakter vorhanden")
}
request.CurrentLevel = currentLevel
}
return characterClass, skillInfo, currentLevel, nil
}
// calculateImprovementCosts berechnet die Gesamtkosten für Multi-Level-Verbesserungen
func calculateImprovementCosts(char *models.Char, request *gsmaster.LernCostRequest, characterClass string, skillInfo *models.SkillLearningInfo, currentLevel, finalLevel int) ([]gsmaster.SkillCostResultNew, int, int, int, error) {
var response []gsmaster.SkillCostResultNew
var totalEP, totalGold, totalPP int
// Loop für jeden Level von currentLevel bis finalLevel
tempLevel := currentLevel
for tempLevel < finalLevel {
nextLevel := tempLevel + 1
// Erstelle temporären Request für diesen Level
tempRequest := *request
tempRequest.CurrentLevel = tempLevel
tempRequest.TargetLevel = nextLevel
// Berechne Kosten für diesen einen Level
var costResult gsmaster.SkillCostResultNew
costResult.CharacterID = fmt.Sprintf("%d", char.ID)
costResult.CharacterClass = characterClass
costResult.SkillName = request.Name
err := CalculateSkillImproveCostNewSystem(&tempRequest, &costResult, nextLevel, &tempRequest.UsePP, &tempRequest.UseGold, skillInfo)
if err != nil {
return nil, 0, 0, 0, fmt.Errorf("Fehler bei der Kostenberechnung: %v", err)
}
// für die nächste Runde die PP und Gold reduzieren die zum Lernen genutzt werden sollen
if costResult.PPUsed > 0 {
request.UsePP -= costResult.PPUsed
if request.UsePP < 0 {
request.UsePP = 0
}
}
if costResult.GoldUsed > 0 {
request.UseGold -= costResult.GoldUsed
if request.UseGold < 0 {
request.UseGold = 0
}
}
response = append(response, costResult)
// Addiere die Kosten
totalEP += costResult.EP
totalGold += costResult.GoldCost
totalPP += costResult.PPUsed
tempLevel++
}
return response, totalEP, totalGold, totalPP, nil
}
// validateResources prüft, ob genügend Ressourcen vorhanden sind
func validateResources(char *models.Char, skillName string, totalEP, totalGold, totalPP int) error {
// Prüfe, ob genügend EP vorhanden sind
currentEP := char.Erfahrungsschatz.EP
if currentEP < totalEP {
return fmt.Errorf("Nicht genügend Erfahrungspunkte vorhanden")
}
// Prüfe, ob genügend Gold vorhanden ist
currentGold := char.Vermoegen.Goldstuecke
if currentGold < totalGold {
return fmt.Errorf("Nicht genügend Gold vorhanden")
}
// Prüfe, ob genügend PP vorhanden sind (PP der jeweiligen Fertigkeit)
currentPP := 0
for _, skill := range char.Fertigkeiten {
if skill.Name == skillName {
currentPP = skill.Pp
break
}
}
// Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten
if currentPP == 0 {
for _, skill := range char.Waffenfertigkeiten {
if skill.Name == skillName {
currentPP = skill.Pp
break
}
}
}
if totalPP > 0 && currentPP < totalPP {
return fmt.Errorf("Nicht genügend Praxispunkte vorhanden")
}
return nil
}
// deductResources zieht die Kosten von den Charakterressourcen ab
// TODO Fehlerbehandlung (Falls Tabelle nicht vorhanden ist)
func deductResources(char *models.Char, skillName string, currentLevel, finalLevel, totalEP, totalGold, totalPP int) (int, int, error) {
currentEP := char.Erfahrungsschatz.EP
currentGold := char.Vermoegen.Goldstuecke
// EP abziehen und Audit-Log erstellen
newEP := currentEP - totalEP
if totalEP > 0 {
// Erstelle Notiz für Multi-Level Improvement
levelCount := finalLevel - currentLevel
var notes string
if levelCount > 1 {
notes = fmt.Sprintf("Fertigkeit '%s' von %d auf %d verbessert (%d Level)", skillName, currentLevel, finalLevel, levelCount)
} else {
notes = fmt.Sprintf("Fertigkeit '%s' von %d auf %d verbessert", skillName, currentLevel, finalLevel)
}
err := CreateAuditLogEntry(char.ID, "experience_points", currentEP, newEP, ReasonSkillImprovement, 0, notes)
if err != nil {
return newEP, 0, fmt.Errorf("Fehler beim Erstellen des Audit-Log-Eintrags: %v", err)
}
char.Erfahrungsschatz.EP = newEP
if err := database.DB.Save(&char.Erfahrungsschatz).Error; err != nil {
return newEP, 0, fmt.Errorf("Fehler beim Speichern der Erfahrungspunkte: %v", err)
}
}
// Gold abziehen und Audit-Log erstellen
newGold := currentGold - totalGold
if totalGold > 0 {
notes := fmt.Sprintf("Gold für Verbesserung von '%s' ausgegeben", skillName)
err := CreateAuditLogEntry(char.ID, "gold", currentGold, newGold, ReasonSkillImprovement, 0, notes)
if err != nil {
return newEP, newGold, fmt.Errorf("Fehler beim Erstellen des Audit-Log-Eintrags: %v", err)
}
char.Vermoegen.Goldstuecke = newGold
if err := database.DB.Save(&char.Vermoegen).Error; err != nil {
return newEP, newGold, fmt.Errorf("Fehler beim Speichern des Vermögens: %v", err)
}
}
// PP abziehen wenn verwendet (PP der jeweiligen Fertigkeit)
if totalPP > 0 {
// Finde die richtige Fertigkeit und ziehe PP ab
for i := range char.Fertigkeiten {
if char.Fertigkeiten[i].Name == skillName {
char.Fertigkeiten[i].Pp -= totalPP
if err := database.DB.Save(&char.Fertigkeiten[i]).Error; err != nil {
return newEP, newGold, fmt.Errorf("Fehler beim Aktualisieren der Praxispunkte: %v", err)
}
break
}
}
// Falls nicht in normalen Fertigkeiten gefunden, prüfe Waffenfertigkeiten
for i := range char.Waffenfertigkeiten {
if char.Waffenfertigkeiten[i].Name == skillName {
char.Waffenfertigkeiten[i].Pp -= totalPP
if err := database.DB.Save(&char.Waffenfertigkeiten[i]).Error; err != nil {
return newEP, newGold, fmt.Errorf("Fehler beim Aktualisieren der Praxispunkte: %v", err)
}
break
}
}
}
return newEP, newGold, nil
}
func ImproveSkill(c *gin.Context) {
var request gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&request); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
// 1. Charakter laden
char, err := loadCharacterForImprovement(request.CharId)
if err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// Check ownership
if !checkCharacterOwnership(c, char) {
return
}
// 2. Skill validieren und Level ermitteln
characterClass, skillInfo, currentLevel, err := validateSkillForImprovement(char, &request)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// Bestimme das finale Level
finalLevel := request.TargetLevel
if finalLevel <= 0 {
finalLevel = currentLevel + 1
}
// 3. Kosten berechnen
response, totalEP, totalGold, totalPP, err := calculateImprovementCosts(char, &request, characterClass, skillInfo, currentLevel, finalLevel)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// 4. Ressourcen validieren
err = validateResources(char, request.Name, totalEP, totalGold, totalPP)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// 5. Ressourcen abziehen
newEP, newGold, err := deductResources(char, request.Name, currentLevel, finalLevel, totalEP, totalGold, totalPP)
if err != nil {
respondWithError(c, http.StatusInternalServerError, err.Error())
return
}
// 6. Skill-Level aktualisieren
if err := updateOrCreateSkill(char, request.Name, finalLevel); err != nil {
respondWithError(c, http.StatusInternalServerError, "Fehler beim Aktualisieren der Fertigkeit: "+err.Error())
return
}
// 7. Charakter speichern
if err := database.DB.Save(char).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Charakters")
return
}
// 8. Response erstellen
responseData := gin.H{
"message": "Fertigkeit erfolgreich verbessert",
"skill_name": request.Name,
"from_level": currentLevel,
"to_level": finalLevel,
"ep_cost": totalEP,
"gold_cost": totalGold,
"remaining_ep": newEP,
"remaining_gold": newGold,
"cost_details": response,
}
// Füge Multi-Level-spezifische Informationen hinzu
levelCount := finalLevel - currentLevel
if levelCount > 1 {
var levelsLearned []int
for i := currentLevel + 1; i <= finalLevel; i++ {
levelsLearned = append(levelsLearned, i)
}
responseData["levels_learned"] = levelsLearned
responseData["level_count"] = levelCount
responseData["multi_level"] = true
}
c.JSON(http.StatusOK, responseData)
}
// validateSpellForLearning validiert Zauber-Namen für neue Zauber (Learning)
func validateSpellForLearning(char *models.Char, request *gsmaster.LernCostRequest) (string, *models.SpellLearningInfo, int, error) {
// Verwende Klassenabkürzung wenn der Typ länger als 3 Zeichen ist
var characterClass string
if len(char.Typ) > 3 {
characterClass = gsmaster.GetClassAbbreviationNewSystem(char.Typ)
} else {
characterClass = char.Typ
}
// Normalize spell name (trim whitespace, proper case)
spellName := strings.TrimSpace(request.Name)
// Ensure spell data is sane for learning calculations (some legacy seeds have level 0 or missing categories)
var spell models.Spell
if err := database.DB.Where("name = ?", spellName).First(&spell).Error; err == nil {
updated := false
if spell.Stufe <= 0 {
spell.Stufe = 1
updated = true
}
if spell.LearningCategory == "" {
spell.LearningCategory = "Spruch"
updated = true
}
if spell.Category == "" {
spell.Category = "Erkennen"
updated = true
}
if updated {
_ = database.DB.Save(&spell).Error
}
}
spellInfo, err := models.GetSpellLearningInfoNewSystem(spellName, characterClass)
if err != nil {
return "", nil, 0, fmt.Errorf("zauber '%s' nicht gefunden oder nicht für Klasse '%s' verfügbar: %v", spellName, characterClass, err)
}
// Für Learning starten wir bei Level 0
currentLevel := 0
// Prüfe, ob der Zauber bereits existiert
for _, spell := range char.Zauber {
if spell.Name == request.Name {
return "", nil, 0, fmt.Errorf("zauber '%s' ist bereits gelernt - Zauber können nicht verbessert werden", request.Name)
}
}
return characterClass, spellInfo, currentLevel, nil
}
// calculateSpellLearningCosts berechnet die Kosten für das Erlernen eines neuen Zaubers
func calculateSpellLearningCosts(char *models.Char, request *gsmaster.LernCostRequest, characterClass string, spellInfo *models.SpellLearningInfo, currentLevel, finalLevel int) ([]gsmaster.SkillCostResultNew, int, error) {
var response []gsmaster.SkillCostResultNew
var totalEP int
// Erstelle cost result structure für Zauber
costResult := gsmaster.SkillCostResultNew{
CharacterID: fmt.Sprintf("%d", char.ID),
CharacterClass: characterClass,
SkillName: request.Name,
TargetLevel: finalLevel,
}
remainingPP := 0
remainingGold := 0
// Verwende die Spell-spezifische Kostenfunktion
err := calculateSpellLearnCostNewSystem(request, &costResult, &remainingPP, &remainingGold, spellInfo)
if err != nil {
return nil, 0, fmt.Errorf("fehler bei der Kostenberechnung: %v", err)
}
response = append(response, costResult)
totalEP = costResult.EP
// Zauber haben normalerweise keine Gold- oder PP-Kosten
return response, totalEP, nil
}
// LearnSpell lernt einen neuen Zauber und erstellt Audit-Log-Einträge
func LearnSpell(c *gin.Context) {
char_ID := c.Param("id")
/*
var character models.Char
if err := character.FirstID(charID); err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
*/
charIDInt, err := strconv.Atoi(char_ID)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Charakter-ID")
return
}
charID := uint(charIDInt)
// Load character to check ownership
var character models.Char
if err := character.FirstID(char_ID); err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// Check ownership
if !checkCharacterOwnership(c, &character) {
return
}
var lernRequest gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&lernRequest); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
// Setze die CharId aus der URL, falls sie nicht im Request enthalten ist
if lernRequest.CharId == 0 {
lernRequest.CharId = charID
}
// Setze Standard-Werte für Spell Learning falls nicht gesetzt
if lernRequest.Type == "" {
lernRequest.Type = "spell"
}
if lernRequest.Action == "" {
lernRequest.Action = "learn"
}
if lernRequest.CurrentLevel == 0 && lernRequest.TargetLevel == 0 {
lernRequest.CurrentLevel = 0 // Zauber sind nicht gelernt
lernRequest.TargetLevel = 1 // Zauber werden auf Level 1 gelernt
}
// 1. Charakter laden
char, err := loadCharacterForImprovement(lernRequest.CharId)
if err != nil {
respondWithError(c, http.StatusNotFound, "Charakter nicht gefunden")
return
}
// 2. Zauber validieren (für Learning beginnen wir bei Level 0)
characterClass, spellInfo, currentLevel, err := validateSpellForLearning(char, &lernRequest)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
finalLevel := 1 // Zauber werden immer auf Level 1 gelernt
// 3. Kosten berechnen (von Level 0 bis 1)
response, totalEP, err := calculateSpellLearningCosts(char, &lernRequest, characterClass, spellInfo, currentLevel, finalLevel)
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// 4. Ressourcen validieren (nur EP für Zauber)
err = validateResources(char, lernRequest.Name, totalEP, 0, 0) // Gold=0, PP=0 für Zauber
if err != nil {
respondWithError(c, http.StatusBadRequest, err.Error())
return
}
// 5. Ressourcen abziehen
newEP, _, err := deductResourcesWithAuditReason(char, lernRequest.Name, 1, totalEP, 0, 0, ReasonSpellLearning)
if err != nil {
respondWithError(c, http.StatusInternalServerError, err.Error())
return
}
// 6. Zauber hinzufügen
if err := addSpellToCharacter(char, lernRequest.Name); err != nil {
respondWithError(c, http.StatusInternalServerError, "Fehler beim Hinzufügen des Zaubers: "+err.Error())
return
}
// 7. Charakter speichern
if err := database.DB.Save(char).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Fehler beim Speichern des Charakters")
return
}
// 8. Response erstellen (kompatibel mit alter Version)
responseData := gin.H{
"message": "Zauber erfolgreich gelernt",
"spell_name": lernRequest.Name,
"ep_cost": totalEP,
"remaining_ep": newEP,
"cost_details": response,
}
c.JSON(http.StatusOK, responseData)
}
// GetRewardTypesStatic is deprecated. Use GetRewardTypes instead.
// This function provides hardcoded reward type mappings.
// GetRewardTypesStatic liefert verfügbare Belohnungsarten für ein bestimmtes Lernszenario
func GetRewardTypesStatic(c *gin.Context) {
characterID := c.Param("id")
learningType := c.Query("learning_type") // 'improve', 'learn', 'spell'
skillName := c.Query("skill_name")
skillType := c.Query("skill_type") // 'skill', 'weapon', 'spell'
// Basis-Belohnungsarten
rewardTypes := []gin.H{}
// Je nach Lerntyp verschiedene Belohnungsarten anbieten
switch learningType {
case "learn":
// Neue Fertigkeit lernen - noGold Belohnung verfügbar
rewardTypes = append(rewardTypes,
gin.H{"value": "default", "label": "Standard (EP + Gold)", "description": "Normale EP- und Goldkosten"},
gin.H{"value": "noGold", "label": "Ohne Gold (nur EP)", "description": "Keine Goldkosten, nur EP als Belohnung"},
)
case "spell":
// Zauber lernen - halveepnoGold verfügbar
rewardTypes = append(rewardTypes,
gin.H{"value": "default", "label": "Standard (EP)", "description": "Normale EP-Kosten"},
gin.H{"value": "halveepnoGold", "label": "Halbe EP ohne Gold", "description": "Halbe EP-Kosten, kein Gold als Belohnung"},
)
case "improve":
// Fertigkeit verbessern - halveepnoGold verfügbar
rewardTypes = append(rewardTypes,
gin.H{"value": "default", "label": "Standard (EP + Gold)", "description": "Normale EP- und Goldkosten"},
gin.H{"value": "halveepnoGold", "label": "Halbe EP ohne Gold", "description": "Halbe EP-Kosten, kein Gold als Belohnung"},
)
// Spezielle Optionen für bestimmte Fertigkeiten
if skillType == "weapon" {
// Waffenfertigkeiten könnten spezielle Trainingsmethoden haben
rewardTypes = append(rewardTypes,
gin.H{"value": "training", "label": "Training mit Meister", "description": "Intensives Training mit einem Waffenmeister"},
)
}
default:
}
c.JSON(http.StatusOK, gin.H{
"reward_types": rewardTypes,
"learning_type": learningType,
"skill_name": skillName,
"skill_type": skillType,
"character_id": characterID,
})
}
// GetAvailableSkillsNewSystem gibt alle verfügbaren Fertigkeiten mit Lernkosten zurück (POST mit LernCostRequest)
func GetAvailableSkillsNewSystem(c *gin.Context) {
// Parse LernCostRequest aus POST body
var baseRequest gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&baseRequest); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
// For character creation (char_id = 0), we don't need to load an existing character
var character models.Char
learnedSkills := make(map[string]bool)
if baseRequest.CharId != 0 {
// Load existing character and their learned skills
if err := database.DB.Preload("Fertigkeiten").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, baseRequest.CharId).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Create map of learned skills for existing character
for _, skill := range character.Fertigkeiten {
learnedSkills[skill.Name] = true
}
}
// For character creation (char_id = 0), learnedSkills remains empty
// Hole alle verfügbaren Fertigkeiten aus der gsmaster Datenbank
var allSkills []models.Skill
allSkills, err := models.SelectSkills("", "")
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve skills from gsmaster")
return
}
// Organisiere Fertigkeiten nach Kategorien
skillsByCategory := make(map[string][]gin.H)
for _, skill := range allSkills {
// Überspringe bereits gelernte Fertigkeiten
if learnedSkills[skill.Name] {
continue
}
// Erstelle LernCostRequest für diese Fertigkeit basierend auf der Basis-Anfrage
request := baseRequest
request.CharId = baseRequest.CharId // Use the char_id from the request (0 for character creation)
request.Name = skill.Name
request.CurrentLevel = 0 // Nicht gelernt
request.TargetLevel = 1 // Auf Level 1 lernen
request.Type = "skill"
request.Action = "learn"
// Erstelle SkillCostResultNew
characterClass := ""
characterID := "0"
if baseRequest.CharId != 0 {
// Use existing character data
characterID = fmt.Sprintf("%d", character.ID)
characterClass = getCharacterClass(&character)
}
// For character creation, we don't have a character class yet, use empty string
levelResult := gsmaster.SkillCostResultNew{
CharacterID: characterID,
CharacterClass: characterClass,
SkillName: skill.Name,
TargetLevel: 1,
}
remainingPP := request.UsePP
remainingGold := request.UseGold
// Hole die vollständigen Skill-Informationen für die Kostenberechnung
skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skill.Name, characterClass)
if err != nil {
// Fallback für unbekannte Skills
skillLearningInfo = &models.SkillLearningInfo{
SkillName: skill.Name,
CategoryName: skill.Category,
LearnCost: 50, // Standard-Lernkosten
}
}
// 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 for improvements
goldCost = 50000 // Fallback-Wert for improvements
if err == nil {
epCost = levelResult.EP
goldCost = levelResult.GoldCost
}
}
skillInfo := gin.H{
"name": skill.Name,
"epCost": epCost,
"goldCost": goldCost,
}
category := skill.Category
if category == "" {
category = "Sonstige"
}
skillsByCategory[category] = append(skillsByCategory[category], skillInfo)
}
c.JSON(http.StatusOK, gin.H{
"skills_by_category": skillsByCategory,
})
}
// getCharacterClassCode converts a character class name to its code using the database
func getCharacterClassCode(className string) (string, error) {
var characterClass models.CharacterClass
err := characterClass.FirstByNameOrCode(className)
if err != nil {
return "", fmt.Errorf("character class '%s' not found: %w", className, err)
}
return characterClass.Code, nil
}
func GetAvailableSpellsForCreation(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("GetAvailableSpellsForCreation - CharacterClass: %s", request.CharacterClass)
// Convert character class name to code
characterClassCode, err := getCharacterClassCode(request.CharacterClass)
if err != nil {
logger.Error("Fehler beim Konvertieren der Charakterklasse: %v", err)
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Unbekannte Charakterklasse: %s", request.CharacterClass),
})
return
}
// Get all available spells with their learning costs
spellsByCategory, err := GetAllSpellsWithLE(characterClassCode, 2)
if err != nil {
logger.Error("Fehler beim Abrufen der Zauber: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Fehler beim Abrufen der Zauber",
})
return
}
logger.Info("GetAvailableSpellsForCreation - Gefundene Kategorien: %d", len(spellsByCategory))
if len(spellsByCategory) == 0 {
logger.Warn("GetAvailableSpellsForCreation - Keine Zauber für Klasse %s gefunden", request.CharacterClass)
c.JSON(http.StatusNotFound, gin.H{
"spells_by_category": map[string][]gin.H{},
})
return
}
c.JSON(http.StatusOK, gin.H{
"spells_by_category": spellsByCategory,
})
}
// 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 GetAllSpellsWithLE(characterClass string, maxLevel int) (map[string][]gin.H, error) {
// Create mapping of character classes to allowed learning categories
allowedCategories := getCharacterClassSpellSchoolMapping()
allowedLearningCategories := getCharacterClassSpellLearningCategoriesMapping()
// Check if character class has allowed spell schools
allowedSchools, exists := allowedCategories[characterClass]
if !exists {
return map[string][]gin.H{}, nil // Return empty map if class can't learn spells
}
allowedSpellType, exists := allowedLearningCategories[characterClass]
if !exists {
return map[string][]gin.H{}, nil // Return empty map if class can't learn spells
}
// Extract allowed school names and spell types from maps
var allowedSchoolNames []string
for school, allowed := range allowedSchools {
if allowed {
allowedSchoolNames = append(allowedSchoolNames, school)
}
}
var allowedSpellTypeNames []string
for spellType, allowed := range allowedSpellType {
if allowed {
allowedSpellTypeNames = append(allowedSpellTypeNames, spellType)
}
}
// Get all spells from database with level filter
var spells []models.Spell
err := database.DB.Where("stufe <= ? AND category in (?) and learning_category in (?) AND category IS NOT NULL AND learning_category IS NOT NULL", maxLevel, allowedSchoolNames, allowedSpellTypeNames).Find(&spells).Error
if err != nil {
return nil, fmt.Errorf("failed to fetch spells: %w", err)
}
// Group spells by category (using LearningCategory field)
spellsByCategory := make(map[string][]gin.H)
for _, spell := range spells {
// Check if this character class can learn this spell school
//if !allowedSchools[spell.Category] || !allowedSpellType[spell.LearningCategory] {
// continue // Skip spells from schools this class can't learn
//}
// Calculate learning cost for this spell
leCost := getSpellLECost(spell.Stufe)
spellData := gin.H{
"id": spell.ID,
"name": spell.Name,
"level": spell.Stufe,
"school": spell.Category, // Display category
"learning_category": spell.LearningCategory, // Internal category for learning
"description": spell.Beschreibung,
"le_cost": leCost,
"ap": spell.AP,
"art": spell.Art,
"zauberdauer": spell.Zauberdauer,
"reichweite": spell.Reichweite,
"wirkungsziel": spell.Wirkungsziel,
"wirkungsbereich": spell.Wirkungsbereich,
"wirkungsdauer": spell.Wirkungsdauer,
}
// Use LearningCategory for grouping
category := spell.LearningCategory
spellsByCategory[category] = append(spellsByCategory[category], spellData)
}
// Sort spells within each category by level, then by name
for category := range spellsByCategory {
spells := spellsByCategory[category]
sort.Slice(spells, func(i, j int) bool {
levelI := spells[i]["level"].(int)
levelJ := spells[j]["level"].(int)
if levelI != levelJ {
return levelI < levelJ
}
nameI := spells[i]["name"].(string)
nameJ := spells[j]["name"].(string)
return nameI < nameJ
})
spellsByCategory[category] = spells
}
return spellsByCategory, nil
}
func getCharacterClassSpellLearningCategoriesMapping() map[string]map[string]bool {
return map[string]map[string]bool{
"Ma": { // Magier
"Spruch": true,
//"Salz": true,
//"Runenstab": true,
},
"Hx": { // Hexer
"Spruch": true,
"Salz": true,
//"Runenstab": true,
},
"Dr": { // Druide
"Spruch": true,
//"Salz": true,
//"Runenstab": true,
"Dweomer": true,
},
"Sc": { // Schamane
"Spruch": true,
//"Salz": true,
//"Runenstab": true,
"Dweomer": true,
},
"PB": { // Priester Beschützer
"Salz": true,
"Wundertat": true,
},
"PS": { // Priester Streiter
"Salz": true,
"Wundertat": true,
},
"Ba": { // Barde
"Lied": true,
},
"Or": { // Ordenskrieger
"Wundertat": true,
},
}
}
// getCharacterClassSpellSchoolMapping returns the mapping of character classes to allowed spell schools
func getCharacterClassSpellSchoolMapping() map[string]map[string]bool {
return map[string]map[string]bool{
"Ma": { // Magier
"Beherrschen": true,
"Bewegen": true,
"Dweomer": true,
"Erkennen": true,
"Erschaffen": true,
"Verändern": true,
"Zerstören": true,
},
"Hx": { // Hexer
"Beherrschen": true,
"Zerstören": true,
"Erkennen": true,
"Verändern": true,
"Erschaffen": true,
"Bewegen": true,
"Formen": true,
},
"Dr": { // Druide
"Bewegen": true,
"Erkennen": true,
"Erschaffen": true,
"Verändern": true,
},
"Sc": { // Schamane
"Beherrschen": true,
"Erkennen": true,
"Verändern": true,
},
"PB": { // Priester Beschützer
"Dweomer": true,
"Erkennen": true,
"Verändern": true,
},
"PS": { // Priester Streiter
"Dweomer": true,
"Erkennen": true,
"Verändern": true,
},
"Ba": { // Barde
"Beherrschen": true,
"Dweomer": true,
"Erkennen": true,
},
"Or": { // Ordenskrieger
"Dweomer": true,
"Erkennen": true,
},
}
}
// getSpellLECost returns the learning cost in LE for a given spell level from the database
func getSpellLECost(level int) int {
var spellLECost models.SpellLevelLECost
// Query the database for the LE cost for this level
err := database.DB.Where("level = ? AND game_system_id = ?", level, 1).First(&spellLECost).Error
if err != nil {
// If not found in database, fall back to standard RPG costs
spellLECosts := map[int]int{
1: 1,
2: 2,
3: 3,
4: 4,
5: 5,
6: 6,
7: 8,
8: 10,
9: 12,
10: 15,
11: 18,
12: 21,
}
if cost, exists := spellLECosts[level]; exists {
return cost
}
// Final fallback for unknown levels
return level
}
return spellLECost.LERequired
}
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 {
if category.Name == "Unbekannt" || scd.Skill.InnateSkill {
continue
}
// For character creation, use reduced costs based on category and difficulty
// Regular learning costs (LearnCost) are for existing characters
creationCost := getSkillCreationCost(category.Name, scd.SkillDifficulty.Name)
skillInfo := gin.H{
"name": scd.Skill.Name,
"leCost": creationCost,
"difficulty": scd.SkillDifficulty.Name,
}
skillsByCategory[category.Name] = append(skillsByCategory[category.Name], skillInfo)
}
}
// Add weapon skills to "Kampf" category
weaponSkills, err := GetWeaponSkillsWithLE()
if err == nil {
if _, exists := skillsByCategory["Waffen"]; !exists {
skillsByCategory["Waffen"] = []gin.H{}
}
skillsByCategory["Waffen"] = append(skillsByCategory["Waffen"], weaponSkills...)
}
return skillsByCategory, nil
}
// getSkillCreationCost returns the LE cost for learning a skill during character creation
// These costs are much lower than regular learning costs, as they represent initial training
// Costs vary by both category and difficulty level
func getSkillCreationCost(category string, difficulty string) int {
// Normalize difficulty string for comparison
difficultyLower := strings.ToLower(difficulty)
// Define cost mapping per category and difficulty
switch category {
case "Alltag":
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 1
case "schwer":
return 2
default:
return 1
}
case "Freiland":
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 2
case "schwer":
return 2
default:
return 2
}
case "Halbwelt":
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 2
case "schwer":
return 2
default:
return 2
}
case "Kampf":
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 2
case "schwer":
return 3
default:
return 2
}
case "Körper":
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 1
case "schwer":
return 2
default:
return 1
}
case "Sozial":
switch difficultyLower {
case "leicht":
return 2
case "normal":
return 2
case "schwer":
return 4
default:
return 2
}
case "Unterwelt":
switch difficultyLower {
case "leicht":
return 2
case "normal":
return 4
case "schwer":
return 6
default:
return 4
}
case "Waffen":
switch difficultyLower {
case "leicht":
return 2
case "normal":
return 4
case "schwer":
return 6
case "sehr schwer":
return 8
default:
return 4
}
case "Wissen":
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 2
case "schwer":
return 2
default:
return 2
}
default:
// Default fallback for unknown categories
switch difficultyLower {
case "leicht":
return 1
case "normal":
return 2
case "schwer":
return 3
case "sehr schwer":
return 4
default:
return 2
}
}
}
// GetWeaponSkillsWithLE returns all weapon skills with their learning costs
func GetWeaponSkillsWithLE() ([]gin.H, error) {
// Query weapon skills with their difficulty from the WeaponSkillCategoryDifficulty table
var weaponSkillDifficulties []models.WeaponSkillCategoryDifficulty
err := database.DB.Preload("WeaponSkill").
Preload("SkillDifficulty").
Preload("SkillCategory").
Find(&weaponSkillDifficulties).Error
if err != nil {
return nil, err
}
var result []gin.H
seenWeapons := make(map[string]bool) // Track weapons we've already added
for _, wscd := range weaponSkillDifficulties {
weaponName := wscd.WeaponSkill.Name
// Skip if we've already added this weapon (avoid duplicates)
if seenWeapons[weaponName] {
continue
}
seenWeapons[weaponName] = true
// Use the category-based creation cost logic for weapons
// Weapons are always in the "Waffen" category
difficulty := wscd.SkillDifficulty.Name
leCost := getSkillCreationCost("Waffen", difficulty)
skillInfo := gin.H{
"name": weaponName,
"leCost": leCost,
"difficulty": difficulty,
"type": "weapon", // Mark as weapon skill
}
result = append(result, skillInfo)
}
return result, 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 {
// 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
skillInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skill.Name, characterClass)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
var bestCategory, difficulty string
if skillInfo != nil {
bestCategory = skillInfo.CategoryName
difficulty = skillInfo.DifficultyName
}
var learnCost int
// error cannot be nil at this point
//if err == nil && bestCategory != "" {
if 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")
// Parse LernCostRequest aus POST body
var baseRequest gsmaster.LernCostRequest
if err := c.ShouldBindJSON(&baseRequest); err != nil {
respondWithError(c, http.StatusBadRequest, "Ungültige Anfrageparameter: "+err.Error())
return
}
var character models.Char
if err := database.DB.Preload("Zauber").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, baseRequest.CharId).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
charakteClass := getCharacterClass(&character)
// Hole alle verfügbaren Zauber aus der gsmaster Datenbank
var allSpells []models.Spell
allSpells, err := models.SelectSpells("", "")
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve spells from gsmaster")
return
}
// Erstelle eine Map der bereits gelernten Zauber
learnedSpells := make(map[string]bool)
for _, spell := range character.Zauber {
learnedSpells[spell.Name] = true
}
// Organisiere Zauber nach Schulen (analog zu Kategorien bei Fertigkeiten)
spellsBySchool := make(map[string][]gin.H)
for _, spell := range allSpells {
// Überspringe bereits gelernte Zauber
if learnedSpells[spell.Name] {
continue
}
// Erstelle LernCostRequest für diesen Zauber basierend auf der Basis-Anfrage
request := baseRequest
request.CharId = character.ID
request.Name = spell.Name
request.CurrentLevel = 0 // Nicht gelernt
request.TargetLevel = 1 // Auf Level 1 lernen
request.Type = "spell"
request.Action = "learn"
// Erstelle SkillCostResultNew
levelResult := gsmaster.SkillCostResultNew{
CharacterID: fmt.Sprintf("%d", character.ID),
CharacterClass: charakteClass,
SkillName: spell.Name,
TargetLevel: 1,
}
remainingPP := request.UsePP
remainingGold := request.UseGold
// Hole die vollständigen Spell-Informationen für die Kostenberechnung
spellLearningInfo, err := models.GetSpellLearningInfoNewSystem(spell.Name, charakteClass)
if err != nil {
// Fallback für unbekannte Zauber
spellLearningInfo = &models.SpellLearningInfo{
SpellName: spell.Name,
SpellLevel: spell.Stufe,
SchoolName: spell.Category,
LERequired: 20, // Standard-Lernkosten für Zauber
}
}
// Berechne Lernkosten mit calculateSpellLearnCostNewSystem
err = calculateSpellLearnCostNewSystem(&request, &levelResult, &remainingPP, &remainingGold, spellLearningInfo)
epCost := 10000 // Fallback-Wert
goldCost := 50000 // Fallback-Wert
if err == nil {
epCost = levelResult.EP
goldCost = levelResult.GoldCost
}
spellInfo := gin.H{
"name": spell.Name,
"level": spell.Stufe,
"epCost": epCost,
"goldCost": goldCost,
}
school := spell.Category
if school == "" {
school = "Sonstige"
}
spellsBySchool[school] = append(spellsBySchool[school], spellInfo)
}
c.JSON(http.StatusOK, gin.H{
"spells_by_school": spellsBySchool,
})
}
// GetSpellDetails gibt detaillierte Informationen zu einem bestimmten Zauber zurück
func GetSpellDetails(c *gin.Context) {
spellName := c.Query("name")
if spellName == "" {
respondWithError(c, http.StatusBadRequest, "Zaubername ist erforderlich")
return
}
// Lade den Zauber aus der Datenbank
var spell models.Spell
if err := database.DB.Where("name = ?", spellName).First(&spell).Error; err != nil {
respondWithError(c, http.StatusNotFound, "Zauber nicht gefunden")
return
}
// Erstelle Response mit allen verfügbaren Details
spellDetails := gin.H{
"id": spell.ID,
"name": spell.Name,
"beschreibung": spell.Beschreibung,
"level": spell.Stufe,
"bonus": spell.Bonus,
"ap": spell.AP,
"art": spell.Art,
"zauberdauer": spell.Zauberdauer,
"reichweite": spell.Reichweite,
"wirkungsziel": spell.Wirkungsziel,
"wirkungsbereich": spell.Wirkungsbereich,
"wirkungsdauer": spell.Wirkungsdauer,
"ursprung": spell.Ursprung,
"category": spell.Category,
"learning_category": spell.LearningCategory,
"quelle": spell.Quelle,
"page_number": spell.PageNumber,
"game_system": spell.GameSystem,
}
c.JSON(http.StatusOK, gin.H{
"spell": spellDetails,
})
}
// Character Creation Session Management
// CreateCharacterSession erstellt eine neue Charakter-Erstellungssession
func CreateCharacterSession(c *gin.Context) {
logger.Debug("CreateCharacterSession aufgerufen")
// Debug: Alle Kontext-Keys anzeigen
keys := make([]string, 0)
for key := range c.Keys {
keys = append(keys, fmt.Sprintf("%s=%v", key, c.Keys[key]))
}
logger.Debug("CreateCharacterSession: Verfügbare Kontext-Keys: [%s]", strings.Join(keys, ", "))
userID := c.GetUint("userID")
logger.Debug("CreateCharacterSession: UserID = %d", userID)
if userID == 0 {
logger.Warn("CreateCharacterSession: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
sessionID := fmt.Sprintf("char_create_%d_%d", userID, time.Now().Unix())
logger.Debug("CreateCharacterSession: Generierte SessionID = %s", sessionID)
session := models.CharacterCreationSession{
ID: sessionID,
UserID: userID,
Name: "",
Geschlecht: "",
Rasse: "",
Typ: "",
Herkunft: "",
Stand: "",
Glaube: "",
Attributes: models.AttributesData{},
DerivedValues: models.DerivedValuesData{},
Skills: models.CharacterCreationSkills{},
Spells: models.CharacterCreationSpells{},
SkillPoints: models.SkillPointsData{},
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ExpiresAt: time.Now().AddDate(0, 0, 14), // 14 Tage
CurrentStep: 1,
}
logger.Debug("CreateCharacterSession: Session-Struktur erstellt, ExpiresAt = %s", session.ExpiresAt.Format(time.RFC3339))
// Session in Datenbank speichern
logger.Debug("CreateCharacterSession: Speichere Session in Datenbank...")
err := database.DB.Create(&session).Error
if err != nil {
logger.Error("CreateCharacterSession: Fehler beim Erstellen der Session: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
return
}
logger.Info("CreateCharacterSession: Session erfolgreich erstellt - SessionID: %s, UserID: %d", sessionID, userID)
c.JSON(http.StatusCreated, gin.H{
"session_id": sessionID,
"expires_at": session.ExpiresAt,
})
}
// ListCharacterSessions gibt alle aktiven Sessions für einen Benutzer zurück
func ListCharacterSessions(c *gin.Context) {
logger.Debug("ListCharacterSessions aufgerufen")
userID := c.GetUint("userID")
logger.Debug("ListCharacterSessions: UserID = %d", userID)
if userID == 0 {
logger.Warn("ListCharacterSessions: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Sessions aus Datenbank laden
logger.Debug("ListCharacterSessions: Lade Sessions für UserID %d aus Datenbank...", userID)
sessions, err := models.GetUserSessions(database.DB, userID)
if err != nil {
logger.Error("ListCharacterSessions: Fehler beim Laden der Sessions: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load sessions"})
return
}
logger.Debug("ListCharacterSessions: Gefundene Sessions: %d", len(sessions))
// Sessions für Frontend formatieren
var formattedSessions []gin.H
for i, session := range sessions {
// Schritt-Text bestimmen
progressText := getProgressText(session.CurrentStep)
logger.Debug("ListCharacterSessions: Formatiere Session %d - ID: %s, Step: %d, Name: %s",
i+1, session.ID, session.CurrentStep, session.Name)
formattedSessions = append(formattedSessions, gin.H{
"session_id": session.ID,
"name": session.Name,
"rasse": session.Rasse,
"typ": session.Typ,
"current_step": session.CurrentStep,
"total_steps": 5,
"created_at": session.CreatedAt,
"updated_at": session.UpdatedAt,
"expires_at": session.ExpiresAt,
"progress_text": progressText,
})
}
logger.Info("ListCharacterSessions: Sessions erfolgreich geladen für UserID %d - Anzahl: %d", userID, len(formattedSessions))
c.JSON(http.StatusOK, gin.H{
"sessions": formattedSessions,
"count": len(formattedSessions),
})
}
// getProgressText gibt den Schritt-Text für die Frontend-Anzeige zurück
func getProgressText(step int) string {
logger.Debug("getProgressText: Ermittle Text für Schritt %d", step)
var text string
switch step {
case 1:
text = "Grundinformationen"
case 2:
text = "Attribute"
case 3:
text = "Abgeleitete Werte"
case 4:
text = "Fertigkeiten"
case 5:
text = "Zauber"
default:
text = "Unbekannt"
logger.Warn("getProgressText: Unbekannter Schritt %d", step)
}
logger.Debug("getProgressText: Schritt %d = '%s'", step, text)
return text
}
// GetCharacterSession gibt Session-Daten zurück
func GetCharacterSession(c *gin.Context) {
logger.Debug("GetCharacterSession aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("GetCharacterSession: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("GetCharacterSession: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Session aus Datenbank laden
logger.Debug("GetCharacterSession: Lade Session aus Datenbank...")
var session models.CharacterCreationSession
err := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error
if err != nil {
logger.Error("GetCharacterSession: Session nicht gefunden - SessionID: %s, UserID: %d, Error: %s",
sessionID, userID, err.Error())
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Debug("GetCharacterSession: Session gefunden - Name: %s, Step: %d, ExpiresAt: %s",
session.Name, session.CurrentStep, session.ExpiresAt.Format(time.RFC3339))
// Prüfen ob Session noch gültig ist
if session.ExpiresAt.Before(time.Now()) {
logger.Warn("GetCharacterSession: Session abgelaufen - SessionID: %s, ExpiresAt: %s",
sessionID, session.ExpiresAt.Format(time.RFC3339))
// Abgelaufene Session löschen
logger.Debug("GetCharacterSession: Lösche abgelaufene Session...")
database.DB.Delete(&session)
c.JSON(http.StatusGone, gin.H{"error": "Session expired"})
return
}
logger.Info("GetCharacterSession: Session erfolgreich geladen - SessionID: %s, UserID: %d, Step: %d",
sessionID, userID, session.CurrentStep)
c.JSON(http.StatusOK, session)
}
// UpdateCharacterBasicInfo Request
type UpdateBasicInfoRequest struct {
Name string `json:"name" binding:"required,min=2,max=50"`
Geschlecht string `json:"geschlecht" binding:"required"`
Rasse string `json:"rasse" binding:"required"`
Typ string `json:"typ" binding:"required"`
Herkunft string `json:"herkunft" binding:"required"`
Stand string `json:"stand" binding:"required"`
Glaube string `json:"glaube"`
}
// UpdateCharacterBasicInfo speichert Grundinformationen
func UpdateCharacterBasicInfo(c *gin.Context) {
logger.Debug("UpdateCharacterBasicInfo aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("UpdateCharacterBasicInfo: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("UpdateCharacterBasicInfo: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var request UpdateBasicInfoRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error("UpdateCharacterBasicInfo: Ungültige Eingabedaten - %s", err.Error())
respondWithError(c, http.StatusBadRequest, "Ungültige Eingabedaten: "+err.Error())
return
}
logger.Debug("UpdateCharacterBasicInfo: Request-Daten - Name: %s, Geschlecht: %s, Rasse: %s, Typ: %s, Herkunft: %s, Stand: %s, Glaube: %s",
request.Name, request.Geschlecht, request.Rasse, request.Typ, request.Herkunft, request.Stand, request.Glaube)
// Session aus Datenbank laden
logger.Debug("UpdateCharacterBasicInfo: Lade Session aus Datenbank...")
var session models.CharacterCreationSession
err := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error
if err != nil {
logger.Error("UpdateCharacterBasicInfo: Session nicht gefunden - SessionID: %s, UserID: %d, Error: %s",
sessionID, userID, err.Error())
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Debug("UpdateCharacterBasicInfo: Aktueller Session-Status - Step: %d, Name: %s",
session.CurrentStep, session.Name)
// Grundinformationen aktualisieren
session.Name = request.Name
session.Geschlecht = request.Geschlecht
session.Rasse = request.Rasse
session.Typ = request.Typ
session.Herkunft = request.Herkunft
session.Stand = request.Stand
session.Glaube = request.Glaube
session.CurrentStep = 2
session.UpdatedAt = time.Now()
logger.Debug("UpdateCharacterBasicInfo: Session aktualisiert, setze CurrentStep auf 2")
// Session in Datenbank aktualisieren
logger.Debug("UpdateCharacterBasicInfo: Speichere Session in Datenbank...")
err = database.DB.Save(&session).Error
if err != nil {
logger.Error("UpdateCharacterBasicInfo: Fehler beim Speichern der Session: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update session"})
return
}
logger.Info("UpdateCharacterBasicInfo: Grundinformationen erfolgreich gespeichert - SessionID: %s, Name: %s",
sessionID, request.Name)
c.JSON(http.StatusOK, gin.H{
"message": "Grundinformationen gespeichert",
"session_id": sessionID,
"current_step": 2,
})
}
// UpdateAttributesRequest
type UpdateAttributesRequest struct {
ST int `json:"st" binding:"required,min=1,max=100"` // Stärke
GS int `json:"gs" binding:"required,min=1,max=100"` // Geschicklichkeit
GW int `json:"gw" binding:"required,min=1,max=100"` // Gewandtheit
KO int `json:"ko" binding:"required,min=1,max=100"` // Konstitution
IN int `json:"in" binding:"required,min=1,max=100"` // Intelligenz
ZT int `json:"zt" binding:"required,min=1,max=100"` // Zaubertalent
AU int `json:"au" binding:"required,min=1,max=100"` // Ausstrahlung
}
// UpdateCharacterAttributes speichert Grundwerte
func UpdateCharacterAttributes(c *gin.Context) {
logger.Debug("UpdateCharacterAttributes aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("UpdateCharacterAttributes: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("UpdateCharacterAttributes: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var request UpdateAttributesRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error("UpdateCharacterAttributes: Ungültige Attributswerte - %s", err.Error())
respondWithError(c, http.StatusBadRequest, "Ungültige Attributswerte: "+err.Error())
return
}
logger.Debug("UpdateCharacterAttributes: Attribute - ST:%d GS:%d GW:%d KO:%d IN:%d ZT:%d AU:%d",
request.ST, request.GS, request.GW, request.KO, request.IN, request.ZT, request.AU)
// Session aus Datenbank laden
logger.Debug("UpdateCharacterAttributes: Lade Session aus Datenbank...")
var session models.CharacterCreationSession
err := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error
if err != nil {
logger.Error("UpdateCharacterAttributes: Session nicht gefunden - SessionID: %s, UserID: %d, Error: %s",
sessionID, userID, err.Error())
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Debug("UpdateCharacterAttributes: Session geladen - CurrentStep: %d", session.CurrentStep)
// Attribute aktualisieren
session.Attributes = models.AttributesData{
ST: request.ST,
GS: request.GS,
GW: request.GW,
KO: request.KO,
IN: request.IN,
ZT: request.ZT,
AU: request.AU,
}
session.CurrentStep = 3
session.UpdatedAt = time.Now()
logger.Debug("UpdateCharacterAttributes: Attribute gesetzt, CurrentStep auf 3 aktualisiert")
// Session in Datenbank aktualisieren
logger.Debug("UpdateCharacterAttributes: Speichere Session in Datenbank...")
err = database.DB.Save(&session).Error
if err != nil {
logger.Error("UpdateCharacterAttributes: Fehler beim Speichern der Session: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update session"})
return
}
logger.Info("UpdateCharacterAttributes: Grundwerte erfolgreich gespeichert - SessionID: %s", sessionID)
c.JSON(http.StatusOK, gin.H{
"message": "Grundwerte gespeichert",
"session_id": sessionID,
"current_step": 3,
})
}
// UpdateDerivedValuesRequest
type UpdateDerivedValuesRequest struct {
PA int `json:"pa" binding:"required,min=1,max=100"` // Persönliche Ausstrahlung
WK int `json:"wk" binding:"required,min=1,max=100"` // Willenskraft
LP_Max int `json:"lp_max" binding:"required,min=1,max=50"` // Lebenspunkte Maximum
AP_Max int `json:"ap_max" binding:"required,min=1,max=200"` // Abenteuerpunkte Maximum
B_Max int `json:"b_max" binding:"required,min=1,max=50"` // Belastung Maximum
ResistenzKoerper int `json:"resistenz_koerper" binding:"required,min=1,max=20"` // Resistenz Körper
ResistenzGeist int `json:"resistenz_geist" binding:"required,min=1,max=20"` // Resistenz Geist
ResistenzBonusKoerper int `json:"resistenz_bonus_koerper" binding:"min=-5,max=5"` // Resistenz Bonus Körper
ResistenzBonusGeist int `json:"resistenz_bonus_geist" binding:"min=-5,max=5"` // Resistenz Bonus Geist
Abwehr int `json:"abwehr" binding:"required,min=1,max=20"` // Abwehr
AbwehrBonus int `json:"abwehr_bonus" binding:"min=-5,max=5"` // Abwehr Bonus
AusdauerBonus int `json:"ausdauer_bonus" binding:"min=-50,max=50"` // Ausdauer Bonus
AngriffsBonus int `json:"angriffs_bonus" binding:"min=-5,max=5"` // Angriffs Bonus
Zaubern int `json:"zaubern" binding:"required,min=1,max=20"` // Zaubern
ZauberBonus int `json:"zauber_bonus" binding:"min=-5,max=5"` // Zauber Bonus
Raufen int `json:"raufen" binding:"required,min=1,max=20"` // Raufen
SchadensBonus int `json:"schadens_bonus" binding:"min=-10,max=10"` // Schadens Bonus
SG int `json:"sg" binding:"min=0,max=50"` // Schicksalsgunst
GG int `json:"gg" binding:"min=0,max=50"` // Göttliche Gnade
GP int `json:"gp" binding:"min=0,max=50"` // Glückspunkte
}
// UpdateCharacterDerivedValues speichert abgeleitete Werte
func UpdateCharacterDerivedValues(c *gin.Context) {
logger.Debug("UpdateCharacterDerivedValues aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("UpdateCharacterDerivedValues: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("UpdateCharacterDerivedValues: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var request UpdateDerivedValuesRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error("UpdateCharacterDerivedValues: Ungültige abgeleitete Werte - %s", err.Error())
respondWithError(c, http.StatusBadRequest, "Ungültige abgeleitete Werte: "+err.Error())
return
}
logger.Debug("UpdateCharacterDerivedValues: Werte - LP_Max:%d AP_Max:%d B_Max:%d PA:%d WK:%d SG:%d GG:%d GP:%d",
request.LP_Max, request.AP_Max, request.B_Max, request.PA, request.WK, request.SG, request.GG, request.GP)
// Session aus Datenbank laden
logger.Debug("UpdateCharacterDerivedValues: Lade Session aus Datenbank...")
var session models.CharacterCreationSession
err := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error
if err != nil {
logger.Error("UpdateCharacterDerivedValues: Session nicht gefunden - SessionID: %s, UserID: %d, Error: %s",
sessionID, userID, err.Error())
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Debug("UpdateCharacterDerivedValues: Session geladen - CurrentStep: %d", session.CurrentStep)
// Abgeleitete Werte aktualisieren
session.DerivedValues = models.DerivedValuesData{
PA: request.PA,
WK: request.WK,
LPMax: request.LP_Max,
APMax: request.AP_Max,
BMax: request.B_Max,
ResistenzKoerper: request.ResistenzKoerper,
ResistenzGeist: request.ResistenzGeist,
ResistenzBonusKoerper: request.ResistenzBonusKoerper,
ResistenzBonusGeist: request.ResistenzBonusGeist,
Abwehr: request.Abwehr,
AbwehrBonus: request.AbwehrBonus,
AusdauerBonus: request.AusdauerBonus,
AngriffsBonus: request.AngriffsBonus,
Zaubern: request.Zaubern,
ZauberBonus: request.ZauberBonus,
Raufen: request.Raufen,
SchadensBonus: request.SchadensBonus,
SG: request.SG,
GG: request.GG,
GP: request.GP,
}
session.CurrentStep = 4
session.UpdatedAt = time.Now()
logger.Debug("UpdateCharacterDerivedValues: Abgeleitete Werte gesetzt, CurrentStep auf 4 aktualisiert")
// Session in Datenbank aktualisieren
logger.Debug("UpdateCharacterDerivedValues: Speichere Session in Datenbank...")
err = database.DB.Save(&session).Error
if err != nil {
logger.Error("UpdateCharacterDerivedValues: Fehler beim Speichern der Session: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update session"})
return
}
logger.Info("UpdateCharacterDerivedValues: Abgeleitete Werte erfolgreich gespeichert - SessionID: %s", sessionID)
c.JSON(http.StatusOK, gin.H{
"message": "Abgeleitete Werte gespeichert",
"session_id": sessionID,
"current_step": 4,
})
}
// UpdateSkillsRequest
type UpdateSkillsRequest struct {
Skills models.CharacterCreationSkills `json:"skills"`
Spells models.CharacterCreationSpells `json:"spells"`
SkillPoints models.SkillPointsData `json:"skill_points"` // Verbleibende Punkte pro Kategorie
}
// UpdateCharacterSkills speichert Fertigkeiten und Zauber
func UpdateCharacterSkills(c *gin.Context) {
logger.Debug("UpdateCharacterSkills aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("UpdateCharacterSkills: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("UpdateCharacterSkills: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
var request UpdateSkillsRequest
if err := c.ShouldBindJSON(&request); err != nil {
logger.Error("UpdateCharacterSkills: Ungültige Fertigkeitsdaten - %s", err.Error())
respondWithError(c, http.StatusBadRequest, "Ungültige Fertigkeitsdaten: "+err.Error())
return
}
logger.Debug("UpdateCharacterSkills: Skills-Anzahl: %d, Spells-Anzahl: %d",
len(request.Skills), len(request.Spells))
// Session aus Datenbank laden
logger.Debug("UpdateCharacterSkills: Lade Session aus Datenbank...")
var session models.CharacterCreationSession
err := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error
if err != nil {
logger.Error("UpdateCharacterSkills: Session nicht gefunden - SessionID: %s, UserID: %d, Error: %s",
sessionID, userID, err.Error())
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Debug("UpdateCharacterSkills: Session geladen - CurrentStep: %d", session.CurrentStep)
// Fertigkeiten und Zauber aktualisieren
session.Skills = request.Skills
session.Spells = request.Spells
session.SkillPoints = request.SkillPoints
session.CurrentStep = 5
session.UpdatedAt = time.Now()
logger.Debug("UpdateCharacterSkills: Skills/Spells gesetzt, CurrentStep auf 5 aktualisiert")
// Session in Datenbank aktualisieren
logger.Debug("UpdateCharacterSkills: Speichere Session in Datenbank...")
err = database.DB.Save(&session).Error
if err != nil {
logger.Error("UpdateCharacterSkills: Fehler beim Speichern der Session: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update session"})
return
}
logger.Info("UpdateCharacterSkills: Fertigkeiten erfolgreich gespeichert - SessionID: %s, Skills: %d, Spells: %d",
sessionID, len(request.Skills), len(request.Spells))
c.JSON(http.StatusOK, gin.H{
"message": "Fertigkeiten gespeichert",
"session_id": sessionID,
"current_step": 5,
})
}
// FinalizeCharacterCreation schließt die Charakter-Erstellung ab
func FinalizeCharacterCreation(c *gin.Context) {
logger.Debug("FinalizeCharacterCreation aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("FinalizeCharacterCreation: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("FinalizeCharacterCreation: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Session laden
logger.Debug("FinalizeCharacterCreation: Lade Session aus Datenbank...")
var session models.CharacterCreationSession
err := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).First(&session).Error
if err != nil {
logger.Error("FinalizeCharacterCreation: Session nicht gefunden - SessionID: %s, UserID: %d, Error: %s",
sessionID, userID, err.Error())
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Debug("FinalizeCharacterCreation: Session geladen - Name: %s, CurrentStep: %d",
session.Name, session.CurrentStep)
// Session validieren
if session.CurrentStep < 5 {
logger.Warn("FinalizeCharacterCreation: Charakter-Erstellung unvollständig - CurrentStep: %d (erwartet: 5)",
session.CurrentStep)
c.JSON(http.StatusBadRequest, gin.H{"error": "Character creation not complete"})
return
}
logger.Debug("FinalizeCharacterCreation: Erstelle Charakter-Struktur...")
// Character erstellen
char := models.Char{
BamortBase: models.BamortBase{
Name: session.Name,
},
UserID: userID,
Rasse: session.Rasse,
Typ: session.Typ,
Gender: session.Geschlecht,
SocialClass: session.Stand,
Herkunft: session.Herkunft,
Glaube: session.Glaube,
Public: false, // Default to private
Grad: 1, // Default starting grade
// Static derived values (can increase with grade)
ResistenzKoerper: session.DerivedValues.ResistenzKoerper,
ResistenzGeist: session.DerivedValues.ResistenzGeist,
Abwehr: session.DerivedValues.Abwehr,
Zaubern: session.DerivedValues.Zaubern,
Raufen: session.DerivedValues.Raufen,
// Lebenspunkte
Lp: models.Lp{
Max: session.DerivedValues.LPMax,
Value: session.DerivedValues.LPMax,
},
// Ausdauerpunkte
Ap: models.Ap{
Max: session.DerivedValues.APMax,
Value: session.DerivedValues.APMax,
},
// Bewegung
B: models.B{
Max: session.DerivedValues.BMax,
Value: session.DerivedValues.BMax,
},
Vermoegen: models.Vermoegen{
BamortCharTrait: models.BamortCharTrait{
UserID: userID,
},
Goldstuecke: 80,
},
// Bennies (Glückspunkte, etc.)
Bennies: models.Bennies{
BamortCharTrait: models.BamortCharTrait{
UserID: userID,
},
Gg: session.DerivedValues.GG,
Gp: session.DerivedValues.GP,
Sg: session.DerivedValues.SG,
},
}
// Eigenschaften (Attribute) hinzufügen
char.Eigenschaften = []models.Eigenschaft{
{UserID: userID, Name: "St", Value: session.Attributes.ST},
{UserID: userID, Name: "Gs", Value: session.Attributes.GS},
{UserID: userID, Name: "Gw", Value: session.Attributes.GW},
{UserID: userID, Name: "Ko", Value: session.Attributes.KO},
{UserID: userID, Name: "In", Value: session.Attributes.IN},
{UserID: userID, Name: "Zt", Value: session.Attributes.ZT},
{UserID: userID, Name: "Au", Value: session.Attributes.AU},
{UserID: userID, Name: "pA", Value: session.DerivedValues.PA}, // PA kommt aus derived values
{UserID: userID, Name: "Wk", Value: session.DerivedValues.WK}, // WK kommt aus derived values
}
logger.Debug("FinalizeCharacterCreation: Charakter-Struktur erstellt mit %d Eigenschaften",
len(char.Eigenschaften))
// Fertigkeiten aus der Session übertragen
logger.Debug("FinalizeCharacterCreation: Übertrage %d Fertigkeiten", len(session.Skills))
for _, skill := range session.Skills {
// Suche den Initialwert der Fertigkeit aus der Datenbank
dbSkill := models.Skill{}
err := dbSkill.First(skill.Name)
if err != nil {
logger.Warn("FinalizeCharacterCreation: Konnte Fertigkeit '%s' nicht in der Datenbank finden, verwende Level %d", skill.Name, skill.Level)
}
// Verwende den Initialwert aus der Datenbank wenn verfügbar, sonst fallback auf Session-Level
initialValue := skill.Level // Fallback
if err == nil {
initialValue = dbSkill.Initialwert
logger.Debug("FinalizeCharacterCreation: Verwende Initialwert %d für Fertigkeit '%s'", initialValue, skill.Name)
}
// Unterscheide zwischen normalen Fertigkeiten und Waffenfertigkeiten
if skill.Category == "Waffen" || skill.Category == "waffen" {
dbWPSkill := models.WeaponSkill{}
err := dbWPSkill.First(skill.Name)
if err != nil {
logger.Warn("FinalizeCharacterCreation: Konnte WaffenFertigkeit '%s' nicht in der Datenbank finden, verwende Level %d", skill.Name, skill.Level)
}
// Verwende den Initialwert aus der Datenbank wenn verfügbar, sonst fallback auf Session-Level
initialValue := skill.Level // Fallback
if err == nil {
initialValue = dbWPSkill.Initialwert
logger.Debug("FinalizeCharacterCreation: Verwende Initialwert %d für Fertigkeit '%s'", initialValue, skill.Name)
}
// Waffenfertigkeit
weaponSkill := models.SkWaffenfertigkeit{
SkFertigkeit: models.SkFertigkeit{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: skill.Name,
},
UserID: userID,
},
Fertigkeitswert: initialValue,
Improvable: true,
Category: skill.Category,
},
}
char.Waffenfertigkeiten = append(char.Waffenfertigkeiten, weaponSkill)
} else {
// Normale Fertigkeit
normalSkill := models.SkFertigkeit{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: skill.Name,
},
CharacterID: char.ID,
UserID: userID,
},
Fertigkeitswert: initialValue,
Improvable: true,
Category: skill.Category,
}
char.Fertigkeiten = append(char.Fertigkeiten, normalSkill)
}
}
// Zauber aus der Session übertragen
logger.Debug("FinalizeCharacterCreation: Übertrage %d Zauber", len(session.Spells))
for _, spell := range session.Spells {
zauber := models.SkZauber{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: spell.Name,
},
CharacterID: char.ID,
UserID: userID,
},
}
char.Zauber = append(char.Zauber, zauber)
}
logger.Debug("FinalizeCharacterCreation: Charakter vollständig erstellt - %d Fertigkeiten, %d Waffenfertigkeiten, %d Zauber",
len(char.Fertigkeiten), len(char.Waffenfertigkeiten), len(char.Zauber))
// Character in Datenbank speichern
logger.Debug("FinalizeCharacterCreation: Speichere Charakter in Datenbank...")
err = char.Create()
if err != nil {
logger.Error("FinalizeCharacterCreation: Fehler beim Erstellen des Charakters: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create character: " + err.Error()})
return
}
logger.Debug("FinalizeCharacterCreation: Charakter erfolgreich erstellt mit ID: %d", char.ID)
// Session löschen
logger.Debug("FinalizeCharacterCreation: Lösche Session aus Datenbank...")
database.DB.Delete(&session)
logger.Info("FinalizeCharacterCreation: Charakter-Erstellung abgeschlossen - CharacterID: %d, SessionID: %s, Name: %s",
char.ID, sessionID, session.Name)
c.JSON(http.StatusCreated, gin.H{
"message": "Charakter erfolgreich erstellt",
"character_id": char.ID,
"session_id": sessionID,
})
}
// DeleteCharacterSession löscht eine Session
func DeleteCharacterSession(c *gin.Context) {
logger.Debug("DeleteCharacterSession aufgerufen")
sessionID := c.Param("sessionId")
userID := c.GetUint("userID")
logger.Debug("DeleteCharacterSession: SessionID = %s, UserID = %d", sessionID, userID)
if userID == 0 {
logger.Warn("DeleteCharacterSession: Unauthorized - UserID ist 0")
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
return
}
// Session aus Datenbank löschen (nur eigene Sessions)
logger.Debug("DeleteCharacterSession: Lösche Session aus Datenbank...")
result := database.DB.Where("id = ? AND user_id = ?", sessionID, userID).Delete(&models.CharacterCreationSession{})
if result.Error != nil {
logger.Error("DeleteCharacterSession: Fehler beim Löschen der Session: %s", result.Error.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete session"})
return
}
if result.RowsAffected == 0 {
logger.Warn("DeleteCharacterSession: Session nicht gefunden oder bereits gelöscht - SessionID: %s, UserID: %d",
sessionID, userID)
c.JSON(http.StatusNotFound, gin.H{"error": "Session not found"})
return
}
logger.Info("DeleteCharacterSession: Session erfolgreich gelöscht - SessionID: %s, UserID: %d, RowsAffected: %d",
sessionID, userID, result.RowsAffected)
c.JSON(http.StatusOK, gin.H{
"message": "Session gelöscht",
"session_id": sessionID,
})
}
// Reference Data Handlers
// GetRaces gibt verfügbare Rassen zurück
func GetRaces(c *gin.Context) {
// TODO: Aus Datenbank laden
races := []string{
"Mensch", "Elf", "Halbling", "Zwerg", "Gnom",
}
c.JSON(http.StatusOK, gin.H{"races": races})
}
// GetCharacterClasses gibt verfügbare Klassen zurück
func GetCharacterClasses(c *gin.Context) {
// Get game system from query parameter, default to GameSystemId: 1
gameSystem := c.DefaultQuery("game_system", "midgard")
// Load character classes from database
classes, err := models.GetCharacterClassesByActiveSources(gameSystem)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load character classes"})
return
}
// Extract class names for the frontend
var classNames []string
for _, class := range classes {
classNames = append(classNames, class.Name)
}
c.JSON(http.StatusOK, gin.H{"classes": classNames})
}
// GetOrigins gibt verfügbare Herkünfte zurück
func GetOrigins(c *gin.Context) {
// TODO: Aus Datenbank laden
origins := []string{
"Alba", "Aran", "Buluga", "Chryseia",
"Eschar", "Fuardain", "Ikenga", "KanThaiPan", "Küstenstaaten",
"Medjis", "Moravod", "Nahuatlan", "Rawindra", "Scharidis",
"Tegarisch Steppe", "Valian", "Waeland", "Ywerddon",
}
c.JSON(http.StatusOK, gin.H{"origins": origins})
}
// SearchBeliefs sucht Glaubensrichtungen
func SearchBeliefs(c *gin.Context) {
query := c.Query("q")
if len(query) < 2 {
c.JSON(http.StatusBadRequest, gin.H{"error": "Mindestens 2 Zeichen erforderlich"})
return
}
// Get game system from query parameter, default to GameSystemId: 1
gameSystem := c.DefaultQuery("game_system", "midgard")
// Load beliefs from database
believes, err := models.GetBelievesByActiveSources(gameSystem)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load beliefs from database: " + err.Error()})
return
}
// Extract belief names and filter by query
var allBeliefs []string
for _, belief := range believes {
allBeliefs = append(allBeliefs, belief.Name)
}
var results []string
queryLower := strings.ToLower(query)
for _, belief := range allBeliefs {
if strings.Contains(strings.ToLower(belief), queryLower) {
results = append(results, belief)
}
}
c.JSON(http.StatusOK, gin.H{"beliefs": results})
}
// SkillCategoryWithPoints repräsentiert eine Kategorie mit verfügbaren Lernpunkten
type SkillCategoryWithPoints struct {
Name string `json:"name"`
DisplayName string `json:"display_name"`
Points int `json:"points"`
MaxPoints int `json:"max_points"`
}
// LearningPointsData repräsentiert die Lernpunkte und typischen Fertigkeiten einer Charakterklasse
type LearningPointsData struct {
ClassName string `json:"class_name"`
ClassCode string `json:"class_code"`
LearningPoints map[string]int `json:"learning_points"` // Kategorie -> Lernpunkte
WeaponPoints int `json:"weapon_points"` // Waffenlernpunkte
SpellPoints int `json:"spell_points"` // Zauberlerneinheiten (falls vorhanden)
TypicalSkills []TypicalSkill `json:"typical_skills"` // Typische Fertigkeiten
TypicalSpells []string `json:"typical_spells"` // Typische Zauber (falls vorhanden)
}
// TypicalSkill repräsentiert eine typische Fertigkeit mit Bonus
type TypicalSkill struct {
Name string `json:"name"`
Bonus int `json:"bonus"`
Attribute string `json:"attribute"` // Zugehöriges Attribut (z.B. "Gs", "In")
Notes string `json:"notes"` // Zusätzliche Notizen
}
// GetCharacterClassLearningPoints gibt die Lernpunkte und typischen Fertigkeiten für eine Charakterklasse zurück
func GetCharacterClassLearningPoints(c *gin.Context) {
className := c.Query("class")
if className == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Charakterklassen-Name ist erforderlich (Parameter 'class')"})
return
}
stand := c.Query("stand") // Optional: Unfreie, Volk, Mittelschicht, Adel
// Hole die Lernpunkte-Daten für die Klasse
learningData, err := getLearningPointsForClass(className, stand)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Charakterklasse nicht gefunden oder nicht unterstützt: " + err.Error()})
return
}
c.JSON(http.StatusOK, learningData)
}
// getLearningPointsForClass gibt die Lernpunkte-Daten für eine bestimmte Charakterklasse zurück
func getLearningPointsForClass(className string, stand string) (*LearningPointsData, error) {
// Get character class from database by name or code
var charClass models.CharacterClass
result := database.DB.Where("name = ? OR code = ?", className, className).First(&charClass)
if result.Error != nil {
return nil, fmt.Errorf("character class not found: %s", className)
}
// Get learning points from database
learningPoints, err := models.GetLearningPointsForClass(charClass.ID)
if err != nil {
return nil, fmt.Errorf("failed to get learning points: %w", err)
}
// Convert to map format
learningPointsMap := make(map[string]int)
for _, lp := range learningPoints {
learningPointsMap[lp.SkillCategory.Name] = lp.Points
}
// Get spell points if applicable
spellPoints, err := models.GetSpellPointsForClass(charClass.ID)
if err != nil {
return nil, fmt.Errorf("failed to get spell points: %w", err)
}
// Get typical skills
typicalSkillsDB, err := models.GetTypicalSkillsForClass(charClass.ID)
if err != nil {
return nil, fmt.Errorf("failed to get typical skills: %w", err)
}
// Convert typical skills
typicalSkills := make([]TypicalSkill, len(typicalSkillsDB))
for i, ts := range typicalSkillsDB {
typicalSkills[i] = TypicalSkill{
Name: ts.Skill.Name,
Bonus: ts.Bonus,
Attribute: ts.Attribute,
Notes: ts.Notes,
}
}
// Get typical spells
typicalSpellsDB, err := models.GetTypicalSpellsForClass(charClass.ID)
if err != nil {
return nil, fmt.Errorf("failed to get typical spells: %w", err)
}
// Convert typical spells
typicalSpells := make([]string, len(typicalSpellsDB))
for i, ts := range typicalSpellsDB {
spellName := ts.Spell.Name
if ts.Notes != "" {
spellName = ts.Notes // Use notes for special cases like "beliebig außer..."
}
typicalSpells[i] = spellName
}
// Build the response data
data := &LearningPointsData{
ClassName: charClass.Name,
ClassCode: charClass.Code,
LearningPoints: learningPointsMap,
SpellPoints: spellPoints.SpellPoints,
TypicalSkills: typicalSkills,
TypicalSpells: typicalSpells,
}
// Bonus-Lernpunkte basierend auf Stand hinzufügen
if stand != "" {
standBonus := getStandBonusPoints(stand)
// Füge die Stand-Bonuspunkte zu den normalen Lernpunkten hinzu
for category, bonus := range standBonus {
if currentPoints, exists := data.LearningPoints[category]; exists {
data.LearningPoints[category] = currentPoints + bonus
} else {
// Falls die Kategorie noch nicht existiert, füge sie hinzu
data.LearningPoints[category] = bonus
}
}
// Speichere die Stand-Bonuspunkte auch separat für Referenz
//data.StandPoints = standBonus
}
return data, nil
}
// getStandBonusPoints gibt die Bonus-Lernpunkte basierend auf dem Stand zurück
func getStandBonusPoints(social_class string) map[string]int {
bonusPoints, err := gsmaster.GetSocialClassBonusPoints(social_class)
if err != nil {
logger.Warn("Fehler beim Laden der Stand-Bonuspunkte: %s", err.Error())
return make(map[string]int)
}
// Fallback for missing lookup data in test fixtures
if len(bonusPoints) == 0 {
switch social_class {
case "Unfreie":
return map[string]int{"Halbwelt": 2}
case "Volk":
return map[string]int{"Alltag": 2}
case "Mittelschicht":
return map[string]int{"Wissen": 2}
case "Adel":
return map[string]int{"Sozial": 2}
}
}
return bonusPoints
}
// GetDatasheetOptions returns all available options for datasheet select boxes
func GetDatasheetOptions(c *gin.Context) {
logger.Debug("GetDatasheetOptions aufgerufen")
gameSystemIDStr := c.DefaultQuery("game_system_id", "")
var gameSystemID uint
if gameSystemIDStr != "" {
parsed, err := strconv.ParseUint(gameSystemIDStr, 10, 64)
if err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid game_system_id")
return
}
gameSystemID = uint(parsed)
}
gs := models.GetGameSystem(gameSystemID, "")
characterID := c.Param("id")
// Load character to get their weapon skills
var character models.Char
err := character.FirstID(characterID)
if err != nil {
logger.Error("GetDatasheetOptions: Charakter nicht gefunden - ID: %s, Error: %s", characterID, err.Error())
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Get all available weapons from database
var allWeapons []models.Weapon
if err := database.DB.Find(&allWeapons).Error; err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Waffen: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load weapons")
return
}
// Filter weapons based on character's weapon skills
characterWeaponSkills := make(map[string]bool)
for _, skill := range character.Waffenfertigkeiten {
characterWeaponSkills[skill.Name] = true
}
availableWeapons := []string{}
for _, weapon := range allWeapons {
if characterWeaponSkills[weapon.SkillRequired] {
availableWeapons = append(availableWeapons, weapon.Name)
}
}
// Load misc lookup data from database
genders, err := gsmaster.GetMiscLookupByKeyForSystem("gender", gs.ID)
if err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Geschlechter: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load genders")
return
}
races, err := gsmaster.GetMiscLookupByKeyForSystem("races", gs.ID)
if err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Rassen: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load races")
return
}
origins, err := gsmaster.GetMiscLookupByKeyForSystem("origins", gs.ID)
if err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Herkünfte: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load origins")
return
}
socialClasses, err := gsmaster.GetMiscLookupByKeyForSystem("social_classes", gs.ID)
if err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Stände: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load social classes")
return
}
faiths, err := gsmaster.GetMiscLookupByKeyForSystem("faiths", gs.ID)
if err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Glaubensrichtungen: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load faiths")
return
}
handedness, err := gsmaster.GetMiscLookupByKeyForSystem("handedness", gs.ID)
if err != nil {
logger.Error("GetDatasheetOptions: Fehler beim Laden der Händigkeiten: %s", err.Error())
respondWithError(c, http.StatusInternalServerError, "Failed to load handedness")
return
}
// Convert to string arrays
genderValues := make([]string, len(genders))
for i, g := range genders {
genderValues[i] = g.Value
}
raceValues := make([]string, len(races))
for i, r := range races {
raceValues[i] = r.Value
}
originValues := make([]string, len(origins))
for i, o := range origins {
originValues[i] = o.Value
}
socialClassValues := make([]string, len(socialClasses))
for i, sc := range socialClasses {
socialClassValues[i] = sc.Value
}
faithValues := make([]string, len(faiths))
for i, f := range faiths {
faithValues[i] = f.Value
}
handednessValues := make([]string, len(handedness))
for i, h := range handedness {
handednessValues[i] = h.Value
}
// Return all options
options := gin.H{
"gender": genderValues,
"races": raceValues,
"origins": originValues,
"social_classes": socialClassValues,
"faiths": faithValues,
"handedness": handednessValues,
"specializations": availableWeapons,
}
logger.Debug("GetDatasheetOptions: Erfolgreich geladen - %d verfügbare Waffen", len(availableWeapons))
c.JSON(http.StatusOK, options)
}