WIP Char create get skills
und Docker stuff für Production deployment
This commit is contained in:
+371
-17
@@ -1996,8 +1996,6 @@ func GetRewardTypesOld(c *gin.Context) {
|
||||
|
||||
// GetAvailableSkillsNewSystem gibt alle verfügbaren Fertigkeiten mit Lernkosten zurück (POST mit LernCostRequest)
|
||||
func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
characterID := c.Param("id")
|
||||
|
||||
// Parse LernCostRequest aus POST body
|
||||
var baseRequest gsmaster.LernCostRequest
|
||||
if err := c.ShouldBindJSON(&baseRequest); err != nil {
|
||||
@@ -2005,11 +2003,23 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// For character creation (char_id = 0), we don't need to load an existing character
|
||||
var character models.Char
|
||||
if err := database.DB.Preload("Fertigkeiten").Preload("Erfahrungsschatz").Preload("Vermoegen").First(&character, characterID).Error; err != nil {
|
||||
respondWithError(c, http.StatusNotFound, "Character not found")
|
||||
return
|
||||
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, aber filtere Placeholder aus
|
||||
var allSkills []models.Skill
|
||||
@@ -2025,12 +2035,6 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
}
|
||||
*/
|
||||
|
||||
// Erstelle eine Map der bereits gelernten Fertigkeiten
|
||||
learnedSkills := make(map[string]bool)
|
||||
for _, skill := range character.Fertigkeiten {
|
||||
learnedSkills[skill.Name] = true
|
||||
}
|
||||
|
||||
// Organisiere Fertigkeiten nach Kategorien
|
||||
skillsByCategory := make(map[string][]gin.H)
|
||||
|
||||
@@ -2046,7 +2050,7 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
|
||||
// Erstelle LernCostRequest für diese Fertigkeit basierend auf der Basis-Anfrage
|
||||
request := baseRequest
|
||||
request.CharId = character.ID
|
||||
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
|
||||
@@ -2054,9 +2058,19 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
request.Action = "learn"
|
||||
|
||||
// Erstelle SkillCostResultNew
|
||||
characterClass := ""
|
||||
characterID := "0"
|
||||
|
||||
if baseRequest.CharId != 0 {
|
||||
// Use existing character data
|
||||
characterID = fmt.Sprintf("%d", character.ID)
|
||||
characterClass = getCharacterClassOld(&character)
|
||||
}
|
||||
// For character creation, we don't have a character class yet, use empty string
|
||||
|
||||
levelResult := gsmaster.SkillCostResultNew{
|
||||
CharacterID: fmt.Sprintf("%d", character.ID),
|
||||
CharacterClass: getCharacterClassOld(&character),
|
||||
CharacterID: characterID,
|
||||
CharacterClass: characterClass,
|
||||
SkillName: skill.Name,
|
||||
TargetLevel: 1,
|
||||
}
|
||||
@@ -2065,7 +2079,7 @@ func GetAvailableSkillsNewSystem(c *gin.Context) {
|
||||
remainingGold := request.UseGold
|
||||
|
||||
// Hole die vollständigen Skill-Informationen für die Kostenberechnung
|
||||
skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skill.Name, getCharacterClassOld(&character))
|
||||
skillLearningInfo, err := models.GetSkillCategoryAndDifficultyNewSystem(skill.Name, characterClass)
|
||||
if err != nil {
|
||||
// Fallback für unbekannte Skills
|
||||
skillLearningInfo = &models.SkillLearningInfo{
|
||||
@@ -3164,11 +3178,351 @@ func GetSkillCategoriesWithPoints(c *gin.Context) {
|
||||
{Name: "wissen", DisplayName: "Wissen", Points: 100, MaxPoints: 100},
|
||||
{Name: "kampf", DisplayName: "Kampf", Points: 80, MaxPoints: 80},
|
||||
{Name: "korper", DisplayName: "Körper", Points: 120, MaxPoints: 120},
|
||||
{Name: "gesellschaft", DisplayName: "Gesellschaft", Points: 60, MaxPoints: 60},
|
||||
{Name: "natur", DisplayName: "Natur", Points: 90, MaxPoints: 90},
|
||||
{Name: "unterwelt", DisplayName: "Unterwelt", Points: 40, MaxPoints: 40},
|
||||
{Name: "zauber", DisplayName: "Zauber", Points: 200, MaxPoints: 200},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"categories": categories})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Mapping der Klassennamen zu Codes (falls notwendig)
|
||||
classMapping := map[string]string{
|
||||
"Assassine": "As",
|
||||
"Barbar": "Bb",
|
||||
"Glücksritter": "Gl",
|
||||
"Händler": "Hä",
|
||||
"Krieger": "Kr",
|
||||
"Spitzbube": "Sp",
|
||||
"Waldläufer": "Wa",
|
||||
"Barde": "Ba",
|
||||
"Ordenskrieger": "Or",
|
||||
"Druide": "Dr",
|
||||
"Hexer": "Hx",
|
||||
"Magier": "Ma",
|
||||
"Priester Beschützer": "PB",
|
||||
"Priester Streiter": "PS",
|
||||
"Schamane": "Sc",
|
||||
}
|
||||
|
||||
classCode := classMapping[className]
|
||||
if classCode == "" {
|
||||
classCode = className // Falls der Name bereits ein Code ist
|
||||
}
|
||||
|
||||
// Definiere die Lernpunkte-Daten basierend auf Lerntabelle_Erstellung.md
|
||||
var data *LearningPointsData
|
||||
|
||||
switch classCode {
|
||||
case "As", "Assassine":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Assassine",
|
||||
ClassCode: "As",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 1,
|
||||
"Halbwelt": 2,
|
||||
"Sozial": 4,
|
||||
"Unterwelt": 8,
|
||||
},
|
||||
WeaponPoints: 24,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Meucheln", Bonus: 8, Attribute: "Gs", Notes: ""},
|
||||
},
|
||||
}
|
||||
case "Bb", "Barbar":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Barbar",
|
||||
ClassCode: "Bb",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Freiland": 4,
|
||||
"Kampf": 1,
|
||||
"Körper": 2,
|
||||
},
|
||||
WeaponPoints: 24,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Spurensuche", Bonus: 8, Attribute: "In", Notes: "in Heimatlandschaft"},
|
||||
{Name: "Überleben", Bonus: 8, Attribute: "In", Notes: "in Heimatlandschaft"},
|
||||
},
|
||||
}
|
||||
case "Gl", "Glücksritter":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Glücksritter",
|
||||
ClassCode: "Gl",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Halbwelt": 3,
|
||||
"Sozial": 8,
|
||||
},
|
||||
WeaponPoints: 24,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Fechten", Bonus: 5, Attribute: "Gs", Notes: "oder beidhändiger Kampf+5 (Gs)"},
|
||||
},
|
||||
}
|
||||
case "Hä", "Händler":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Händler",
|
||||
ClassCode: "Hä",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 4,
|
||||
"Sozial": 8,
|
||||
"Wissen": 4,
|
||||
},
|
||||
WeaponPoints: 20,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Geschäftssinn", Bonus: 8, Attribute: "In", Notes: ""},
|
||||
},
|
||||
}
|
||||
case "Kr", "Krieger":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Krieger",
|
||||
ClassCode: "Kr",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Kampf": 3,
|
||||
"Körper": 1,
|
||||
},
|
||||
WeaponPoints: 36,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Kampf in Vollrüstung", Bonus: 5, Attribute: "St", Notes: ""},
|
||||
},
|
||||
}
|
||||
case "Sp", "Spitzbube":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Spitzbube",
|
||||
ClassCode: "Sp",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Halbwelt": 6,
|
||||
"Unterwelt": 12,
|
||||
},
|
||||
WeaponPoints: 20,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Fallenmechanik", Bonus: 8, Attribute: "Gs", Notes: "oder Geschäftssinn+8 (In)"},
|
||||
},
|
||||
}
|
||||
case "Wa", "Waldläufer":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Waldläufer",
|
||||
ClassCode: "Wa",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 1,
|
||||
"Freiland": 11,
|
||||
"Körper": 4,
|
||||
},
|
||||
WeaponPoints: 20,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Scharfschießen", Bonus: 5, Attribute: "Gs", Notes: ""},
|
||||
},
|
||||
}
|
||||
case "Ba", "Barde":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Barde",
|
||||
ClassCode: "Ba",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Sozial": 4,
|
||||
"Wissen": 4,
|
||||
},
|
||||
WeaponPoints: 16,
|
||||
SpellPoints: 3,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Musizieren", Bonus: 12, Attribute: "Gs", Notes: ""},
|
||||
{Name: "Landeskunde", Bonus: 8, Attribute: "In", Notes: "für Heimat"},
|
||||
},
|
||||
TypicalSpells: []string{"Zauberlieder"},
|
||||
}
|
||||
case "Or", "Ordenskrieger":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Ordenskrieger",
|
||||
ClassCode: "Or",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Kampf": 3,
|
||||
"Wissen": 2,
|
||||
},
|
||||
WeaponPoints: 18,
|
||||
SpellPoints: 3,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Athletik", Bonus: 8, Attribute: "St", Notes: "oder Meditieren+8 (Wk)"},
|
||||
},
|
||||
TypicalSpells: []string{"Wundertaten"},
|
||||
}
|
||||
case "Dr", "Druide":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Druide",
|
||||
ClassCode: "Dr",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Freiland": 4,
|
||||
"Wissen": 2,
|
||||
},
|
||||
WeaponPoints: 6,
|
||||
SpellPoints: 5,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Pflanzenkunde", Bonus: 8, Attribute: "In", Notes: ""},
|
||||
{Name: "Schreiben", Bonus: 12, Attribute: "In", Notes: "für Ogam-Zeichen"},
|
||||
},
|
||||
TypicalSpells: []string{"Dweomer", "Tiere rufen"},
|
||||
}
|
||||
case "Hx", "Hexer":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Hexer",
|
||||
ClassCode: "Hx",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 3,
|
||||
"Sozial": 2,
|
||||
"Wissen": 2,
|
||||
},
|
||||
WeaponPoints: 2,
|
||||
SpellPoints: 6,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Gassenwissen", Bonus: 8, Attribute: "In", Notes: "oder Verführen+8 (pA)"},
|
||||
},
|
||||
TypicalSpells: []string{"Beherrschen", "Verändern", "Verwünschen", "Binden des Vertrauten"},
|
||||
}
|
||||
case "Ma", "Magier":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Magier",
|
||||
ClassCode: "Ma",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 1,
|
||||
"Wissen": 5,
|
||||
},
|
||||
WeaponPoints: 2,
|
||||
SpellPoints: 7,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Zauberkunde", Bonus: 8, Attribute: "In", Notes: ""},
|
||||
{Name: "Schreiben", Bonus: 12, Attribute: "In", Notes: "für Muttersprache"},
|
||||
},
|
||||
TypicalSpells: []string{"beliebig außer Dweomer, Wundertaten, Zauberlieder", "Erkennen von Zauberei"},
|
||||
}
|
||||
case "PB", "Priester Beschützer":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Priester Beschützer",
|
||||
ClassCode: "PB",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Sozial": 2,
|
||||
"Wissen": 3,
|
||||
},
|
||||
WeaponPoints: 6,
|
||||
SpellPoints: 5,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Menschenkenntnis", Bonus: 8, Attribute: "In", Notes: ""},
|
||||
{Name: "Schreiben", Bonus: 12, Attribute: "In", Notes: "für Muttersprache"},
|
||||
},
|
||||
TypicalSpells: []string{"Wundertaten", "Heilen von Wunden"},
|
||||
}
|
||||
case "PS", "Priester Streiter":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Priester Streiter",
|
||||
ClassCode: "PS",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 3,
|
||||
"Kampf": 2,
|
||||
"Wissen": 2,
|
||||
},
|
||||
WeaponPoints: 8,
|
||||
SpellPoints: 5,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Erste Hilfe", Bonus: 8, Attribute: "Gs", Notes: ""},
|
||||
{Name: "Schreiben", Bonus: 12, Attribute: "In", Notes: "für Muttersprache"},
|
||||
},
|
||||
TypicalSpells: []string{"Wundertaten", "Bannen von Finsterwerk", "Strahlender Panzer"},
|
||||
}
|
||||
case "Sc", "Schamane":
|
||||
data = &LearningPointsData{
|
||||
ClassName: "Schamane",
|
||||
ClassCode: "Sc",
|
||||
LearningPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Körper": 4,
|
||||
"Wissen": 2,
|
||||
},
|
||||
WeaponPoints: 6,
|
||||
SpellPoints: 5,
|
||||
TypicalSkills: []TypicalSkill{
|
||||
{Name: "Tierkunde", Bonus: 8, Attribute: "In", Notes: ""},
|
||||
{Name: "Überleben", Bonus: 8, Attribute: "In", Notes: "in Heimatlandschaft"},
|
||||
},
|
||||
TypicalSpells: []string{"Dweomer", "Wundertaten", "Austreibung des Bösen", "Bannen von Gift"},
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unbekannte Charakterklasse: %s", className)
|
||||
}
|
||||
|
||||
// Bonus-Lernpunkte basierend auf Stand hinzufügen
|
||||
if stand != "" && data != nil {
|
||||
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(stand string) map[string]int {
|
||||
switch stand {
|
||||
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}
|
||||
default:
|
||||
return make(map[string]int)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
package character
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetCharacterClassLearningPoints(t *testing.T) {
|
||||
// Setup Gin in test mode
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/api/characters/classes/learning-points", GetCharacterClassLearningPoints)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
classParam string
|
||||
standParam string
|
||||
expectedStatus int
|
||||
expectError bool
|
||||
expectedClass string
|
||||
checkWeapons bool
|
||||
expectedWeapons int
|
||||
checkSpells bool
|
||||
expectedSpells int
|
||||
}{
|
||||
{
|
||||
name: "Valid Hexer class without stand",
|
||||
classParam: "Hexer",
|
||||
standParam: "",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Hexer",
|
||||
checkWeapons: true,
|
||||
expectedWeapons: 2,
|
||||
checkSpells: true,
|
||||
expectedSpells: 6,
|
||||
},
|
||||
{
|
||||
name: "Valid Hexer class with Volk stand",
|
||||
classParam: "Hexer",
|
||||
standParam: "Volk",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Hexer",
|
||||
checkWeapons: true,
|
||||
expectedWeapons: 2,
|
||||
checkSpells: true,
|
||||
expectedSpells: 6,
|
||||
},
|
||||
{
|
||||
name: "Valid Krieger class with Adel stand",
|
||||
classParam: "Krieger",
|
||||
standParam: "Adel",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Krieger",
|
||||
checkWeapons: true,
|
||||
expectedWeapons: 36,
|
||||
checkSpells: false,
|
||||
},
|
||||
{
|
||||
name: "Valid Magier class",
|
||||
classParam: "Magier",
|
||||
standParam: "",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Magier",
|
||||
checkWeapons: true,
|
||||
expectedWeapons: 2,
|
||||
checkSpells: true,
|
||||
expectedSpells: 7,
|
||||
},
|
||||
{
|
||||
name: "Valid Spitzbube class",
|
||||
classParam: "Spitzbube",
|
||||
standParam: "",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Spitzbube",
|
||||
checkWeapons: true,
|
||||
expectedWeapons: 20,
|
||||
checkSpells: false,
|
||||
},
|
||||
{
|
||||
name: "Valid Waldläufer class",
|
||||
classParam: "Waldläufer",
|
||||
standParam: "",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Waldläufer",
|
||||
checkWeapons: true,
|
||||
expectedWeapons: 20,
|
||||
checkSpells: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid class should return error",
|
||||
classParam: "InvalidClass",
|
||||
standParam: "",
|
||||
expectedStatus: http.StatusNotFound,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Missing class parameter should return error",
|
||||
classParam: "",
|
||||
standParam: "",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "Valid class with invalid stand should still work",
|
||||
classParam: "Hexer",
|
||||
standParam: "InvalidStand",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectError: false,
|
||||
expectedClass: "Hexer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Build request URL
|
||||
url := "/api/characters/classes/learning-points"
|
||||
if tt.classParam != "" || tt.standParam != "" {
|
||||
url += "?"
|
||||
if tt.classParam != "" {
|
||||
url += "class=" + tt.classParam
|
||||
}
|
||||
if tt.standParam != "" {
|
||||
if tt.classParam != "" {
|
||||
url += "&"
|
||||
}
|
||||
url += "stand=" + tt.standParam
|
||||
}
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Create response recorder
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
// Perform request
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// Check status code
|
||||
assert.Equal(t, tt.expectedStatus, w.Code)
|
||||
|
||||
if tt.expectError {
|
||||
// For error cases, check that we have an error message
|
||||
var response map[string]interface{}
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "error")
|
||||
} else {
|
||||
// For success cases, check the response structure
|
||||
var response LearningPointsData
|
||||
err := json.Unmarshal(w.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Check basic fields
|
||||
assert.Equal(t, tt.expectedClass, response.ClassName)
|
||||
assert.NotEmpty(t, response.ClassCode)
|
||||
assert.NotNil(t, response.LearningPoints)
|
||||
assert.NotNil(t, response.TypicalSkills)
|
||||
|
||||
// Check weapon points if specified
|
||||
if tt.checkWeapons {
|
||||
assert.Equal(t, tt.expectedWeapons, response.WeaponPoints)
|
||||
}
|
||||
|
||||
// Check spell points if specified
|
||||
if tt.checkSpells {
|
||||
assert.Equal(t, tt.expectedSpells, response.SpellPoints)
|
||||
}
|
||||
|
||||
// Check that learning points are not empty
|
||||
assert.NotEmpty(t, response.LearningPoints)
|
||||
|
||||
// Check that typical skills are not empty
|
||||
assert.NotEmpty(t, response.TypicalSkills)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLearningPointsForClass(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
className string
|
||||
stand string
|
||||
expectError bool
|
||||
expectedClass string
|
||||
expectedCode string
|
||||
checkPoints map[string]int
|
||||
checkStand map[string]int
|
||||
}{
|
||||
{
|
||||
name: "Hexer class data",
|
||||
className: "Hexer",
|
||||
stand: "",
|
||||
expectError: false,
|
||||
expectedClass: "Hexer",
|
||||
expectedCode: "Hx",
|
||||
checkPoints: map[string]int{
|
||||
"Alltag": 3,
|
||||
"Sozial": 2,
|
||||
"Wissen": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Hexer with Volk stand",
|
||||
className: "Hexer",
|
||||
stand: "Volk",
|
||||
expectError: false,
|
||||
expectedClass: "Hexer",
|
||||
expectedCode: "Hx",
|
||||
checkPoints: map[string]int{
|
||||
"Alltag": 5, // Base 3 + Volk bonus 2 = 5
|
||||
"Sozial": 2,
|
||||
"Wissen": 2,
|
||||
},
|
||||
checkStand: map[string]int{
|
||||
"Alltag": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Krieger class data",
|
||||
className: "Krieger",
|
||||
stand: "",
|
||||
expectError: false,
|
||||
expectedClass: "Krieger",
|
||||
expectedCode: "Kr",
|
||||
checkPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Kampf": 3,
|
||||
"Körper": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Krieger with Adel stand",
|
||||
className: "Krieger",
|
||||
stand: "Adel",
|
||||
expectError: false,
|
||||
expectedClass: "Krieger",
|
||||
expectedCode: "Kr",
|
||||
checkPoints: map[string]int{
|
||||
"Alltag": 2,
|
||||
"Kampf": 3,
|
||||
"Körper": 1,
|
||||
"Sozial": 2, // Stand bonus adds this new category
|
||||
},
|
||||
checkStand: map[string]int{
|
||||
"Sozial": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid class should return error",
|
||||
className: "InvalidClass",
|
||||
stand: "",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data, err := getLearningPointsForClass(tt.className, tt.stand)
|
||||
|
||||
if tt.expectError {
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, data)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, data)
|
||||
|
||||
// Check basic properties
|
||||
assert.Equal(t, tt.expectedClass, data.ClassName)
|
||||
assert.Equal(t, tt.expectedCode, data.ClassCode)
|
||||
|
||||
// Check learning points
|
||||
if tt.checkPoints != nil {
|
||||
for category, expectedPoints := range tt.checkPoints {
|
||||
actualPoints, exists := data.LearningPoints[category]
|
||||
assert.True(t, exists, "Category %s should exist", category)
|
||||
assert.Equal(t, expectedPoints, actualPoints, "Points for category %s", category)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we have some typical skills
|
||||
assert.NotEmpty(t, data.TypicalSkills)
|
||||
|
||||
// Validate typical skills structure
|
||||
for _, skill := range data.TypicalSkills {
|
||||
assert.NotEmpty(t, skill.Name)
|
||||
assert.NotEmpty(t, skill.Attribute)
|
||||
assert.GreaterOrEqual(t, skill.Bonus, 0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStandBonusPoints(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stand string
|
||||
expected map[string]int
|
||||
}{
|
||||
{
|
||||
name: "Unfreie stand",
|
||||
stand: "Unfreie",
|
||||
expected: map[string]int{"Halbwelt": 2},
|
||||
},
|
||||
{
|
||||
name: "Volk stand",
|
||||
stand: "Volk",
|
||||
expected: map[string]int{"Alltag": 2},
|
||||
},
|
||||
{
|
||||
name: "Mittelschicht stand",
|
||||
stand: "Mittelschicht",
|
||||
expected: map[string]int{"Wissen": 2},
|
||||
},
|
||||
{
|
||||
name: "Adel stand",
|
||||
stand: "Adel",
|
||||
expected: map[string]int{"Sozial": 2},
|
||||
},
|
||||
{
|
||||
name: "Invalid stand",
|
||||
stand: "Invalid",
|
||||
expected: map[string]int{},
|
||||
},
|
||||
{
|
||||
name: "Empty stand",
|
||||
stand: "",
|
||||
expected: map[string]int{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := getStandBonusPoints(tt.stand)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test all character classes to ensure they're properly defined
|
||||
func TestAllCharacterClassesAreDefined(t *testing.T) {
|
||||
expectedClasses := []string{
|
||||
"Assassine", "Barbar", "Glücksritter", "Händler", "Krieger", "Spitzbube", "Waldläufer",
|
||||
"Barde", "Ordenskrieger", "Druide", "Hexer", "Magier", "Priester Beschützer", "Priester Streiter", "Schamane",
|
||||
}
|
||||
|
||||
for _, className := range expectedClasses {
|
||||
t.Run("Class_"+className, func(t *testing.T) {
|
||||
data, err := getLearningPointsForClass(className, "")
|
||||
assert.NoError(t, err, "Class %s should be defined", className)
|
||||
assert.NotNil(t, data, "Class %s should return data", className)
|
||||
assert.Equal(t, className, data.ClassName)
|
||||
assert.NotEmpty(t, data.ClassCode)
|
||||
assert.NotEmpty(t, data.LearningPoints)
|
||||
assert.GreaterOrEqual(t, data.WeaponPoints, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
||||
// Reference Data für Character Creation
|
||||
charGrp.GET("/races", GetRaces) // Verfügbare Rassen
|
||||
charGrp.GET("/classes", GetCharacterClasses) // Verfügbare Klassen
|
||||
charGrp.GET("/classes/learning-points", GetCharacterClassLearningPoints) // Lernpunkte für Charakterklasse
|
||||
charGrp.GET("/origins", GetOrigins) // Verfügbare Herkünfte
|
||||
charGrp.GET("/beliefs", SearchBeliefs) // Glaube-Suche
|
||||
charGrp.GET("/skill-categories-with-points", GetSkillCategoriesWithPoints) // Kategorien mit Lernpunkten
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# Lerneinheiten bei Spielbeginn
|
||||
Kämpfer
|
||||
|
||||
Assassine (As)
|
||||
Typische Fertigkeit: Meucheln+8 (Gs)
|
||||
Alltag: 1 LE, Halbwelt: 2 LE, Sozial: 4 LE, Unterwelt:
|
||||
8 LE
|
||||
Waffen: 24 LE
|
||||
|
||||
Barbar (Bb)
|
||||
Typische Fertigkeit: Spurensuche+8 (In) und Überleben+8 (In) in Heimatlandschaft*
|
||||
Alltag: 2 LE, Freiland: 4 LE, Kampf: 1 LE, Körper: 2 LE
|
||||
Waffen: 24 LE
|
||||
|
||||
Glücksritter (Gl)
|
||||
Typische Fertigkeit: Fechten+5 (Gs) oder beidhändiger
|
||||
Kampf+5 (Gs)
|
||||
Alltag: 2 LE, Halbwelt: 3 LE, Sozial: 8 LE
|
||||
Waffen: 24 LE
|
||||
|
||||
Händler (Hä)
|
||||
Typische Fertigkeit: Geschäftssinn+8 (In)
|
||||
Alltag: 4 LE, Sozial: 8 LE, Wissen: 4 LE
|
||||
Waffen: 20 LE
|
||||
|
||||
Krieger (Kr)
|
||||
Typische Fertigkeit: Kampf in Vollrüstung+5 (St)
|
||||
Alltag: 2 LE, Kampf: 3 LE, Körper: 1 LE
|
||||
Waffen: 36 LE
|
||||
|
||||
Spitzbube (Sp)
|
||||
Typische Fertigkeit: Fallenmechanik+8 (Gs) oder Geschäftssinn+8 (In)
|
||||
Alltag: 2 LE, Halbwelt: 6 LE, Unterwelt: 12 LE
|
||||
Waffen: 20 LE
|
||||
|
||||
Waldläufer (Wa)
|
||||
Typische Fertigkeit: Scharfschießen+5 (Gs)
|
||||
Alltag: 1 LE, Freiland: 11 LE, Körper: 4 LE
|
||||
Waffen: 20 LE
|
||||
|
||||
Zauberkundige Kämpfer
|
||||
|
||||
Barde (Ba)
|
||||
Typische Fertigkeit: Musizieren+12 (Gs) und Landeskunde+8 (In) für Heimat
|
||||
Alltag: 2 LE, Sozial: 4 LE, Wissen: 4 LE
|
||||
Waffen: 16 LE
|
||||
Zauber: 3 LE (Zauberlieder)
|
||||
|
||||
Ordenskrieger (Or)
|
||||
Typische Fertigkeit: Athletik+8 (St) oder Meditieren+8
|
||||
(Wk)
|
||||
Alltag: 2 LE, Kampf: 3 LE, Wissen: 2 LE
|
||||
Waffen: 18 LE
|
||||
Zauber: 3 LE (Wundertaten)
|
||||
|
||||
Zauberer
|
||||
|
||||
Druide (Dr)
|
||||
Typische Fertigkeit: Pflanzenkunde+8 (In) und Schreiben+12 (In) für Ogam-Zeichen
|
||||
Alltag: 2 LE, Freiland: 4 LE, Wissen: 2 LE
|
||||
Waffen: 6 LE
|
||||
Zauber: 5 LE (Dweomer)
|
||||
Typischer Zauber: Tiere rufen
|
||||
|
||||
Hexer (Hx)
|
||||
Typische Fertigkeit: Gassenwissen+8 (In) oder Verführen+8 (pA)
|
||||
Alltag: 3 LE, Sozial: 2 LE, Wissen: 2 LE
|
||||
Waffen: 2 LE
|
||||
Zauber: 6 LE (Beherrschen, Verändern)
|
||||
Typischer Zauber: Verwünschen oder Binden des Vertauten
|
||||
|
||||
Magier (Ma)
|
||||
Typische Fertigkeit: Zauberkunde+8 (In) und Schreiben+12 (In) für Muttersprache**
|
||||
Alltag: 1 LE, Wissen: 5 LE
|
||||
Waffen: 2 LE
|
||||
Zauber: 7 LE (beliebig außer Dweomer, Wundertaten,
|
||||
Zauberlieder)
|
||||
Typischer Zauber: Erkennen von Zauberei
|
||||
|
||||
Priester, Beschützer (PB)
|
||||
Typische Fertigkeit: Menschenkenntnis+8 (In) und
|
||||
Schrei ben+12 (In) für Muttersprache**
|
||||
Alltag: 2 LE, Sozial: 2 LE, Wissen: 3 LE
|
||||
Waffen: 6 LE
|
||||
Zauber: 5 LE (Wundertaten)
|
||||
Typischer Zauber: Heilen von Wunden
|
||||
|
||||
Priester, Streiter (PS)
|
||||
Typische Fertigkeit: Erste Hilfe+8 (Gs) und Schreiben+12 (In) für Muttersprache**
|
||||
Alltag: 3 LE, Kampf: 2 LE, Wissen: 2 LE
|
||||
Waffen: 8 LE
|
||||
Zauber: 5 LE (Wundertaten)
|
||||
Typischer Zauber: Bannen von Finsterwerk oder Strahlender Panzer
|
||||
|
||||
Schamane (Sc)
|
||||
Typische Fertigkeit: Tierkunde+8 (In) und Überleben+8 (In) in Heimatlandschaft*
|
||||
Alltag: 2 LE, Körper: 4 LE, Wissen: 2 LE
|
||||
Waffen: 6 LE
|
||||
Zauber: 5 LE (Dweomer, Wundertaten)
|
||||
Typischer Zauber: Austreibung des Bösen oder Bannen von Gift
|
||||
|
||||
*: Gebirge, Steppe oder Wald - je nach Lebensraum seines Stammes
|
||||
**: abhängig von der Heimat (s. S. 127 für eine Liste der Sprachen)
|
||||
|
||||
Standesfertigkeiten
|
||||
|
||||
Unfreie: 2 LE für Halbwelt
|
||||
Volk: 2 LE für Alltag
|
||||
Mittelschicht: 2 LE für Wissen
|
||||
Adel: 2 LE für Sozial, leicht
|
||||
@@ -1,18 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bamort/config"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Teste, ob die globale Konfigurationsvariable funktioniert
|
||||
fmt.Printf("Globale Konfiguration:\n")
|
||||
fmt.Printf("Environment: %s\n", config.Cfg.Environment)
|
||||
fmt.Printf("DatabaseType: %s\n", config.Cfg.DatabaseType)
|
||||
fmt.Printf("DatabaseURL: %s\n", config.Cfg.DatabaseURL)
|
||||
fmt.Printf("ServerPort: %s\n", config.Cfg.ServerPort)
|
||||
fmt.Printf("DebugMode: %v\n", config.Cfg.DebugMode)
|
||||
fmt.Printf("LogLevel: %s\n", config.Cfg.LogLevel)
|
||||
fmt.Printf("Testing: %s\n", config.Cfg.DevTesting)
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
# =========== 1) Build stage ===========
|
||||
FROM golang:1.23-alpine AS builder
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Create and set working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# =========== 1) Build stage ===========
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
# Install necessary packages for CGO and SQLite
|
||||
RUN apk add --no-cache gcc musl-dev sqlite-dev
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go.mod and go.sum first
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy the rest of the backend code
|
||||
COPY . .
|
||||
|
||||
# Build the Go binary
|
||||
RUN go build -v -o server cmd/main.go
|
||||
|
||||
# =========== 2) Runtime stage ===========
|
||||
FROM alpine:3.18
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the compiled binary from builder stage
|
||||
COPY --from=builder /app/server /app
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8180
|
||||
|
||||
# Run the Go server
|
||||
CMD ["./server"]
|
||||
@@ -1,5 +1,4 @@
|
||||
version: "3.8"
|
||||
|
||||
services:
|
||||
backend-dev:
|
||||
build:
|
||||
|
||||
+29
-22
@@ -1,26 +1,5 @@
|
||||
version: "3.8"
|
||||
services:
|
||||
mariadb:
|
||||
image: mariadb:11.4
|
||||
container_name: bamort-mariadb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-secure_root_password}
|
||||
MARIADB_DATABASE: bamort
|
||||
MARIADB_USER: bamort
|
||||
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-secure_user_password}
|
||||
MARIADB_CHARSET: utf8mb4
|
||||
MARIADB_COLLATION: utf8mb4_unicode_ci
|
||||
#ports:
|
||||
# - "3306:3306"
|
||||
volumes:
|
||||
- ./bamort-db:/var/lib/mysql
|
||||
- ./init-db:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
backend:
|
||||
build:
|
||||
@@ -35,6 +14,7 @@ services:
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
|
||||
frontend:
|
||||
build:
|
||||
@@ -44,10 +24,37 @@ services:
|
||||
ports:
|
||||
- "5173:80"
|
||||
environment:
|
||||
- VITE_API_URL=${VITE_API_URL:-http://192.168.0.48:8180}
|
||||
- VITE_API_URL=${VITE_API_URL:-http://bamort.trokan.de:8180}
|
||||
- BASE_URL=http://bamort.trokan.de:8180
|
||||
- PORT=8180
|
||||
- ENVIRONMENT=production
|
||||
- DATABASE_TYPE=mysql
|
||||
- DATABASE_URL=bamort:bG4)efozrc@tcp(mariadb:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local
|
||||
depends_on:
|
||||
- backend
|
||||
|
||||
mariadb:
|
||||
image: mariadb:11.4
|
||||
container_name: bamort-mariadb
|
||||
restart: unless-stopped
|
||||
#ports:
|
||||
# - "3306:3306"
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD:-secure_root_password}
|
||||
MARIADB_DATABASE: bamort
|
||||
MARIADB_USER: bamort
|
||||
MARIADB_PASSWORD: ${MARIADB_PASSWORD:-secure_user_password}
|
||||
MARIADB_CHARSET: utf8mb4
|
||||
MARIADB_COLLATION: utf8mb4_unicode_ci
|
||||
volumes:
|
||||
- ./bamort-db:/var/lib/mysql
|
||||
- ./init-db:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
start_period: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# phpMyAdmin - Database Management (commented out for production)
|
||||
# Uncomment the following section if you need database management in production
|
||||
# phpmyadmin:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -162,7 +162,7 @@ export default {
|
||||
common: {
|
||||
loading: 'Laden...',
|
||||
cancel: 'Abbrechen',
|
||||
revious: 'Zurück',
|
||||
previous: 'Zurück',
|
||||
next: 'Weiter'
|
||||
},
|
||||
experience: {
|
||||
|
||||
@@ -17,6 +17,6 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
//port: 8080,
|
||||
host: ['192.168.0.48', 'localhost','terrra.local'],
|
||||
host: ['bamort.trokan.de','192.168.0.48', 'localhost','terrra.local'],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user