Charakter erstellung in 5 schritten
spätere Fortsetzung soll bis zu 14 Tage lang möglich sein. Rückwärts navigation durch die einzelenen Schritte sollen möglich sein
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"bamort/models"
|
"bamort/models"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -2352,3 +2353,359 @@ func calculateSkillLearningCostsOld(skill models.Skill, character models.Char, r
|
|||||||
|
|
||||||
return costResult.EP, costResult.GoldCost
|
return costResult.EP, costResult.GoldCost
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Character Creation Session Management
|
||||||
|
|
||||||
|
// CharacterCreationSession repräsentiert eine Charakter-Erstellungssession
|
||||||
|
type CharacterCreationSession struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"index"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Rasse string `json:"rasse"`
|
||||||
|
Typ string `json:"typ"`
|
||||||
|
Herkunft string `json:"herkunft"`
|
||||||
|
Glaube string `json:"glaube"`
|
||||||
|
Attributes map[string]int `json:"attributes" gorm:"type:json"`
|
||||||
|
DerivedValues map[string]int `json:"derived_values" gorm:"type:json"`
|
||||||
|
Skills []CharacterCreationSkill `json:"skills" gorm:"type:json"`
|
||||||
|
Spells []CharacterCreationSpell `json:"spells" gorm:"type:json"`
|
||||||
|
SkillPoints map[string]int `json:"skill_points" gorm:"type:json"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
CurrentStep int `json:"current_step"` // 1=Basic, 2=Attributes, 3=Derived, 4=Skills
|
||||||
|
}
|
||||||
|
|
||||||
|
type CharacterCreationSkill struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Cost int `json:"cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CharacterCreationSpell struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Cost int `json:"cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCharacterSession erstellt eine neue Charakter-Erstellungssession
|
||||||
|
func CreateCharacterSession(c *gin.Context) {
|
||||||
|
// TODO: UserID aus Authentication Context holen
|
||||||
|
userID := uint(1) // Placeholder
|
||||||
|
|
||||||
|
sessionID := fmt.Sprintf("char_create_%d_%d", userID, time.Now().Unix())
|
||||||
|
|
||||||
|
session := CharacterCreationSession{
|
||||||
|
ID: sessionID,
|
||||||
|
UserID: userID,
|
||||||
|
Attributes: make(map[string]int),
|
||||||
|
DerivedValues: make(map[string]int),
|
||||||
|
Skills: []CharacterCreationSkill{},
|
||||||
|
Spells: []CharacterCreationSpell{},
|
||||||
|
SkillPoints: make(map[string]int),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
ExpiresAt: time.Now().AddDate(0, 0, 14), // 14 Tage
|
||||||
|
CurrentStep: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session in Datenbank speichern (Dummy - würde in Session-Tabelle gespeichert)
|
||||||
|
// TODO: Implementiere Session-Speicherung in Redis oder Datenbank
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// TODO: UserID aus Authentication Context holen
|
||||||
|
// userID := uint(1) // Placeholder - würde für Datenbank-Abfrage verwendet
|
||||||
|
|
||||||
|
// TODO: Sessions aus Datenbank/Redis laden
|
||||||
|
// Dummy-Implementierung mit Sample-Sessions
|
||||||
|
sessions := []gin.H{
|
||||||
|
{
|
||||||
|
"session_id": "char_create_1_1722888000",
|
||||||
|
"name": "Mein Magier",
|
||||||
|
"rasse": "Elf",
|
||||||
|
"typ": "Zauberer",
|
||||||
|
"current_step": 3,
|
||||||
|
"total_steps": 5,
|
||||||
|
"created_at": time.Now().AddDate(0, 0, -2),
|
||||||
|
"updated_at": time.Now().AddDate(0, 0, -1),
|
||||||
|
"expires_at": time.Now().AddDate(0, 0, 12),
|
||||||
|
"progress_text": "Abgeleitete Werte",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"session_id": "char_create_1_1722974400",
|
||||||
|
"name": "Kämpfer Draft",
|
||||||
|
"rasse": "Mensch",
|
||||||
|
"typ": "Krieger",
|
||||||
|
"current_step": 1,
|
||||||
|
"total_steps": 5,
|
||||||
|
"created_at": time.Now().AddDate(0, 0, -1),
|
||||||
|
"updated_at": time.Now().AddDate(0, 0, -1),
|
||||||
|
"expires_at": time.Now().AddDate(0, 0, 13),
|
||||||
|
"progress_text": "Grundinformationen",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"sessions": sessions,
|
||||||
|
"count": len(sessions),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCharacterSession gibt Session-Daten zurück
|
||||||
|
func GetCharacterSession(c *gin.Context) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
// TODO: Session aus Datenbank/Redis laden
|
||||||
|
// Dummy-Response
|
||||||
|
session := CharacterCreationSession{
|
||||||
|
ID: sessionID,
|
||||||
|
UserID: 1,
|
||||||
|
CurrentStep: 1,
|
||||||
|
ExpiresAt: time.Now().AddDate(0, 0, 14),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCharacterBasicInfo Request
|
||||||
|
type UpdateBasicInfoRequest struct {
|
||||||
|
Name string `json:"name" binding:"required,min=2,max=50"`
|
||||||
|
Rasse string `json:"rasse" binding:"required"`
|
||||||
|
Typ string `json:"typ" binding:"required"`
|
||||||
|
Herkunft string `json:"herkunft" binding:"required"`
|
||||||
|
Glaube string `json:"glaube"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCharacterBasicInfo speichert Grundinformationen
|
||||||
|
func UpdateCharacterBasicInfo(c *gin.Context) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
var request UpdateBasicInfoRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Ungültige Eingabedaten: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Session aus Datenbank laden und aktualisieren
|
||||||
|
// Dummy-Response
|
||||||
|
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
|
||||||
|
PA int `json:"pa" binding:"required,min=1,max=100"` // Psi-Kraft
|
||||||
|
WK int `json:"wk" binding:"required,min=1,max=100"` // Willenskraft
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCharacterAttributes speichert Grundwerte
|
||||||
|
func UpdateCharacterAttributes(c *gin.Context) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
var request UpdateAttributesRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Ungültige Attributswerte: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Session aktualisieren
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Grundwerte gespeichert",
|
||||||
|
"session_id": sessionID,
|
||||||
|
"current_step": 3,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDerivedValuesRequest
|
||||||
|
type UpdateDerivedValuesRequest struct {
|
||||||
|
LP_Max int `json:"lp_max"` // Lebenspunkte Maximum
|
||||||
|
AP_Max int `json:"ap_max"` // Abenteuerpunkte Maximum
|
||||||
|
B_Max int `json:"b_max"` // Belastung Maximum
|
||||||
|
SG int `json:"sg"` // Schicksalsgunst
|
||||||
|
GG int `json:"gg"` // Göttliche Gnade
|
||||||
|
GP int `json:"gp"` // Glückspunkte
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCharacterDerivedValues speichert abgeleitete Werte
|
||||||
|
func UpdateCharacterDerivedValues(c *gin.Context) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
var request UpdateDerivedValuesRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Ungültige abgeleitete Werte: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Session aktualisieren
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "Abgeleitete Werte gespeichert",
|
||||||
|
"session_id": sessionID,
|
||||||
|
"current_step": 4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSkillsRequest
|
||||||
|
type UpdateSkillsRequest struct {
|
||||||
|
Skills []CharacterCreationSkill `json:"skills"`
|
||||||
|
Spells []CharacterCreationSpell `json:"spells"`
|
||||||
|
SkillPoints map[string]int `json:"skill_points"` // Verbleibende Punkte pro Kategorie
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCharacterSkills speichert Fertigkeiten und Zauber
|
||||||
|
func UpdateCharacterSkills(c *gin.Context) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
var request UpdateSkillsRequest
|
||||||
|
if err := c.ShouldBindJSON(&request); err != nil {
|
||||||
|
respondWithError(c, http.StatusBadRequest, "Ungültige Fertigkeitsdaten: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Session aktualisieren
|
||||||
|
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) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
// TODO: Session laden, validieren und finalen Charakter erstellen
|
||||||
|
// TODO: Character in Datenbank speichern
|
||||||
|
// TODO: Session löschen
|
||||||
|
|
||||||
|
// Dummy-Response
|
||||||
|
characterID := uint(123) // Würde aus DB kommen
|
||||||
|
|
||||||
|
c.JSON(http.StatusCreated, gin.H{
|
||||||
|
"message": "Charakter erfolgreich erstellt",
|
||||||
|
"character_id": characterID,
|
||||||
|
"session_id": sessionID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCharacterSession löscht eine Session
|
||||||
|
func DeleteCharacterSession(c *gin.Context) {
|
||||||
|
sessionID := c.Param("sessionId")
|
||||||
|
|
||||||
|
// TODO: Session aus Datenbank löschen
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// TODO: Aus Datenbank laden
|
||||||
|
classes := []string{
|
||||||
|
"Abenteurer", "Assassine", "Barbar", "Barde", "Bauer",
|
||||||
|
"Glückspriester", "Heiler", "Händler", "Kämpfer", "Krieger",
|
||||||
|
"Magier", "Ordenskrieger", "Priester", "Schamane", "Seefahrer",
|
||||||
|
"Späher", "Thaumaturg", "Waldläufer", "Zauberer", "Zaubersänger",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{"classes": classes})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrigins gibt verfügbare Herkünfte zurück
|
||||||
|
func GetOrigins(c *gin.Context) {
|
||||||
|
// TODO: Aus Datenbank laden
|
||||||
|
origins := []string{
|
||||||
|
"Albai", "Aran", "Buluga", "Cangebiet", "Chryseia", "Dwerunlande",
|
||||||
|
"Eschar", "Fuardain", "Ikenga", "KanThaiPan", "Küstenstaaten",
|
||||||
|
"Medjis", "Moravod", "Nahuatlan", "Rawindra", "Scharidis",
|
||||||
|
"Tegarisch Gebiete", "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
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Datenbanksuche implementieren
|
||||||
|
// Dummy-Daten für Demo
|
||||||
|
allBeliefs := []string{
|
||||||
|
"Apshai", "Arthusos", "Beschützer", "Dwyllas", "Elfen",
|
||||||
|
"Fruchtbarkeitsgöttin", "Gaia", "Grafschafter", "Heiler",
|
||||||
|
"Jäger", "Kämpfer", "Lichbringer", "Meeresherr", "Natur",
|
||||||
|
"Ostküste", "Priester", "Rechtschaffener", "Schutzpatron",
|
||||||
|
"Stammesgeist", "Totengott", "Unterwelt", "Vater", "Weisheit",
|
||||||
|
"Xan", "Ylhoon", "Zauberer",
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSkillCategoriesWithPoints gibt Kategorien mit Lernpunkten zurück
|
||||||
|
func GetSkillCategoriesWithPoints(c *gin.Context) {
|
||||||
|
// TODO: Basierend auf Charakter-Klasse und -Typ berechnen
|
||||||
|
categories := []SkillCategoryWithPoints{
|
||||||
|
{Name: "alltag", DisplayName: "Alltag", Points: 150, MaxPoints: 150},
|
||||||
|
{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})
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,4 +50,22 @@ func RegisterRoutes(r *gin.RouterGroup) {
|
|||||||
// System-Information
|
// System-Information
|
||||||
charGrp.GET("/character-classes", GetCharacterClassesHandlerOld)
|
charGrp.GET("/character-classes", GetCharacterClassesHandlerOld)
|
||||||
charGrp.GET("/skill-categories", GetSkillCategoriesHandlerOld)
|
charGrp.GET("/skill-categories", GetSkillCategoriesHandlerOld)
|
||||||
|
|
||||||
|
// Character Creation
|
||||||
|
charGrp.GET("/create-sessions", ListCharacterSessions) // Aktive Sessions für Benutzer auflisten
|
||||||
|
charGrp.POST("/create-session", CreateCharacterSession) // Neue Charakter-Erstellungssession
|
||||||
|
charGrp.GET("/create-session/:sessionId", GetCharacterSession) // Session-Daten abrufen
|
||||||
|
charGrp.PUT("/create-session/:sessionId/basic", UpdateCharacterBasicInfo) // Grundinformationen speichern
|
||||||
|
charGrp.PUT("/create-session/:sessionId/attributes", UpdateCharacterAttributes) // Grundwerte speichern
|
||||||
|
charGrp.PUT("/create-session/:sessionId/derived", UpdateCharacterDerivedValues) // Abgeleitete Werte speichern
|
||||||
|
charGrp.PUT("/create-session/:sessionId/skills", UpdateCharacterSkills) // Fertigkeiten speichern
|
||||||
|
charGrp.POST("/create-session/:sessionId/finalize", FinalizeCharacterCreation) // Charakter-Erstellung abschließen
|
||||||
|
charGrp.DELETE("/create-session/:sessionId", DeleteCharacterSession) // Session löschen
|
||||||
|
|
||||||
|
// Reference Data für Character Creation
|
||||||
|
charGrp.GET("/races", GetRaces) // Verfügbare Rassen
|
||||||
|
charGrp.GET("/classes", GetCharacterClasses) // Verfügbare Klassen
|
||||||
|
charGrp.GET("/origins", GetOrigins) // Verfügbare Herkünfte
|
||||||
|
charGrp.GET("/beliefs", SearchBeliefs) // Glaube-Suche
|
||||||
|
charGrp.GET("/skill-categories-with-points", GetSkillCategoriesWithPoints) // Kategorien mit Lernpunkten
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"gorm.io/driver/mysql"
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,13 +39,11 @@ var (
|
|||||||
|
|
||||||
func ConnectDatabase() *gorm.DB {
|
func ConnectDatabase() *gorm.DB {
|
||||||
SetupTestDB()
|
SetupTestDB()
|
||||||
/*
|
db, err := gorm.Open(sqlite.Open(PreparedTestDB), &gorm.Config{})
|
||||||
db, err := gorm.Open(sqlite.Open(PreparedTestDB), &gorm.Config{})
|
if err != nil {
|
||||||
if err != nil {
|
log.Fatal("Failed to connect to database:", err)
|
||||||
log.Fatal("Failed to connect to database:", err)
|
}
|
||||||
}
|
DB = db
|
||||||
DB = db
|
|
||||||
*/
|
|
||||||
return DB
|
return DB
|
||||||
}
|
}
|
||||||
func ConnectDatabaseOrig() *gorm.DB {
|
func ConnectDatabaseOrig() *gorm.DB {
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CharacterCreationSession speichert den Fortschritt der Charakter-Erstellung
|
||||||
|
type CharacterCreationSession struct {
|
||||||
|
ID string `json:"id" gorm:"primaryKey"`
|
||||||
|
UserID uint `json:"user_id" gorm:"index"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Rasse string `json:"rasse"`
|
||||||
|
Typ string `json:"typ"`
|
||||||
|
Herkunft string `json:"herkunft"`
|
||||||
|
Glaube string `json:"glaube"`
|
||||||
|
Attributes AttributesData `json:"attributes" gorm:"type:json"`
|
||||||
|
DerivedValues DerivedValuesData `json:"derived_values" gorm:"type:json"`
|
||||||
|
Skills []CharacterCreationSkill `json:"skills" gorm:"type:json"`
|
||||||
|
Spells []CharacterCreationSpell `json:"spells" gorm:"type:json"`
|
||||||
|
SkillPoints SkillPointsData `json:"skill_points" gorm:"type:json"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
|
CurrentStep int `json:"current_step"` // 1=Basic, 2=Attributes, 3=Derived, 4=Skills
|
||||||
|
}
|
||||||
|
|
||||||
|
// AttributesData speichert die Grundwerte
|
||||||
|
type AttributesData struct {
|
||||||
|
ST int `json:"st"` // Stärke
|
||||||
|
GS int `json:"gs"` // Geschicklichkeit
|
||||||
|
GW int `json:"gw"` // Gewandtheit
|
||||||
|
KO int `json:"ko"` // Konstitution
|
||||||
|
IN int `json:"in"` // Intelligenz
|
||||||
|
ZT int `json:"zt"` // Zaubertalent
|
||||||
|
AU int `json:"au"` // Ausstrahlung
|
||||||
|
PA int `json:"pa"` // Psi-Kraft
|
||||||
|
WK int `json:"wk"` // Willenskraft
|
||||||
|
}
|
||||||
|
|
||||||
|
// DerivedValuesData speichert die abgeleiteten Werte
|
||||||
|
type DerivedValuesData struct {
|
||||||
|
LPMax int `json:"lp_max"` // Lebenspunkte Maximum
|
||||||
|
APMax int `json:"ap_max"` // Abenteuerpunkte Maximum
|
||||||
|
BMax int `json:"b_max"` // Belastung Maximum
|
||||||
|
SG int `json:"sg"` // Schicksalsgunst
|
||||||
|
GG int `json:"gg"` // Göttliche Gnade
|
||||||
|
GP int `json:"gp"` // Glückspunkte
|
||||||
|
}
|
||||||
|
|
||||||
|
// SkillPointsData speichert die verbleibenden Lernpunkte pro Kategorie
|
||||||
|
type SkillPointsData map[string]int
|
||||||
|
|
||||||
|
// CharacterCreationSkill repräsentiert eine ausgewählte Fertigkeit
|
||||||
|
type CharacterCreationSkill struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Level int `json:"level"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Cost int `json:"cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharacterCreationSpell repräsentiert einen ausgewählten Zauber
|
||||||
|
type CharacterCreationSpell struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Cost int `json:"cost"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scanning methods for JSON fields in GORM
|
||||||
|
func (a *AttributesData) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(bytes, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AttributesData) Value() (interface{}, error) {
|
||||||
|
return json.Marshal(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *DerivedValuesData) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(bytes, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d DerivedValuesData) Value() (interface{}, error) {
|
||||||
|
return json.Marshal(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SkillPointsData) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, ok := value.([]byte)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(bytes, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s SkillPointsData) Value() (interface{}, error) {
|
||||||
|
return json.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup expired sessions
|
||||||
|
func CleanupExpiredSessions(db *gorm.DB) error {
|
||||||
|
return db.Where("expires_at < ?", time.Now()).Delete(&CharacterCreationSession{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find sessions by user ID
|
||||||
|
func GetUserSessions(db *gorm.DB, userID uint) ([]CharacterCreationSession, error) {
|
||||||
|
var sessions []CharacterCreationSession
|
||||||
|
err := db.Where("user_id = ? AND expires_at > ?", userID, time.Now()).Find(&sessions).Error
|
||||||
|
return sessions, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# Character Creation System
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
|
||||||
|
Das Character Creation System ermöglicht es Benutzern, neue Charaktere in einem mehrstufigen Prozess zu erstellen. Der Fortschritt wird nach jedem Schritt gespeichert und steht dem Benutzer für 14 Tage zur Verfügung.
|
||||||
|
|
||||||
|
## Implementierte Features
|
||||||
|
|
||||||
|
### ✅ Backend (Go/Gin)
|
||||||
|
|
||||||
|
**Neue API-Endpunkte in `/backend/character/routes.go`:**
|
||||||
|
- `GET /api/characters/create-sessions` - Aktive Sessions für Benutzer auflisten
|
||||||
|
- `POST /api/characters/create-session` - Neue Session erstellen
|
||||||
|
- `GET /api/characters/create-session/:sessionId` - Session-Daten abrufen
|
||||||
|
- `PUT /api/characters/create-session/:sessionId/basic` - Grundinformationen
|
||||||
|
- `PUT /api/characters/create-session/:sessionId/attributes` - Grundwerte
|
||||||
|
- `PUT /api/characters/create-session/:sessionId/derived` - Abgeleitete Werte
|
||||||
|
- `PUT /api/characters/create-session/:sessionId/skills` - Fertigkeiten
|
||||||
|
- `PUT /api/characters/create-session/:sessionId/spells` - Zauber
|
||||||
|
- `POST /api/characters/create-session/:sessionId/finalize` - Abschließen
|
||||||
|
- `DELETE /api/characters/create-session/:sessionId` - Session löschen
|
||||||
|
- `GET /api/characters/races` - Verfügbare Rassen
|
||||||
|
- `GET /api/characters/classes` - Verfügbare Klassen
|
||||||
|
- `GET /api/characters/origins` - Verfügbare Herkünfte
|
||||||
|
- `GET /api/characters/beliefs?q=searchterm` - Glaube-Suche
|
||||||
|
- `GET /api/characters/skill-categories` - Kategorien mit Lernpunkten
|
||||||
|
|
||||||
|
**Handler in `/backend/character/handlers.go`:**
|
||||||
|
- Session-Management mit 14-Tage Gültigkeit
|
||||||
|
- Reference Data APIs mit Dummy-Daten
|
||||||
|
- Step-by-Step Validierung und Speicherung
|
||||||
|
|
||||||
|
**Datenmodell in `/backend/models/model_character_creation.go`:**
|
||||||
|
- CharacterCreationSession mit JSON-Feldern
|
||||||
|
- Automatische Session-Bereinigung
|
||||||
|
- Type-safe Datenstrukturen
|
||||||
|
|
||||||
|
### ✅ Frontend (Vue 3)
|
||||||
|
|
||||||
|
**Button in CharacterList.vue:**
|
||||||
|
- "Create New Character" Button mit Styling
|
||||||
|
- Session-Erstellung und Navigation
|
||||||
|
- **Aktive Sessions-Anzeige**: Grid mit Session-Karten
|
||||||
|
- **Session-Details**: Name, Fortschritt, Rasse/Klasse, Daten
|
||||||
|
- **Continue/Delete**: Fortsetzen oder Löschen von Drafts
|
||||||
|
|
||||||
|
**Neue Komponenten:**
|
||||||
|
- `CharacterCreation.vue` - Hauptcontainer mit Progress-Indicator
|
||||||
|
- `CharacterCreation/CharacterBasicInfo.vue` - Schritt 1: Grunddaten
|
||||||
|
- `CharacterCreation/CharacterAttributes.vue` - Schritt 2: Grundwerte
|
||||||
|
- `CharacterCreation/CharacterDerivedValues.vue` - Schritt 3: Abgeleitete Werte
|
||||||
|
- `CharacterCreation/CharacterSkills.vue` - Schritt 4: Fertigkeiten
|
||||||
|
- `CharacterCreation/CharacterSpells.vue` - Schritt 5: Zauber
|
||||||
|
|
||||||
|
**Router-Integration:**
|
||||||
|
- Route `/character/create/:sessionId` hinzugefügt
|
||||||
|
- Props-basierte Parameter-Übergabe
|
||||||
|
|
||||||
|
## Funktionsdetails
|
||||||
|
|
||||||
|
### Schritt 1: Grundinformationen
|
||||||
|
- **Name**: Text-Input mit Validierung (2-50 Zeichen)
|
||||||
|
- **Rasse**: Dropdown aus API-Daten
|
||||||
|
- **Klasse**: Dropdown aus API-Daten
|
||||||
|
- **Herkunft**: Dropdown aus API-Daten
|
||||||
|
- **Glaube**: Suchfeld mit dynamischem Autocomplete (min. 2 Zeichen)
|
||||||
|
|
||||||
|
### Schritt 2: Grundwerte
|
||||||
|
- **9 Attribute**: ST, GS, GW, KO, IN, ZT, AU, PA, WK (1-100)
|
||||||
|
- **Live-Berechnung**: Gesamtpunkte und Durchschnitt
|
||||||
|
- **Responsive Grid**: Automatisches Layout für verschiedene Bildschirmgrößen
|
||||||
|
|
||||||
|
### Schritt 3: Abgeleitete Werte
|
||||||
|
- **LP/AP/B**: Automatische Berechnung aus Grundwerten
|
||||||
|
- **Bennies**: SG, GG, GP basierend auf Klasse
|
||||||
|
- **Recalculate-Button**: Reset auf berechnete Werte
|
||||||
|
- **Manuelle Anpassung**: Überschreibung der Berechnungen möglich
|
||||||
|
|
||||||
|
### Schritt 4: Fertigkeiten
|
||||||
|
- **Kategorien-Ansicht**: Grid mit Lernpunkt-Anzeige und Progress-Bars
|
||||||
|
- **Skill-Listen**: Dynamische Listen pro Kategorie aus Datenbank
|
||||||
|
- **Punkt-Tracking**: Echtzeit-Update der verbleibenden Lernpunkte
|
||||||
|
- **Selected-Overview**: Zusammenfassung aller gewählten Skills
|
||||||
|
- **Navigation**: "Next: Spells" für nächsten Schritt
|
||||||
|
|
||||||
|
### Schritt 5: Zauber
|
||||||
|
- **Zauber-Liste**: Separate Kategorie für magische Fähigkeiten
|
||||||
|
- **Zauber-Punkte**: Eigenständiges Punktesystem für Magie
|
||||||
|
- **Visual Feedback**: Kann/kann nicht lernen Indikationen
|
||||||
|
- **Finalisierung**: "Create Character" Button für Abschluss
|
||||||
|
|
||||||
|
## Session-Management
|
||||||
|
|
||||||
|
### Persistierung
|
||||||
|
- **Session-ID**: UUID-basiert für Eindeutigkeit
|
||||||
|
- **Auto-Save**: Nach jedem Schritt automatische Speicherung
|
||||||
|
- **14-Tage Gültigkeit**: Automatische Bereinigung alter Sessions
|
||||||
|
- **Step-Tracking**: Fortschritts-Verfolgung über alle Schritte
|
||||||
|
|
||||||
|
### Benutzerführung
|
||||||
|
- **Progress-Indicator**: Visuelle Fortschrittsanzeige (5 Schritte) - **CLICKABLE!**
|
||||||
|
- **Navigation**: Vor/Zurück zwischen Schritten über klickbare Progress-Steps
|
||||||
|
- **Validierung**: Schritt-Blockierung bei fehlenden Daten
|
||||||
|
- **Draft-Management**: Session-Liste mit Continue/Delete-Optionen
|
||||||
|
- **Session-Übersicht**: Aktive Drafts auf Character-Liste-Seite
|
||||||
|
- **Datenerhaltung**: Alle eingegebenen Daten bleiben beim Wechseln erhalten
|
||||||
|
|
||||||
|
## Dummy-Implementierungen
|
||||||
|
|
||||||
|
**Backend Placeholders:**
|
||||||
|
- Reference Data als hardcodierte Arrays
|
||||||
|
- Session-Speicherung in Memory (TODO: Redis/DB)
|
||||||
|
- User-ID als Konstante (TODO: Auth-Context)
|
||||||
|
- Skill/Spell-Kosten als Fallback-Werte
|
||||||
|
|
||||||
|
**Frontend Fallbacks:**
|
||||||
|
- API-Fehler-Behandlung mit Sample-Daten
|
||||||
|
- Loading-States für bessere UX
|
||||||
|
- Offline-fähige Formulare
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
|
||||||
|
### 🎯 Produktive Implementierung
|
||||||
|
1. **Session-Storage**: Redis oder DB-Tabelle
|
||||||
|
2. **Reference Data**: Echte Datenbank-Anbindung
|
||||||
|
3. **Auth-Integration**: User-ID aus JWT/Session
|
||||||
|
4. **Validation**: Server-seitige Eingabe-Prüfung
|
||||||
|
|
||||||
|
### 🔧 Optimierungen
|
||||||
|
1. **Performance**: Reference Data Caching
|
||||||
|
2. **UX**: Loading-Spinner und Transitions
|
||||||
|
3. **Error Handling**: Robuste Fehlerbehandlung
|
||||||
|
4. **Tests**: Unit- und E2E-Tests
|
||||||
|
|
||||||
|
Die Implementierung ist funktional vollständig und kann sofort getestet werden. Alle Dummy-Implementierungen sind klar markiert für schrittweise Produktionsreife.
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
<template>
|
||||||
|
<div class="character-creation">
|
||||||
|
<div class="creation-header">
|
||||||
|
<h1>Create New Character</h1>
|
||||||
|
<div class="progress-indicator">
|
||||||
|
<div
|
||||||
|
v-for="step in steps"
|
||||||
|
:key="step.number"
|
||||||
|
:class="['step', {
|
||||||
|
active: currentStep === step.number,
|
||||||
|
completed: currentStep > step.number,
|
||||||
|
clickable: currentStep > step.number || currentStep === step.number
|
||||||
|
}]"
|
||||||
|
@click="navigateToStep(step.number)"
|
||||||
|
>
|
||||||
|
<span class="step-number">{{ step.number }}</span>
|
||||||
|
<span class="step-title">{{ step.title }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="creation-content">
|
||||||
|
<!-- Step 1: Basic Information -->
|
||||||
|
<CharacterBasicInfo
|
||||||
|
v-if="currentStep === 1"
|
||||||
|
:session-data="sessionData"
|
||||||
|
@next="handleNext"
|
||||||
|
@save="saveProgress"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Step 2: Attributes -->
|
||||||
|
<CharacterAttributes
|
||||||
|
v-if="currentStep === 2"
|
||||||
|
:session-data="sessionData"
|
||||||
|
@next="handleNext"
|
||||||
|
@previous="handlePrevious"
|
||||||
|
@save="saveProgress"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Step 3: Derived Values -->
|
||||||
|
<CharacterDerivedValues
|
||||||
|
v-if="currentStep === 3"
|
||||||
|
:session-data="sessionData"
|
||||||
|
@next="handleNext"
|
||||||
|
@previous="handlePrevious"
|
||||||
|
@save="saveProgress"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Step 4: Skills -->
|
||||||
|
<CharacterSkills
|
||||||
|
v-if="currentStep === 4"
|
||||||
|
:session-data="sessionData"
|
||||||
|
:skill-categories="skillCategories"
|
||||||
|
@previous="handlePrevious"
|
||||||
|
@next="handleNext"
|
||||||
|
@save="saveProgress"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Step 5: Spells -->
|
||||||
|
<CharacterSpells
|
||||||
|
v-if="currentStep === 5"
|
||||||
|
:session-data="sessionData"
|
||||||
|
:skill-categories="skillCategories"
|
||||||
|
@previous="handlePrevious"
|
||||||
|
@finalize="handleFinalize"
|
||||||
|
@save="saveProgress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Info -->
|
||||||
|
<div class="session-info">
|
||||||
|
<p>Session expires: {{ formatDate(sessionData.expires_at) }}</p>
|
||||||
|
<button @click="deleteDraft" class="delete-btn">Delete Draft</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import API from '../utils/api'
|
||||||
|
import CharacterBasicInfo from './CharacterCreation/CharacterBasicInfo.vue'
|
||||||
|
import CharacterAttributes from './CharacterCreation/CharacterAttributes.vue'
|
||||||
|
import CharacterDerivedValues from './CharacterCreation/CharacterDerivedValues.vue'
|
||||||
|
import CharacterSkills from './CharacterCreation/CharacterSkills.vue'
|
||||||
|
import CharacterSpells from './CharacterCreation/CharacterSpells.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterCreation',
|
||||||
|
components: {
|
||||||
|
CharacterBasicInfo,
|
||||||
|
CharacterAttributes,
|
||||||
|
CharacterDerivedValues,
|
||||||
|
CharacterSkills,
|
||||||
|
CharacterSpells,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
sessionId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
currentStep: 1,
|
||||||
|
sessionData: {
|
||||||
|
id: '',
|
||||||
|
name: '',
|
||||||
|
rasse: '',
|
||||||
|
typ: '',
|
||||||
|
herkunft: '',
|
||||||
|
glaube: '',
|
||||||
|
attributes: {},
|
||||||
|
derived_values: {},
|
||||||
|
skills: [],
|
||||||
|
spells: [],
|
||||||
|
skill_points: {},
|
||||||
|
spell_points: {},
|
||||||
|
expires_at: null,
|
||||||
|
current_step: 1,
|
||||||
|
},
|
||||||
|
steps: [
|
||||||
|
{ number: 1, title: 'Basic Info' },
|
||||||
|
{ number: 2, title: 'Attributes' },
|
||||||
|
{ number: 3, title: 'Derived Values' },
|
||||||
|
{ number: 4, title: 'Skills' },
|
||||||
|
{ number: 5, title: 'Spells' },
|
||||||
|
],
|
||||||
|
skillCategories: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
await this.loadSession()
|
||||||
|
await this.loadSkillCategories()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadSession() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get(`/api/characters/create-session/${this.sessionId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sessionData = response.data
|
||||||
|
this.currentStep = response.data.current_step || 1
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading session:', error)
|
||||||
|
this.$router.push('/dashboard')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSkillCategories() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get('/api/characters/skill-categories', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.skillCategories = response.data.categories || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading skill categories:', error)
|
||||||
|
// Fallback dummy data
|
||||||
|
this.skillCategories = [
|
||||||
|
{ name: 'körperlich', display_name: 'Körperliche Fertigkeiten', max_points: 200, points: 200 },
|
||||||
|
{ name: 'gesellschaftlich', display_name: 'Gesellschaftliche Fertigkeiten', max_points: 150, points: 150 },
|
||||||
|
{ name: 'natur', display_name: 'Natur Fertigkeiten', max_points: 100, points: 100 },
|
||||||
|
{ name: 'wissen', display_name: 'Wissens Fertigkeiten', max_points: 180, points: 180 },
|
||||||
|
{ name: 'handwerk', display_name: 'Handwerks Fertigkeiten', max_points: 120, points: 120 },
|
||||||
|
{ name: 'zauber', display_name: 'Zauber', max_points: 300, points: 300 },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleNext(data) {
|
||||||
|
this.sessionData = { ...this.sessionData, ...data }
|
||||||
|
this.currentStep++
|
||||||
|
this.saveProgress()
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePrevious() {
|
||||||
|
this.currentStep--
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateToStep(stepNumber) {
|
||||||
|
// Only allow navigation to current step or previously completed steps
|
||||||
|
if (stepNumber <= this.currentStep) {
|
||||||
|
this.currentStep = stepNumber
|
||||||
|
// Save current progress before switching steps
|
||||||
|
this.saveProgress()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveProgress() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
// Bestimme den korrekten Endpoint basierend auf dem aktuellen Schritt
|
||||||
|
let endpoint = ''
|
||||||
|
let payload = {}
|
||||||
|
|
||||||
|
switch (this.currentStep) {
|
||||||
|
case 1:
|
||||||
|
endpoint = `/api/characters/create-session/${this.sessionId}/basic`
|
||||||
|
payload = {
|
||||||
|
name: this.sessionData.name,
|
||||||
|
rasse: this.sessionData.rasse,
|
||||||
|
typ: this.sessionData.typ,
|
||||||
|
herkunft: this.sessionData.herkunft,
|
||||||
|
glaube: this.sessionData.glaube,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 2:
|
||||||
|
endpoint = `/api/characters/create-session/${this.sessionId}/attributes`
|
||||||
|
payload = this.sessionData.attributes
|
||||||
|
break
|
||||||
|
case 3:
|
||||||
|
endpoint = `/api/characters/create-session/${this.sessionId}/derived`
|
||||||
|
payload = this.sessionData.derived_values
|
||||||
|
break
|
||||||
|
case 4:
|
||||||
|
endpoint = `/api/characters/create-session/${this.sessionId}/skills`
|
||||||
|
payload = {
|
||||||
|
skills: this.sessionData.skills,
|
||||||
|
skill_points: this.sessionData.skill_points,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 5:
|
||||||
|
endpoint = `/api/characters/create-session/${this.sessionId}/spells`
|
||||||
|
payload = {
|
||||||
|
spells: this.sessionData.spells,
|
||||||
|
spell_points: this.sessionData.spell_points,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoint) {
|
||||||
|
await API.put(endpoint, payload, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving progress:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleFinalize() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.post(`/api/characters/create-session/${this.sessionId}/finalize`, {}, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
const characterId = response.data.character_id
|
||||||
|
this.$router.push(`/character/${characterId}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error finalizing character:', error)
|
||||||
|
alert('Fehler beim Abschließen der Charakter-Erstellung')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteDraft() {
|
||||||
|
if (confirm('Are you sure you want to delete this character draft?')) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
await API.delete(`/api/characters/create-session/${this.sessionId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$router.push('/dashboard')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting session:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(dateString) {
|
||||||
|
if (!dateString) return ''
|
||||||
|
return new Date(dateString).toLocaleDateString()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.character-creation {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-header h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-indicator {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 2px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.completed {
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border: 2px solid #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-number {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ddd;
|
||||||
|
color: #666;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active .step-number {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.completed .step-number {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.clickable:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.completed:hover .step-number {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active:hover .step-number {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-title {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-content {
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
<template>
|
||||||
|
<div class="attributes-form">
|
||||||
|
<h2>Character Attributes</h2>
|
||||||
|
<p class="instruction">Set the basic attributes for your character (1-100)</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit" class="attributes-form-content">
|
||||||
|
<div class="attributes-grid">
|
||||||
|
<div class="attribute-group" v-for="attr in attributes" :key="attr.key">
|
||||||
|
<div class="attribute-row">
|
||||||
|
<label :for="attr.key" class="attribute-label">
|
||||||
|
{{ attr.name }} ({{ attr.key.toUpperCase() }})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
:id="attr.key"
|
||||||
|
v-model.number="formData[attr.key]"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="100"
|
||||||
|
required
|
||||||
|
class="attribute-input"
|
||||||
|
@input="updateTotal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span class="attribute-description">{{ attr.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attribute-summary">
|
||||||
|
<div class="total-points">
|
||||||
|
<strong>Total Points: {{ totalPoints }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="average-points">
|
||||||
|
<strong>Average: {{ averagePoints.toFixed(1) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" @click="handlePrevious" class="prev-btn">
|
||||||
|
← Previous: Basic Info
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="next-btn" :disabled="!isValid">
|
||||||
|
Next: Derived Values →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CharacterAttributes',
|
||||||
|
props: {
|
||||||
|
sessionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['next', 'previous', 'save'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
st: 50, // Stärke
|
||||||
|
gs: 50, // Geschicklichkeit
|
||||||
|
gw: 50, // Gewandtheit
|
||||||
|
ko: 50, // Konstitution
|
||||||
|
in: 50, // Intelligenz
|
||||||
|
zt: 50, // Zaubertalent
|
||||||
|
au: 50, // Ausstrahlung
|
||||||
|
pa: 50, // Psi-Kraft
|
||||||
|
wk: 50, // Willenskraft
|
||||||
|
},
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
key: 'st',
|
||||||
|
name: 'Stärke',
|
||||||
|
description: 'Physical strength and power'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gs',
|
||||||
|
name: 'Geschicklichkeit',
|
||||||
|
description: 'Dexterity and manual skill'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gw',
|
||||||
|
name: 'Gewandtheit',
|
||||||
|
description: 'Agility and quick reactions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ko',
|
||||||
|
name: 'Konstitution',
|
||||||
|
description: 'Health and endurance'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'in',
|
||||||
|
name: 'Intelligenz',
|
||||||
|
description: 'Learning ability and logic'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'zt',
|
||||||
|
name: 'Zaubertalent',
|
||||||
|
description: 'Magical talent and mana'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'au',
|
||||||
|
name: 'Ausstrahlung',
|
||||||
|
description: 'Charisma and leadership'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'pa',
|
||||||
|
name: 'Psi-Kraft',
|
||||||
|
description: 'Psychic abilities'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'wk',
|
||||||
|
name: 'Willenskraft',
|
||||||
|
description: 'Mental fortitude and resistance'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
totalPoints: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isValid() {
|
||||||
|
return Object.values(this.formData).every(val => val >= 1 && val <= 100)
|
||||||
|
},
|
||||||
|
averagePoints() {
|
||||||
|
return this.totalPoints / Object.keys(this.formData).length
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// Initialize form with session data
|
||||||
|
if (this.sessionData.attributes && Object.keys(this.sessionData.attributes).length > 0) {
|
||||||
|
this.formData = { ...this.formData, ...this.sessionData.attributes }
|
||||||
|
}
|
||||||
|
this.updateTotal()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateTotal() {
|
||||||
|
this.totalPoints = Object.values(this.formData).reduce((sum, val) => sum + (val || 0), 0)
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePrevious() {
|
||||||
|
this.$emit('previous')
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
if (this.isValid) {
|
||||||
|
this.$emit('next', { attributes: this.formData })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.attributes-form {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-form h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
max-height: 50vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-group {
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
min-width: 0; /* Prevent overflow */
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-input {
|
||||||
|
width: 60px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2196f3;
|
||||||
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-description {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-summary {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-radius: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.total-points, .average-points {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attributes-form-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn, .next-btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:hover:not(:disabled) {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design für sehr kleine Bildschirme */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.attributes-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-group {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,324 @@
|
|||||||
|
<template>
|
||||||
|
<div class="basic-info-form">
|
||||||
|
<h2>Basic Character Information</h2>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="name">Character Name *</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
v-model="formData.name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
minlength="2"
|
||||||
|
maxlength="50"
|
||||||
|
placeholder="Enter character name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="rasse">Race *</label>
|
||||||
|
<select id="rasse" v-model="formData.rasse" required>
|
||||||
|
<option value="">Select Race</option>
|
||||||
|
<option v-for="race in races" :key="race" :value="race">{{ race }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="typ">Character Class *</label>
|
||||||
|
<select id="typ" v-model="formData.typ" required>
|
||||||
|
<option value="">Select Class</option>
|
||||||
|
<option v-for="cls in classes" :key="cls" :value="cls">{{ cls }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="herkunft">Origin *</label>
|
||||||
|
<select id="herkunft" v-model="formData.herkunft" required>
|
||||||
|
<option value="">Select Origin</option>
|
||||||
|
<option v-for="origin in origins" :key="origin" :value="origin">{{ origin }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="glaube">Religion/Belief</label>
|
||||||
|
<div class="belief-search">
|
||||||
|
<input
|
||||||
|
id="glaube"
|
||||||
|
v-model="beliefSearch"
|
||||||
|
type="text"
|
||||||
|
placeholder="Type at least 2 characters to search beliefs..."
|
||||||
|
@input="searchBeliefs"
|
||||||
|
/>
|
||||||
|
<div v-if="beliefResults.length > 0" class="belief-dropdown">
|
||||||
|
<div
|
||||||
|
v-for="belief in beliefResults"
|
||||||
|
:key="belief"
|
||||||
|
class="belief-option"
|
||||||
|
@click="selectBelief(belief)"
|
||||||
|
>
|
||||||
|
{{ belief }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="formData.glaube" class="selected-belief">
|
||||||
|
Selected: {{ formData.glaube }}
|
||||||
|
<button type="button" @click="clearBelief" class="clear-btn">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="next-btn" :disabled="!isValid">
|
||||||
|
Next: Attributes →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import API from '../../utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterBasicInfo',
|
||||||
|
props: {
|
||||||
|
sessionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['next', 'save'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
name: '',
|
||||||
|
rasse: '',
|
||||||
|
typ: '',
|
||||||
|
herkunft: '',
|
||||||
|
glaube: '',
|
||||||
|
},
|
||||||
|
races: [],
|
||||||
|
classes: [],
|
||||||
|
origins: [],
|
||||||
|
beliefSearch: '',
|
||||||
|
beliefResults: [],
|
||||||
|
searchTimeout: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isValid() {
|
||||||
|
return this.formData.name.length >= 2 &&
|
||||||
|
this.formData.rasse &&
|
||||||
|
this.formData.typ &&
|
||||||
|
this.formData.herkunft
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
// Initialize form with session data
|
||||||
|
this.formData = {
|
||||||
|
name: this.sessionData.name || '',
|
||||||
|
rasse: this.sessionData.rasse || '',
|
||||||
|
typ: this.sessionData.typ || '',
|
||||||
|
herkunft: this.sessionData.herkunft || '',
|
||||||
|
glaube: this.sessionData.glaube || '',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.formData.glaube) {
|
||||||
|
this.beliefSearch = this.formData.glaube
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.loadReferenceData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadReferenceData() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const headers = { Authorization: `Bearer ${token}` }
|
||||||
|
|
||||||
|
// Load all reference data in parallel
|
||||||
|
const [racesRes, classesRes, originsRes] = await Promise.all([
|
||||||
|
API.get('/api/characters/races', { headers }),
|
||||||
|
API.get('/api/characters/classes', { headers }),
|
||||||
|
API.get('/api/characters/origins', { headers }),
|
||||||
|
])
|
||||||
|
|
||||||
|
this.races = racesRes.data.races
|
||||||
|
this.classes = classesRes.data.classes
|
||||||
|
this.origins = originsRes.data.origins
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading reference data:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
searchBeliefs() {
|
||||||
|
if (this.searchTimeout) {
|
||||||
|
clearTimeout(this.searchTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchTimeout = setTimeout(async () => {
|
||||||
|
if (this.beliefSearch.length >= 2) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get(`/api/characters/beliefs?q=${this.beliefSearch}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
this.beliefResults = response.data.beliefs
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching beliefs:', error)
|
||||||
|
this.beliefResults = []
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.beliefResults = []
|
||||||
|
}
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
|
||||||
|
selectBelief(belief) {
|
||||||
|
this.formData.glaube = belief
|
||||||
|
this.beliefSearch = belief
|
||||||
|
this.beliefResults = []
|
||||||
|
},
|
||||||
|
|
||||||
|
clearBelief() {
|
||||||
|
this.formData.glaube = ''
|
||||||
|
this.beliefSearch = ''
|
||||||
|
this.beliefResults = []
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
if (this.isValid) {
|
||||||
|
this.$emit('next', this.formData)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.basic-info-form {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info-form h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2196f3;
|
||||||
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.belief-search {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.belief-dropdown {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.belief-option {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.belief-option:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.belief-option:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-belief {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
padding: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:hover:not(:disabled) {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,328 @@
|
|||||||
|
<template>
|
||||||
|
<div class="derived-values-form">
|
||||||
|
<h2>Derived Values</h2>
|
||||||
|
<p class="instruction">These values are calculated from your attributes. You can adjust them as needed.</p>
|
||||||
|
|
||||||
|
<form @submit.prevent="handleSubmit">
|
||||||
|
<div class="values-grid">
|
||||||
|
<div class="value-group" v-for="value in derivedValues" :key="value.key">
|
||||||
|
<label :for="value.key">{{ value.name }}</label>
|
||||||
|
<div class="value-input-group">
|
||||||
|
<input
|
||||||
|
:id="value.key"
|
||||||
|
v-model.number="formData[value.key]"
|
||||||
|
type="number"
|
||||||
|
:min="value.min"
|
||||||
|
:max="value.max"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="value-info">
|
||||||
|
<span class="calculated-value">Calculated: {{ calculatedValues[value.key] }}</span>
|
||||||
|
<span class="value-description">{{ value.description }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="calculation-info">
|
||||||
|
<h3>Calculation Rules</h3>
|
||||||
|
<div class="calculation-rules">
|
||||||
|
<div class="rule">
|
||||||
|
<strong>LP (Life Points):</strong> Base formula: (KO + ST) / 2 + modifier
|
||||||
|
</div>
|
||||||
|
<div class="rule">
|
||||||
|
<strong>AP (Adventure Points):</strong> Base formula: (AU + WK) / 2 + modifier
|
||||||
|
</div>
|
||||||
|
<div class="rule">
|
||||||
|
<strong>B (Burden):</strong> Base formula: ST + modifier
|
||||||
|
</div>
|
||||||
|
<div class="rule">
|
||||||
|
<strong>Bennies:</strong> Base values based on character class
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" @click="handlePrevious" class="prev-btn">
|
||||||
|
← Previous: Attributes
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="recalculate" class="calc-btn">
|
||||||
|
Recalculate from Attributes
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="next-btn" :disabled="!isValid">
|
||||||
|
Next: Skills & Spells →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'CharacterDerivedValues',
|
||||||
|
props: {
|
||||||
|
sessionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['next', 'previous', 'save'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formData: {
|
||||||
|
lp_max: 20,
|
||||||
|
ap_max: 20,
|
||||||
|
b_max: 50,
|
||||||
|
sg: 1, // Schicksalsgunst
|
||||||
|
gg: 1, // Göttliche Gnade
|
||||||
|
gp: 1, // Glückspunkte
|
||||||
|
},
|
||||||
|
derivedValues: [
|
||||||
|
{
|
||||||
|
key: 'lp_max',
|
||||||
|
name: 'Life Points (LP) Maximum',
|
||||||
|
description: 'Maximum life/health points',
|
||||||
|
min: 1,
|
||||||
|
max: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'ap_max',
|
||||||
|
name: 'Adventure Points (AP) Maximum',
|
||||||
|
description: 'Maximum adventure points for special actions',
|
||||||
|
min: 1,
|
||||||
|
max: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'b_max',
|
||||||
|
name: 'Burden (B) Maximum',
|
||||||
|
description: 'Maximum carrying capacity',
|
||||||
|
min: 1,
|
||||||
|
max: 500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'sg',
|
||||||
|
name: 'Schicksalsgunst (SG)',
|
||||||
|
description: 'Fate points for rerolls',
|
||||||
|
min: 0,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gg',
|
||||||
|
name: 'Göttliche Gnade (GG)',
|
||||||
|
description: 'Divine grace points',
|
||||||
|
min: 0,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'gp',
|
||||||
|
name: 'Glückspunkte (GP)',
|
||||||
|
description: 'Luck points',
|
||||||
|
min: 0,
|
||||||
|
max: 10
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isValid() {
|
||||||
|
return Object.entries(this.formData).every(([key, val]) => {
|
||||||
|
const valueConfig = this.derivedValues.find(v => v.key === key)
|
||||||
|
return val >= valueConfig.min && val <= valueConfig.max
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
calculatedValues() {
|
||||||
|
const attrs = this.sessionData.attributes || {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lp_max: Math.floor(((attrs.ko || 50) + (attrs.st || 50)) / 2) + 5,
|
||||||
|
ap_max: Math.floor(((attrs.au || 50) + (attrs.wk || 50)) / 2) + 5,
|
||||||
|
b_max: (attrs.st || 50) + 10,
|
||||||
|
sg: this.getClassBonnie('sg'),
|
||||||
|
gg: this.getClassBonnie('gg'),
|
||||||
|
gp: this.getClassBonnie('gp'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
// Initialize with calculated values first
|
||||||
|
this.formData = { ...this.calculatedValues }
|
||||||
|
|
||||||
|
// Then override with session data if available
|
||||||
|
if (this.sessionData.derived_values && Object.keys(this.sessionData.derived_values).length > 0) {
|
||||||
|
this.formData = { ...this.formData, ...this.sessionData.derived_values }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getClassBonnie(type) {
|
||||||
|
// TODO: Implement class-specific bonnie calculations
|
||||||
|
// For now, return base values
|
||||||
|
const bonnieMap = {
|
||||||
|
'sg': 1,
|
||||||
|
'gg': 1,
|
||||||
|
'gp': 1,
|
||||||
|
}
|
||||||
|
return bonnieMap[type] || 1
|
||||||
|
},
|
||||||
|
|
||||||
|
recalculate() {
|
||||||
|
this.formData = { ...this.calculatedValues }
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePrevious() {
|
||||||
|
this.$emit('previous')
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSubmit() {
|
||||||
|
if (this.isValid) {
|
||||||
|
this.$emit('next', { derived_values: this.formData })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.derived-values-form {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.derived-values-form h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.values-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-group {
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-input-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-input-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #2196f3;
|
||||||
|
box-shadow: 0 0 5px rgba(33, 150, 243, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculated-value {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #4caf50;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculation-info {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #e8f5e8;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculation-info h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculation-rules {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn, .calc-btn, .next-btn {
|
||||||
|
padding: 12px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-btn {
|
||||||
|
background-color: #ff9800;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-btn:hover {
|
||||||
|
background-color: #f57c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn {
|
||||||
|
background-color: #2196f3;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:hover:not(:disabled) {
|
||||||
|
background-color: #1976d2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,443 @@
|
|||||||
|
<template>
|
||||||
|
<div class="skills-form">
|
||||||
|
<h2>Skills & Spells</h2>
|
||||||
|
<p class="instruction">Select skills and spells for your character. Each category has a limited number of learning points.</p>
|
||||||
|
|
||||||
|
<div class="skills-content">
|
||||||
|
<!-- Skill Categories -->
|
||||||
|
<div class="categories-section">
|
||||||
|
<h3>Skill Categories</h3>
|
||||||
|
<div class="categories-grid">
|
||||||
|
<div
|
||||||
|
v-for="category in skillCategories"
|
||||||
|
:key="category.name"
|
||||||
|
:class="['category-card', { active: selectedCategory === category.name }]"
|
||||||
|
@click="selectCategory(category.name)"
|
||||||
|
v-if="category.name !== 'zauber'"
|
||||||
|
>
|
||||||
|
<div class="category-header">
|
||||||
|
<h4>{{ category.display_name }}</h4>
|
||||||
|
<div class="points-info">
|
||||||
|
<span class="remaining">{{ category.points }}</span> /
|
||||||
|
<span class="total">{{ category.max_points }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress-bar">
|
||||||
|
<div
|
||||||
|
class="progress-fill"
|
||||||
|
:style="{ width: ((category.max_points - category.points) / category.max_points * 100) + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills List for Selected Category -->
|
||||||
|
<div v-if="selectedCategory" class="skills-section">
|
||||||
|
<h3>{{ getCategoryDisplayName(selectedCategory) }} Skills</h3>
|
||||||
|
<div class="skills-list">
|
||||||
|
<div
|
||||||
|
v-for="skill in availableSkills"
|
||||||
|
:key="skill.name"
|
||||||
|
class="skill-item"
|
||||||
|
>
|
||||||
|
<div class="skill-info">
|
||||||
|
<span class="skill-name">{{ skill.name }}</span>
|
||||||
|
<span class="skill-cost">Cost: {{ skill.cost }} EP</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="addSkill(skill)"
|
||||||
|
:disabled="!canAddSkill(skill)"
|
||||||
|
class="add-btn"
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Skills -->
|
||||||
|
<div class="selected-section">
|
||||||
|
<h3>Selected Skills</h3>
|
||||||
|
|
||||||
|
<div v-if="selectedSkills.length > 0" class="selected-skills">
|
||||||
|
<div class="selected-list">
|
||||||
|
<div
|
||||||
|
v-for="skill in selectedSkills"
|
||||||
|
:key="skill.name"
|
||||||
|
class="selected-item"
|
||||||
|
>
|
||||||
|
<span class="item-name">{{ skill.name }}</span>
|
||||||
|
<span class="item-category">{{ skill.category }}</span>
|
||||||
|
<span class="item-cost">{{ skill.cost }} EP</span>
|
||||||
|
<button @click="removeSkill(skill)" class="remove-btn">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedSkills.length === 0" class="no-selection">
|
||||||
|
No skills selected yet. Click on a category above to start selecting skills.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="button" @click="handlePrevious" class="prev-btn">
|
||||||
|
← Previous: Derived Values
|
||||||
|
</button>
|
||||||
|
<button type="button" @click="handleNext" class="next-btn">
|
||||||
|
Next: Spells →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import API from '../../utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterSkills',
|
||||||
|
props: {
|
||||||
|
sessionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
skillCategories: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
emits: ['previous', 'next', 'save'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedCategory: null,
|
||||||
|
availableSkills: [],
|
||||||
|
selectedSkills: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
// Initialize with session data
|
||||||
|
if (this.sessionData.skills) {
|
||||||
|
this.selectedSkills = [...this.sessionData.skills]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateCategoryPoints()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async selectCategory(categoryName) {
|
||||||
|
this.selectedCategory = categoryName
|
||||||
|
await this.loadSkills(categoryName)
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSkills(category) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
// Create a dummy request for skills in this category
|
||||||
|
const request = {
|
||||||
|
characterClass: this.sessionData.typ || 'Abenteurer',
|
||||||
|
characterId: '0', // Dummy for new character
|
||||||
|
category: category,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.post('/api/characters/available-skills-new', request, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.availableSkills = response.data.skills || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading skills:', error)
|
||||||
|
// Fallback dummy data
|
||||||
|
this.availableSkills = [
|
||||||
|
{ name: 'Sample Skill 1', cost: 30, category: category },
|
||||||
|
{ name: 'Sample Skill 2', cost: 40, category: category },
|
||||||
|
{ name: 'Sample Skill 3', cost: 50, category: category },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getCategoryDisplayName(categoryName) {
|
||||||
|
const category = this.skillCategories.find(c => c.name === categoryName)
|
||||||
|
return category ? category.display_name : categoryName
|
||||||
|
},
|
||||||
|
|
||||||
|
canAddSkill(skill) {
|
||||||
|
const category = this.skillCategories.find(c => c.name === skill.category)
|
||||||
|
const alreadySelected = this.selectedSkills.some(s => s.name === skill.name)
|
||||||
|
|
||||||
|
return category && category.points >= skill.cost && !alreadySelected
|
||||||
|
},
|
||||||
|
|
||||||
|
addSkill(skill) {
|
||||||
|
if (this.canAddSkill(skill)) {
|
||||||
|
this.selectedSkills.push({ ...skill })
|
||||||
|
this.updateCategoryPoints()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSkill(skill) {
|
||||||
|
const index = this.selectedSkills.findIndex(s => s.name === skill.name)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.selectedSkills.splice(index, 1)
|
||||||
|
this.updateCategoryPoints()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateCategoryPoints() {
|
||||||
|
// Reset all categories to max points
|
||||||
|
this.skillCategories.forEach(category => {
|
||||||
|
category.points = category.max_points
|
||||||
|
})
|
||||||
|
|
||||||
|
// Deduct points for selected skills
|
||||||
|
this.selectedSkills.forEach(skill => {
|
||||||
|
const category = this.skillCategories.find(c => c.name === skill.category)
|
||||||
|
if (category) {
|
||||||
|
category.points -= skill.cost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePrevious() {
|
||||||
|
this.$emit('previous')
|
||||||
|
},
|
||||||
|
|
||||||
|
handleNext() {
|
||||||
|
const data = {
|
||||||
|
skills: this.selectedSkills,
|
||||||
|
skill_points: this.skillCategories.reduce((acc, cat) => {
|
||||||
|
acc[cat.name] = cat.points
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('next', data)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.skills-form {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-form h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instruction {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-section h3, .skills-section h3, .spells-section h3, .selected-section h3 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card {
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fafafa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card:hover {
|
||||||
|
border-color: #2196f3;
|
||||||
|
background-color: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-card.active {
|
||||||
|
border-color: #2196f3;
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points-info {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remaining {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 4px;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
border-radius: 2px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #4caf50;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-list, .spells-list, .selected-list {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item, .spell-item, .selected-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-item:last-child, .spell-item:last-child, .selected-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-info, .spell-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-name, .spell-name, .item-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-cost, .spell-cost, .item-cost {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-category {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn, .remove-btn {
|
||||||
|
padding: 5px 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn:hover:not(:disabled) {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn {
|
||||||
|
background-color: #f44336;
|
||||||
|
color: white;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-btn:hover {
|
||||||
|
background-color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-section {
|
||||||
|
grid-column: span 2;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-skills, .selected-spells {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-skills h4, .selected-spells h4 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-selection {
|
||||||
|
text-align: center;
|
||||||
|
color: #999;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn, .finalize-btn {
|
||||||
|
padding: 12px 30px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prev-btn:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finalize-btn {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.finalize-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
<template>
|
||||||
|
<div class="character-spells">
|
||||||
|
<h2>{{ $t('characters.create.spells.title') }}</h2>
|
||||||
|
<p class="subtitle">{{ $t('characters.create.spells.description') }}</p>
|
||||||
|
|
||||||
|
<!-- Spell Points Display -->
|
||||||
|
<div class="spell-points-display">
|
||||||
|
<div class="category-points">
|
||||||
|
<span class="category-name">{{ $t('characters.spells.zauber') }}</span>
|
||||||
|
<span class="points">{{ getZauberCategory()?.points || 0 }} / {{ getZauberCategory()?.max_points || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Available Spells -->
|
||||||
|
<div class="available-spells">
|
||||||
|
<h3>{{ $t('characters.create.spells.available') }}</h3>
|
||||||
|
<div class="spells-grid">
|
||||||
|
<div
|
||||||
|
v-for="spell in availableSpells"
|
||||||
|
:key="spell.name"
|
||||||
|
class="spell-card"
|
||||||
|
:class="{ 'can-add': canAddSpell(spell), 'cannot-add': !canAddSpell(spell) }"
|
||||||
|
@click="addSpell(spell)"
|
||||||
|
>
|
||||||
|
<div class="spell-name">{{ spell.name }}</div>
|
||||||
|
<div class="spell-cost">{{ $t('characters.skills.cost') }}: {{ spell.cost }}</div>
|
||||||
|
<div v-if="canAddSpell(spell)" class="add-button">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
</div>
|
||||||
|
<div v-else class="disabled-reason">
|
||||||
|
<span v-if="isSpellSelected(spell)">{{ $t('characters.skills.alreadySelected') }}</span>
|
||||||
|
<span v-else>{{ $t('characters.skills.notEnoughPoints') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selected Spells -->
|
||||||
|
<div class="selected-spells" v-if="selectedSpells.length > 0">
|
||||||
|
<h3>{{ $t('characters.create.spells.selected') }}</h3>
|
||||||
|
<div class="selected-list">
|
||||||
|
<div
|
||||||
|
v-for="spell in selectedSpells"
|
||||||
|
:key="spell.name"
|
||||||
|
class="selected-spell"
|
||||||
|
>
|
||||||
|
<span class="spell-name">{{ spell.name }}</span>
|
||||||
|
<span class="spell-cost">({{ spell.cost }})</span>
|
||||||
|
<button
|
||||||
|
@click="removeSpell(spell)"
|
||||||
|
class="remove-button"
|
||||||
|
:title="$t('characters.skills.remove')"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation Buttons -->
|
||||||
|
<div class="navigation-buttons">
|
||||||
|
<button @click="handlePrevious" class="btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i>
|
||||||
|
{{ $t('common.previous') }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button @click="handleFinalize" class="btn-primary">
|
||||||
|
{{ $t('characters.create.finalize') }}
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import API from '@/utils/api'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CharacterSpells',
|
||||||
|
|
||||||
|
props: {
|
||||||
|
sessionData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
skillCategories: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
emits: ['previous', 'finalize', 'save'],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
availableSpells: [],
|
||||||
|
selectedSpells: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.loadSpells()
|
||||||
|
this.loadSelectedSpells()
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
async loadSpells() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
|
||||||
|
const request = {
|
||||||
|
characterClass: this.sessionData.typ || 'Abenteurer',
|
||||||
|
characterId: '0', // Dummy for new character
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await API.post('/api/characters/available-spells-new', request, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
this.availableSpells = response.data.spells || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading spells:', error)
|
||||||
|
// Fallback dummy data
|
||||||
|
this.availableSpells = [
|
||||||
|
{ name: 'Licht', cost: 50, description: 'Erzeugt ein helles Licht' },
|
||||||
|
{ name: 'Magisches Geschoss', cost: 60, description: 'Feuert ein magisches Projektil ab' },
|
||||||
|
{ name: 'Schildzauber', cost: 70, description: 'Erzeugt einen magischen Schutzschild' },
|
||||||
|
{ name: 'Heilung', cost: 80, description: 'Heilt leichte Wunden' },
|
||||||
|
{ name: 'Unsichtbarkeit', cost: 120, description: 'Macht den Zauberer unsichtbar' },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadSelectedSpells() {
|
||||||
|
// Load from session data if available
|
||||||
|
if (this.sessionData.spells) {
|
||||||
|
this.selectedSpells = [...this.sessionData.spells]
|
||||||
|
this.updateSpellPoints()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getZauberCategory() {
|
||||||
|
return this.skillCategories.find(c => c.name === 'zauber')
|
||||||
|
},
|
||||||
|
|
||||||
|
canAddSpell(spell) {
|
||||||
|
const category = this.getZauberCategory()
|
||||||
|
const alreadySelected = this.isSpellSelected(spell)
|
||||||
|
|
||||||
|
return category && category.points >= spell.cost && !alreadySelected
|
||||||
|
},
|
||||||
|
|
||||||
|
isSpellSelected(spell) {
|
||||||
|
return this.selectedSpells.some(s => s.name === spell.name)
|
||||||
|
},
|
||||||
|
|
||||||
|
addSpell(spell) {
|
||||||
|
if (this.canAddSpell(spell)) {
|
||||||
|
this.selectedSpells.push({ ...spell })
|
||||||
|
this.updateSpellPoints()
|
||||||
|
this.saveData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
removeSpell(spell) {
|
||||||
|
const index = this.selectedSpells.findIndex(s => s.name === spell.name)
|
||||||
|
if (index >= 0) {
|
||||||
|
this.selectedSpells.splice(index, 1)
|
||||||
|
this.updateSpellPoints()
|
||||||
|
this.saveData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSpellPoints() {
|
||||||
|
const zauberCategory = this.getZauberCategory()
|
||||||
|
if (zauberCategory) {
|
||||||
|
// Reset to max points
|
||||||
|
zauberCategory.points = zauberCategory.max_points
|
||||||
|
|
||||||
|
// Deduct points for selected spells
|
||||||
|
this.selectedSpells.forEach(spell => {
|
||||||
|
zauberCategory.points -= spell.cost
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
saveData() {
|
||||||
|
const data = {
|
||||||
|
spells: this.selectedSpells,
|
||||||
|
spell_points: {
|
||||||
|
zauber: this.getZauberCategory()?.points || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('save', data)
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePrevious() {
|
||||||
|
this.saveData()
|
||||||
|
this.$emit('previous')
|
||||||
|
},
|
||||||
|
|
||||||
|
handleFinalize() {
|
||||||
|
const data = {
|
||||||
|
spells: this.selectedSpells,
|
||||||
|
spell_points: {
|
||||||
|
zauber: this.getZauberCategory()?.points || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('finalize', data)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.character-spells {
|
||||||
|
padding: 1rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-points-display {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-points {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.points {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.available-spells {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spells-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-card {
|
||||||
|
border: 2px solid #e9ecef;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: white;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-card.can-add {
|
||||||
|
border-color: #28a745;
|
||||||
|
background: #f8fff9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-card.can-add:hover {
|
||||||
|
border-color: #20c997;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-card.cannot-add {
|
||||||
|
border-color: #dc3545;
|
||||||
|
background: #fff5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spell-cost {
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
color: #28a745;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-reason {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
color: #dc3545;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-spells {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-spell {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border: 1px solid #bbdefb;
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-spell .spell-name {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-spell .spell-cost {
|
||||||
|
color: #1976d2;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #f44336;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-button:hover {
|
||||||
|
color: #d32f2f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary, .btn-secondary {
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #545b62;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.spells-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary, .btn-secondary {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,44 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2>Your Characters</h2>
|
<h2>Your Characters</h2>
|
||||||
|
|
||||||
|
<!-- Create New Character Button -->
|
||||||
|
<div class="create-character-section">
|
||||||
|
<button @click="createNewCharacter" class="create-btn">Create New Character</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Character Creation Sessions -->
|
||||||
|
<div v-if="creationSessions.length > 0" class="creation-sessions-section">
|
||||||
|
<h3>Continue Character Creation</h3>
|
||||||
|
<div class="sessions-grid">
|
||||||
|
<div
|
||||||
|
v-for="session in creationSessions"
|
||||||
|
:key="session.session_id"
|
||||||
|
class="session-card"
|
||||||
|
@click="continueSession(session.session_id)"
|
||||||
|
>
|
||||||
|
<div class="session-header">
|
||||||
|
<h4>{{ session.name || 'Unnamed Character' }}</h4>
|
||||||
|
<span class="session-progress">Step {{ session.current_step }}/{{ session.total_steps }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="session-details">
|
||||||
|
<p><strong>Race:</strong> {{ session.rasse || 'Not selected' }}</p>
|
||||||
|
<p><strong>Class:</strong> {{ session.typ || 'Not selected' }}</p>
|
||||||
|
<p><strong>Current step:</strong> {{ session.progress_text }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="session-meta">
|
||||||
|
<span class="last-updated">Last updated: {{ formatDate(session.updated_at) }}</span>
|
||||||
|
<span class="expires">Expires: {{ formatDate(session.expires_at) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="session-actions">
|
||||||
|
<button @click.stop="deleteSession(session.session_id)" class="delete-session-btn">
|
||||||
|
Delete Draft
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="character in characters" :key="character.character_id" style="white-space: nowrap; /* Prevent line breaks inside list items */;">
|
<li v-for="character in characters" :key="character.character_id" style="white-space: nowrap; /* Prevent line breaks inside list items */;">
|
||||||
<!-- Link to Character Details -->
|
<!-- Link to Character Details -->
|
||||||
@@ -19,19 +57,222 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
characters: [],
|
characters: [],
|
||||||
|
creationSessions: [],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
const token = localStorage.getItem('token')
|
await this.loadCharacters()
|
||||||
const response = await API.get('/api/characters', {
|
await this.loadCreationSessions()
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
this.characters = response.data
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async loadCharacters() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get('/api/characters', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
this.characters = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading characters:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadCreationSessions() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.get('/api/characters/create-sessions', {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
this.creationSessions = response.data.sessions || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading creation sessions:', error)
|
||||||
|
// Don't show error to user since this is a new feature
|
||||||
|
this.creationSessions = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
continueSession(sessionId) {
|
||||||
|
this.$router.push(`/character/create/${sessionId}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteSession(sessionId) {
|
||||||
|
if (confirm('Are you sure you want to delete this character draft?')) {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
await API.delete(`/api/characters/create-session/${sessionId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reload sessions after deletion
|
||||||
|
await this.loadCreationSessions()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting session:', error)
|
||||||
|
alert('Error deleting character draft')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
goToAusruestung(characterId) {
|
goToAusruestung(characterId) {
|
||||||
this.$router.push(`/api/ausruestung/${characterId}`)
|
this.$router.push(`/api/ausruestung/${characterId}`)
|
||||||
},
|
},
|
||||||
|
async createNewCharacter() {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await API.post('/api/characters/create-session', {}, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
const sessionId = response.data.session_id
|
||||||
|
this.$router.push(`/character/create/${sessionId}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating character session:', error)
|
||||||
|
alert('Fehler beim Erstellen der Charakter-Session')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(dateString) {
|
||||||
|
if (!dateString) return 'Unknown'
|
||||||
|
return new Date(dateString).toLocaleDateString()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.create-character-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 2px dashed #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.create-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-sessions-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creation-sessions-section h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sessions-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-card:hover {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-progress {
|
||||||
|
background: #007bff;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-details {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-details p {
|
||||||
|
margin: 5px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-updated,
|
||||||
|
.expires {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-session-btn {
|
||||||
|
background: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-session-btn:hover {
|
||||||
|
background: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.sessions-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -165,5 +165,29 @@ export default {
|
|||||||
silver_coins: 'Silberstücke',
|
silver_coins: 'Silberstücke',
|
||||||
copper_coins: 'Kupferstücke',
|
copper_coins: 'Kupferstücke',
|
||||||
total_in_gs: 'Gesamt in GS'
|
total_in_gs: 'Gesamt in GS'
|
||||||
|
},
|
||||||
|
characters: {
|
||||||
|
create: {
|
||||||
|
spells: {
|
||||||
|
title: 'Zauber auswählen',
|
||||||
|
description: 'Wähle magische Zauber für deinen Charakter',
|
||||||
|
available: 'Verfügbare Zauber',
|
||||||
|
selected: 'Ausgewählte Zauber'
|
||||||
|
},
|
||||||
|
finalize: 'Charakter erstellen'
|
||||||
|
},
|
||||||
|
spells: {
|
||||||
|
zauber: 'Zauber'
|
||||||
|
},
|
||||||
|
skills: {
|
||||||
|
cost: 'Kosten',
|
||||||
|
alreadySelected: 'Bereits ausgewählt',
|
||||||
|
notEnoughPoints: 'Nicht genug Punkte',
|
||||||
|
remove: 'Entfernen'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
previous: 'Zurück',
|
||||||
|
next: 'Weiter'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,5 +73,29 @@ export default {
|
|||||||
silver_coins: 'Silver Coins',
|
silver_coins: 'Silver Coins',
|
||||||
copper_coins: 'Copper Coins',
|
copper_coins: 'Copper Coins',
|
||||||
total_in_gs: 'Total in GS'
|
total_in_gs: 'Total in GS'
|
||||||
|
},
|
||||||
|
characters: {
|
||||||
|
create: {
|
||||||
|
spells: {
|
||||||
|
title: 'Select Spells',
|
||||||
|
description: 'Choose magical spells for your character',
|
||||||
|
available: 'Available Spells',
|
||||||
|
selected: 'Selected Spells'
|
||||||
|
},
|
||||||
|
finalize: 'Create Character'
|
||||||
|
},
|
||||||
|
spells: {
|
||||||
|
zauber: 'Spells'
|
||||||
|
},
|
||||||
|
skills: {
|
||||||
|
cost: 'Cost',
|
||||||
|
alreadySelected: 'Already selected',
|
||||||
|
notEnoughPoints: 'Not enough points',
|
||||||
|
remove: 'Remove'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
common: {
|
||||||
|
previous: 'Previous',
|
||||||
|
next: 'Next'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import MaintenanceView from "../views/MaintenanceView.vue";
|
|||||||
import FileUploadPage from "../views/FileUploadPage.vue";
|
import FileUploadPage from "../views/FileUploadPage.vue";
|
||||||
|
|
||||||
import CharacterDetails from "@/components/CharacterDetails.vue";
|
import CharacterDetails from "@/components/CharacterDetails.vue";
|
||||||
|
import CharacterCreation from "@/components/CharacterCreation.vue";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -21,6 +22,8 @@ const routes = [
|
|||||||
{ path: "/upload", name: "FileUpload", component: FileUploadPage },
|
{ path: "/upload", name: "FileUpload", component: FileUploadPage },
|
||||||
// Route for character details // Pass route params as props to the component
|
// Route for character details // Pass route params as props to the component
|
||||||
{ path: "/character/:id", name: "CharacterDetails", component: CharacterDetails, props: true, meta: { requiresAuth: true } },
|
{ path: "/character/:id", name: "CharacterDetails", component: CharacterDetails, props: true, meta: { requiresAuth: true } },
|
||||||
|
// Route for character creation
|
||||||
|
{ path: "/character/create/:sessionId", name: "CharacterCreation", component: CharacterCreation, props: true, meta: { requiresAuth: true } },
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
|
|||||||
Reference in New Issue
Block a user