OK, hope this works

This commit is contained in:
2025-12-29 18:13:24 +01:00
parent a46598c36c
commit 64abe4c985
4 changed files with 1095 additions and 0 deletions
+537
View File
@@ -0,0 +1,537 @@
package importer
import (
"bamort/models"
"encoding/csv"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
)
// ExportCharToVTT converts a Bamort character to VTT JSON format
func ExportCharToVTT(char *models.Char) (*CharacterImport, error) {
vtt := &CharacterImport{}
// Basic info
vtt.ID = fmt.Sprintf("bamort-character-%d", char.ID)
vtt.Name = char.Name
vtt.Rasse = char.Rasse
vtt.Typ = char.Typ
vtt.Alter = char.Alter
vtt.Anrede = char.Anrede
vtt.Grad = char.Grad
vtt.Groesse = char.Groesse
vtt.Gewicht = char.Gewicht
vtt.Glaube = char.Glaube
vtt.Hand = char.Hand
vtt.Image = char.Image
// LP
vtt.Lp.Max = char.Lp.Max
vtt.Lp.Value = char.Lp.Value
// AP
vtt.Ap.Max = char.Ap.Max
vtt.Ap.Value = char.Ap.Value
// B
vtt.B.Max = char.B.Max
vtt.B.Value = char.B.Value
// Eigenschaften - convert from array to struct
for _, e := range char.Eigenschaften {
switch e.Name {
case "Au":
vtt.Eigenschaften.Au = e.Value
case "Gs":
vtt.Eigenschaften.Gs = e.Value
case "Gw":
vtt.Eigenschaften.Gw = e.Value
case "In":
vtt.Eigenschaften.In = e.Value
case "Ko":
vtt.Eigenschaften.Ko = e.Value
case "PA":
vtt.Eigenschaften.Pa = e.Value
case "St":
vtt.Eigenschaften.St = e.Value
case "Wk":
vtt.Eigenschaften.Wk = e.Value
case "Zt":
vtt.Eigenschaften.Zt = e.Value
}
}
// Merkmale
vtt.Merkmale.Augenfarbe = char.Merkmale.Augenfarbe
vtt.Merkmale.Haarfarbe = char.Merkmale.Haarfarbe
vtt.Merkmale.Sonstige = char.Merkmale.Sonstige
// Gestalt
vtt.Gestalt.Breite = char.Merkmale.Breite
vtt.Gestalt.Groesse = char.Merkmale.Groesse
// Bennies
vtt.Bennies.Gg = char.Bennies.Gg
vtt.Bennies.Sg = char.Bennies.Sg
vtt.Bennies.Gp = char.Bennies.Gp
// Erfahrungsschatz
vtt.Erfahrungsschatz.Value = char.Erfahrungsschatz.ES
// Spezialisierung
vtt.Spezialisierung = char.Spezialisierung
// Fertigkeiten
for i, f := range char.Fertigkeiten {
vtt.Fertigkeiten = append(vtt.Fertigkeiten, Fertigkeit{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-skill-%d-%d", char.ID, i),
Name: f.Name,
},
Beschreibung: f.Beschreibung,
Fertigkeitswert: f.Fertigkeitswert,
Bonus: f.Bonus,
Pp: f.Pp,
Quelle: "", // Not stored in character skill
})
}
// Waffenfertigkeiten
for i, w := range char.Waffenfertigkeiten {
vtt.Waffenfertigkeiten = append(vtt.Waffenfertigkeiten, Waffenfertigkeit{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-weaponskill-%d-%d", char.ID, i),
Name: w.Name,
},
Beschreibung: w.Beschreibung,
Fertigkeitswert: w.Fertigkeitswert,
Bonus: w.Bonus,
Pp: w.Pp,
Quelle: "", // Not stored in character skill
})
}
// Zauber
for i, z := range char.Zauber {
vtt.Zauber = append(vtt.Zauber, Zauber{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-spell-%d-%d", char.ID, i),
Name: z.Name,
},
Beschreibung: z.Beschreibung,
Bonus: z.Bonus,
Quelle: z.Quelle,
})
}
// Waffen
for i, w := range char.Waffen {
vtt.Waffen = append(vtt.Waffen, Waffe{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-weapon-%d-%d", char.ID, i),
Name: w.Name,
},
Beschreibung: w.Beschreibung,
Gewicht: w.Gewicht,
Wert: w.Wert,
Anzahl: w.Anzahl,
Anb: w.Anb,
Schb: w.Schb,
Abwb: w.Abwb,
NameFuerSpezialisierung: w.NameFuerSpezialisierung,
BeinhaltetIn: fmt.Sprintf("bamort-container-%d", w.ContainedIn),
ContainedIn: w.ContainedIn,
Magisch: Magisch{
IstMagisch: w.IstMagisch,
Abw: w.Abw,
Ausgebrannt: w.Ausgebrannt,
},
})
}
// Ausrüstung
for i, a := range char.Ausruestung {
vtt.Ausruestung = append(vtt.Ausruestung, Ausruestung{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-equipment-%d-%d", char.ID, i),
Name: a.Name,
},
Beschreibung: a.Beschreibung,
Gewicht: a.Gewicht,
Wert: a.Wert,
Anzahl: a.Anzahl,
Bonus: a.Bonus,
BeinhaltetIn: fmt.Sprintf("bamort-container-%d", a.ContainedIn),
ContainedIn: a.ContainedIn,
Magisch: Magisch{
IstMagisch: a.IstMagisch,
Abw: a.Abw,
Ausgebrannt: a.Ausgebrannt,
},
})
}
// Behältnisse
for i, b := range char.Behaeltnisse {
vtt.Behaeltnisse = append(vtt.Behaeltnisse, Behaeltniss{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-container-%d-%d", char.ID, i),
Name: b.Name,
},
Beschreibung: b.Beschreibung,
Gewicht: b.Gewicht,
Wert: b.Wert,
Tragkraft: b.Tragkraft,
Volumen: b.Volumen,
BeinhaltetIn: fmt.Sprintf("bamort-container-%d", b.ContainedIn),
ContainedIn: b.ContainedIn,
Magisch: Magisch{
IstMagisch: b.IstMagisch,
Abw: b.Abw,
Ausgebrannt: b.Ausgebrannt,
},
})
}
// Transportmittel
for i, tm := range char.Transportmittel {
vtt.Transportmittel = append(vtt.Transportmittel, Transportation{
ImportBase: ImportBase{
ID: fmt.Sprintf("bamort-transport-%d-%d", char.ID, i),
Name: tm.Name,
},
Beschreibung: tm.Beschreibung,
Gewicht: int(tm.Gewicht),
Wert: tm.Wert,
Tragkraft: tm.Tragkraft,
BeinhaltetIn: fmt.Sprintf("bamort-container-%d", tm.ContainedIn),
ContainedIn: tm.ContainedIn,
Magisch: Magisch{
IstMagisch: tm.IstMagisch,
Abw: tm.Abw,
Ausgebrannt: tm.Ausgebrannt,
},
})
}
return vtt, nil
}
// ExportCharToVTTFile exports a character to VTT JSON file
func ExportCharToVTTFile(char *models.Char, filename string) error {
vtt, err := ExportCharToVTT(char)
if err != nil {
return err
}
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(vtt)
}
// ExportSpellsToCSV exports spell master data to CSV format
func ExportSpellsToCSV(spells []models.Spell, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
// Header
header := []string{
"Nr", "game_system", "name", "Beschreibung", "Quelle", "source_id",
"page_number", "bonus", "stufe", "ap", "Art", "Zauberdauer",
"Reichweite", "Wirkungsziel", "Wirkungsbereich", "Wirkungsdauer",
"Ursprung", "Category", "learning_category", "Agens", "Reagens",
"Material (Kosten)",
}
if err := writer.Write(header); err != nil {
return err
}
// Data rows
for i, spell := range spells {
row := []string{
strconv.Itoa(i + 1),
spell.GameSystem,
spell.Name,
spell.Beschreibung,
spell.Quelle,
strconv.Itoa(int(spell.SourceID)),
strconv.Itoa(spell.PageNumber),
strconv.Itoa(spell.Bonus),
strconv.Itoa(spell.Stufe),
spell.AP,
spell.Art,
spell.Zauberdauer,
spell.Reichweite,
spell.Wirkungsziel,
spell.Wirkungsbereich,
spell.Wirkungsdauer,
spell.Ursprung,
spell.Category,
spell.LearningCategory,
"", // Agens - not in current model
"", // Reagens - not in current model
"", // Material - not in current model
}
if err := writer.Write(row); err != nil {
return err
}
}
return nil
}
// ExportCharToCSV exports a character to CSV format (MOAM-compatible)
func ExportCharToCSV(char *models.Char, filename string) error {
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()
writer := csv.NewWriter(file)
writer.Comma = ';'
defer writer.Flush()
// Helper to write a row
writeRow := func(fields ...string) error {
return writer.Write(fields)
}
// Header: Name, Typ, Grad, Stand, Glaube, Herkunft
if err := writeRow("Name", "Typ", "Grad", "Stand", "Glaube", "Herkunft"); err != nil {
return err
}
stand := ""
herkunft := ""
if err := writeRow(char.Name, char.Typ, strconv.Itoa(char.Grad), stand, char.Glaube, herkunft); err != nil {
return err
}
// Spezialisierung
if err := writeRow("Spezialisierung", strings.Join(char.Spezialisierung, ";")); err != nil {
return err
}
if err := writeRow(""); err != nil { // Empty line
return err
}
// Basiseigenschaften
if err := writeRow("Basiseigenschaften"); err != nil {
return err
}
eigenschaftenMap := make(map[string]int)
for _, e := range char.Eigenschaften {
eigenschaftenMap[e.Name] = e.Value
}
if err := writeRow("St", strconv.Itoa(eigenschaftenMap["St"])); err != nil {
return err
}
if err := writeRow("Gs", strconv.Itoa(eigenschaftenMap["Gs"])); err != nil {
return err
}
if err := writeRow("Gw", strconv.Itoa(eigenschaftenMap["Gw"])); err != nil {
return err
}
if err := writeRow("Ko", strconv.Itoa(eigenschaftenMap["Ko"])); err != nil {
return err
}
if err := writeRow("In", strconv.Itoa(eigenschaftenMap["In"])); err != nil {
return err
}
if err := writeRow("Zt", strconv.Itoa(eigenschaftenMap["Zt"])); err != nil {
return err
}
if err := writeRow("Au", strconv.Itoa(eigenschaftenMap["Au"])); err != nil {
return err
}
if err := writeRow("pA", strconv.Itoa(eigenschaftenMap["PA"])); err != nil {
return err
}
if err := writeRow("Wk", strconv.Itoa(eigenschaftenMap["Wk"])); err != nil {
return err
}
// LP, AP, B, SchB, AbB, AnB
if err := writeRow("LP", "AP", "B", "SchB", "AbB", "AnB"); err != nil {
return err
}
if err := writeRow(
strconv.Itoa(char.Lp.Max),
strconv.Itoa(char.Ap.Max),
strconv.Itoa(char.B.Max),
"0", // SchB - not in model
"0", // AbB - not in model
"0", // AnB - not in model
); err != nil {
return err
}
if err := writeRow(""); err != nil {
return err
}
// Raufen, Abwehr (not in current model, use defaults)
if err := writeRow("Raufen", "Abwehr"); err != nil {
return err
}
if err := writeRow("7", "14"); err != nil {
return err
}
// Resistenz
if err := writeRow("Resistenz Geist", "Resistenz Körper"); err != nil {
return err
}
if err := writeRow("0", "0"); err != nil {
return err
}
if err := writeRow("Bonus Resistenz Geist", "Bonus Resistenz Körper"); err != nil {
return err
}
if err := writeRow("0", "0"); err != nil {
return err
}
if err := writeRow(""); err != nil {
return err
}
// Waffen
if err := writeRow("Waffe", "Erfolgswert", "Angriffsbonus", "Schadensbonus", "Abwehrbonus", "Praxispunkte"); err != nil {
return err
}
for _, w := range char.Waffen {
// Find corresponding weapon skill
erfolgswert := 0
for _, ws := range char.Waffenfertigkeiten {
if strings.Contains(w.Name, ws.Name) || strings.Contains(ws.Name, w.Name) {
erfolgswert = ws.Fertigkeitswert
break
}
}
if err := writeRow(
w.Name,
strconv.Itoa(erfolgswert),
strconv.Itoa(w.Anb),
strconv.Itoa(w.Schb),
strconv.Itoa(w.Abwb),
"0",
); err != nil {
return err
}
}
if err := writeRow(""); err != nil {
return err
}
// Rüstung (armor from equipment)
if err := writeRow("Rüstung", "RK", "Rüstungsbonus"); err != nil {
return err
}
for _, a := range char.Ausruestung {
if strings.Contains(strings.ToLower(a.Name), "rüstung") || strings.Contains(strings.ToLower(a.Name), "armor") {
if err := writeRow(a.Name, strconv.Itoa(a.Bonus), "0"); err != nil {
return err
}
}
}
if err := writeRow(""); err != nil {
return err
}
// Fertigkeiten
if err := writeRow("Fertigkeit", "Erfolgswert", "Bonus", "Praxispunkte"); err != nil {
return err
}
for _, f := range char.Fertigkeiten {
desc := f.Beschreibung
if desc != "" {
desc = " (" + desc + ")"
}
if err := writeRow(
f.Name+desc,
strconv.Itoa(f.Fertigkeitswert),
strconv.Itoa(f.Bonus),
strconv.Itoa(f.Pp),
); err != nil {
return err
}
}
if err := writeRow(""); err != nil {
return err
}
// Zauber section (if character has spells)
if len(char.Zauber) > 0 {
if err := writeRow("Zaubern", "ZauB"); err != nil {
return err
}
if err := writeRow("0", "0"); err != nil { // Placeholder values
return err
}
if err := writeRow("Zauber", "Bonus", "Praxispunkte"); err != nil {
return err
}
for _, z := range char.Zauber {
if err := writeRow(z.Name, strconv.Itoa(z.Bonus), "0"); err != nil {
return err
}
}
if err := writeRow(""); err != nil {
return err
}
}
// Ausrüstung (equipment list)
if err := writeRow("Ausrüstung"); err != nil {
return err
}
var equipmentNames []string
for _, a := range char.Ausruestung {
equipmentNames = append(equipmentNames, a.Name)
}
for _, b := range char.Behaeltnisse {
equipmentNames = append(equipmentNames, b.Name)
}
for _, t := range char.Transportmittel {
equipmentNames = append(equipmentNames, t.Name)
}
if len(equipmentNames) > 0 {
if err := writeRow(strings.Join(equipmentNames, ";")); err != nil {
return err
}
}
if err := writeRow(""); err != nil {
return err
}
// Erfahrung
if err := writeRow("Erfahrung"); err != nil {
return err
}
if err := writeRow("Erfahrungsschatz", "EP", "Gold"); err != nil {
return err
}
if err := writeRow(
strconv.Itoa(char.Erfahrungsschatz.ES),
"0", // EP not in model
"0", // Gold not in model
); err != nil {
return err
}
return nil
}
+339
View File
@@ -0,0 +1,339 @@
package importer
import (
"bamort/database"
"bamort/models"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestExportChar2VTT(t *testing.T) {
database.SetupTestDB()
defer database.ResetTestDB()
// Import a test character first
fileName := fmt.Sprintf("../testdata/%s", "VTT_Import1.json")
char, err := ImportVTTJSON(fileName)
assert.NoError(t, err, "Expected no error when importing char")
// Export the character back to VTT format
exportedChar, err := ExportCharToVTT(char)
assert.NoError(t, err, "Expected no error when exporting char")
// Basic validations
assert.Equal(t, char.Name, exportedChar.Name)
assert.Equal(t, char.Rasse, exportedChar.Rasse)
assert.Equal(t, char.Typ, exportedChar.Typ)
assert.Equal(t, char.Alter, exportedChar.Alter)
assert.Equal(t, char.Grad, exportedChar.Grad)
assert.Equal(t, char.Groesse, exportedChar.Groesse)
assert.Equal(t, char.Gewicht, exportedChar.Gewicht)
assert.Equal(t, char.Glaube, exportedChar.Glaube)
assert.Equal(t, char.Hand, exportedChar.Hand)
// Check LP
assert.Equal(t, char.Lp.Max, exportedChar.Lp.Max)
assert.Equal(t, char.Lp.Value, exportedChar.Lp.Value)
// Check AP
assert.Equal(t, char.Ap.Max, exportedChar.Ap.Max)
assert.Equal(t, char.Ap.Value, exportedChar.Ap.Value)
// Check Eigenschaften
eigenschaftenMap := getEigenschaftenMap(char)
assert.Equal(t, eigenschaftenMap["Au"], exportedChar.Eigenschaften.Au)
assert.Equal(t, eigenschaftenMap["Gs"], exportedChar.Eigenschaften.Gs)
assert.Equal(t, eigenschaftenMap["Gw"], exportedChar.Eigenschaften.Gw)
assert.Equal(t, eigenschaftenMap["In"], exportedChar.Eigenschaften.In)
assert.Equal(t, eigenschaftenMap["Ko"], exportedChar.Eigenschaften.Ko)
assert.Equal(t, eigenschaftenMap["PA"], exportedChar.Eigenschaften.Pa)
assert.Equal(t, eigenschaftenMap["St"], exportedChar.Eigenschaften.St)
assert.Equal(t, eigenschaftenMap["Wk"], exportedChar.Eigenschaften.Wk)
assert.Equal(t, eigenschaftenMap["Zt"], exportedChar.Eigenschaften.Zt)
// Check Fertigkeiten exist
assert.Greater(t, len(exportedChar.Fertigkeiten), 0, "Should have fertigkeiten")
// Check Waffenfertigkeiten exist
assert.Greater(t, len(exportedChar.Waffenfertigkeiten), 0, "Should have waffenfertigkeiten")
}
func TestExportChar2VTTRoundTrip(t *testing.T) {
database.SetupTestDB()
defer database.ResetTestDB()
// Import original
fileName := fmt.Sprintf("../testdata/%s", "VTT_Import1.json")
char1, err := ImportVTTJSON(fileName)
assert.NoError(t, err, "Expected no error when importing char")
// Export to VTT
exportedChar, err := ExportCharToVTT(char1)
assert.NoError(t, err, "Expected no error when exporting char")
// Write to temp file
tempFile, err := os.CreateTemp("", "vtt_export_*.json")
assert.NoError(t, err, "Expected no error creating temp file")
defer os.Remove(tempFile.Name())
encoder := json.NewEncoder(tempFile)
encoder.SetIndent("", " ")
err = encoder.Encode(exportedChar)
assert.NoError(t, err, "Expected no error encoding JSON")
tempFile.Close()
// Re-import the exported file
char2, err := ImportVTTJSON(tempFile.Name())
assert.NoError(t, err, "Expected no error when re-importing char")
// Compare key fields
assert.Equal(t, char1.Name, char2.Name)
assert.Equal(t, char1.Rasse, char2.Rasse)
assert.Equal(t, char1.Typ, char2.Typ)
assert.Equal(t, char1.Alter, char2.Alter)
assert.Equal(t, char1.Grad, char2.Grad)
assert.Equal(t, char1.Lp.Max, char2.Lp.Max)
assert.Equal(t, char1.Ap.Max, char2.Ap.Max)
}
// Helper function to convert char eigenschaften array to map
func getEigenschaftenMap(char *models.Char) map[string]int {
m := make(map[string]int)
for _, e := range char.Eigenschaften {
m[e.Name] = e.Value
}
return m
}
func TestExportSpellsToCSV(t *testing.T) {
database.SetupTestDB()
defer database.ResetTestDB()
// Get some spells from master data
var spells []models.Spell
database.DB.Limit(10).Find(&spells)
if len(spells) == 0 {
t.Skip("No spells in test database")
}
// Export to CSV
tempFile, err := os.CreateTemp("", "spell_export_*.csv")
assert.NoError(t, err, "Expected no error creating temp file")
defer os.Remove(tempFile.Name())
tempFile.Close()
err = ExportSpellsToCSV(spells, tempFile.Name())
assert.NoError(t, err, "Expected no error exporting spells to CSV")
// Verify file exists and has content
data, err := os.ReadFile(tempFile.Name())
assert.NoError(t, err, "Expected no error reading CSV file")
assert.Greater(t, len(data), 0, "CSV file should have content")
// Verify CSV has header
content := string(data)
assert.Contains(t, content, "game_system")
assert.Contains(t, content, "name")
assert.Contains(t, content, "Beschreibung")
}
func TestExportCharToCSV(t *testing.T) {
database.SetupTestDB()
defer database.ResetTestDB()
// Import a test character first
fileName := fmt.Sprintf("../testdata/%s", "VTT_Import1.json")
char, err := ImportVTTJSON(fileName)
assert.NoError(t, err, "Expected no error when importing char")
// Export to CSV
tempFile, err := os.CreateTemp("", "char_export_*.csv")
assert.NoError(t, err, "Expected no error creating temp file")
defer os.Remove(tempFile.Name())
tempFile.Close()
err = ExportCharToCSV(char, tempFile.Name())
assert.NoError(t, err, "Expected no error exporting character to CSV")
// Verify file exists and has content
data, err := os.ReadFile(tempFile.Name())
assert.NoError(t, err, "Expected no error reading CSV file")
assert.Greater(t, len(data), 0, "CSV file should have content")
// Verify CSV has expected sections
content := string(data)
assert.Contains(t, content, char.Name, "Should contain character name")
assert.Contains(t, content, "Basiseigenschaften", "Should contain base attributes section")
assert.Contains(t, content, "Fertigkeit", "Should contain skills section")
assert.Contains(t, content, "Waffe", "Should contain weapons section")
assert.Contains(t, content, "Erfahrung", "Should contain experience section")
}
func TestExportImportWithoutMasterData(t *testing.T) {
database.SetupTestDB()
defer database.ResetTestDB()
// Import a test character first
fileName := fmt.Sprintf("../testdata/%s", "VTT_Import1.json")
char1, err := ImportVTTJSON(fileName)
assert.NoError(t, err, "Expected no error when importing char")
// Export to VTT
vttChar, err := ExportCharToVTT(char1)
assert.NoError(t, err, "Expected no error when exporting char")
// Write to temp file
tempFile, err := os.CreateTemp("", "vtt_export_*.json")
assert.NoError(t, err, "Expected no error creating temp file")
defer os.Remove(tempFile.Name())
encoder := json.NewEncoder(tempFile)
encoder.SetIndent("", " ")
err = encoder.Encode(vttChar)
assert.NoError(t, err, "Expected no error encoding JSON")
tempFile.Close()
// Clear all master data tables
database.DB.Exec("DELETE FROM gsm_skills")
database.DB.Exec("DELETE FROM gsm_weaponskills")
database.DB.Exec("DELETE FROM gsm_spells")
database.DB.Exec("DELETE FROM gsm_weapons")
database.DB.Exec("DELETE FROM gsm_equipments")
database.DB.Exec("DELETE FROM gsm_containers")
database.DB.Exec("DELETE FROM gsm_transportations")
database.DB.Exec("DELETE FROM gsm_believes")
database.DB.Exec("DELETE FROM sqlite_sequence WHERE name LIKE 'gsm_%'")
// Re-import without master data
char2, err := ImportVTTJSON(tempFile.Name())
assert.NoError(t, err, "Expected no error when re-importing without master data")
assert.NotNil(t, char2, "Character should be imported")
// Verify critical data was preserved
assert.Equal(t, char1.Name, char2.Name, "Name should match")
assert.Equal(t, char1.Rasse, char2.Rasse, "Race should match")
assert.Equal(t, char1.Typ, char2.Typ, "Type should match")
assert.Equal(t, char1.Grad, char2.Grad, "Grade should match")
// Verify LP/AP
assert.Equal(t, char1.Lp.Max, char2.Lp.Max, "LP Max should match")
assert.Equal(t, char1.Ap.Max, char2.Ap.Max, "AP Max should match")
// Verify skills were imported
assert.Greater(t, len(char2.Fertigkeiten), 0, "Should have skills after reimport")
assert.Greater(t, len(char2.Waffenfertigkeiten), 0, "Should have weapon skills after reimport")
// Verify weapons were imported
assert.Greater(t, len(char2.Waffen), 0, "Should have weapons after reimport")
// Verify master data was created
var skillCount, weaponSkillCount, weaponCount int64
database.DB.Model(&models.Skill{}).Count(&skillCount)
database.DB.Model(&models.WeaponSkill{}).Count(&weaponSkillCount)
database.DB.Model(&models.Weapon{}).Count(&weaponCount)
assert.Greater(t, skillCount, int64(0), "Master data should be created for skills")
assert.Greater(t, weaponSkillCount, int64(0), "Master data should be created for weapon skills")
assert.Greater(t, weaponCount, int64(0), "Master data should be created for weapons")
}
func TestExportImportPreservesCharacterData(t *testing.T) {
database.SetupTestDB()
defer database.ResetTestDB()
// Import a test character
fileName := fmt.Sprintf("../testdata/%s", "VTT_Import1.json")
char1, err := ImportVTTJSON(fileName)
assert.NoError(t, err, "Expected no error when importing char")
// Store original counts and values
originalSkillCount := len(char1.Fertigkeiten)
originalWeaponSkillCount := len(char1.Waffenfertigkeiten)
originalSpellCount := len(char1.Zauber)
originalWeaponCount := len(char1.Waffen)
originalEquipmentCount := len(char1.Ausruestung)
originalContainerCount := len(char1.Behaeltnisse)
originalTransportCount := len(char1.Transportmittel)
// Export to VTT
vttChar, err := ExportCharToVTT(char1)
assert.NoError(t, err, "Expected no error when exporting char")
// Verify export has all data
assert.Equal(t, originalSkillCount, len(vttChar.Fertigkeiten), "All skills should be exported")
assert.Equal(t, originalWeaponSkillCount, len(vttChar.Waffenfertigkeiten), "All weapon skills should be exported")
assert.Equal(t, originalSpellCount, len(vttChar.Zauber), "All spells should be exported")
assert.Equal(t, originalWeaponCount, len(vttChar.Waffen), "All weapons should be exported")
assert.Equal(t, originalEquipmentCount, len(vttChar.Ausruestung), "All equipment should be exported")
assert.Equal(t, originalContainerCount, len(vttChar.Behaeltnisse), "All containers should be exported")
assert.Equal(t, originalTransportCount, len(vttChar.Transportmittel), "All transportation should be exported")
// Verify specific skill data is preserved
if originalSkillCount > 0 {
assert.NotEmpty(t, vttChar.Fertigkeiten[0].Name, "Skill name should be exported")
assert.GreaterOrEqual(t, vttChar.Fertigkeiten[0].Fertigkeitswert, 0, "Skill value should be exported")
}
// Verify weapon data is preserved
if originalWeaponCount > 0 {
assert.NotEmpty(t, vttChar.Waffen[0].Name, "Weapon name should be exported")
assert.GreaterOrEqual(t, vttChar.Waffen[0].Gewicht, float64(0), "Weapon weight should be exported")
}
// Write to temp file
tempFile, err := os.CreateTemp("", "vtt_export_*.json")
assert.NoError(t, err, "Expected no error creating temp file")
defer os.Remove(tempFile.Name())
encoder := json.NewEncoder(tempFile)
encoder.SetIndent("", " ")
err = encoder.Encode(vttChar)
assert.NoError(t, err, "Expected no error encoding JSON")
tempFile.Close()
// Clear master data
database.DB.Exec("DELETE FROM gsm_skills")
database.DB.Exec("DELETE FROM gsm_weaponskills")
database.DB.Exec("DELETE FROM gsm_spells")
database.DB.Exec("DELETE FROM gsm_weapons")
database.DB.Exec("DELETE FROM gsm_equipments")
database.DB.Exec("DELETE FROM gsm_containers")
database.DB.Exec("DELETE FROM gsm_transportations")
// Re-import
char2, err := ImportVTTJSON(tempFile.Name())
assert.NoError(t, err, "Expected no error when re-importing")
// Verify all data was preserved
assert.Equal(t, originalSkillCount, len(char2.Fertigkeiten), "All skills should be reimported")
assert.Equal(t, originalWeaponSkillCount, len(char2.Waffenfertigkeiten), "All weapon skills should be reimported")
assert.Equal(t, originalSpellCount, len(char2.Zauber), "All spells should be reimported")
assert.Equal(t, originalWeaponCount, len(char2.Waffen), "All weapons should be reimported")
assert.Equal(t, originalEquipmentCount, len(char2.Ausruestung), "All equipment should be reimported")
assert.Equal(t, originalContainerCount, len(char2.Behaeltnisse), "All containers should be reimported")
assert.Equal(t, originalTransportCount, len(char2.Transportmittel), "All transportation should be reimported")
// Verify specific values match
if originalSkillCount > 0 {
skill1 := findSkillByName(char1.Fertigkeiten, char1.Fertigkeiten[0].Name)
skill2 := findSkillByName(char2.Fertigkeiten, char1.Fertigkeiten[0].Name)
assert.NotNil(t, skill1, "Original skill should exist")
assert.NotNil(t, skill2, "Reimported skill should exist")
assert.Equal(t, skill1.Fertigkeitswert, skill2.Fertigkeitswert, "Skill values should match")
}
}
// Helper function to find skill by name
func findSkillByName(skills []models.SkFertigkeit, name string) *models.SkFertigkeit {
for i := range skills {
if skills[i].Name == name {
return &skills[i]
}
}
return nil
}
+210
View File
@@ -200,3 +200,213 @@ func ImportSpellCSVHandler(c *gin.Context) {
"total_spells": spellCount,
})
}
// ExportCharacterVTTHandler exports a character to VTT JSON format
// @Summary Export character to VTT JSON format
// @Description Exports a character to VTT JSON format for use in other systems
// @Tags importer
// @Produce json
// @Param id path int true "Character ID"
// @Success 200 {object} CharacterImport "Export successful"
// @Failure 400 {object} map[string]interface{} "Bad request - invalid character ID"
// @Failure 404 {object} map[string]interface{} "Character not found"
// @Failure 500 {object} map[string]interface{} "Internal server error - export failed"
// @Router /api/importer/export/vtt/{id} [get]
func ExportCharacterVTTHandler(c *gin.Context) {
// Get character ID from URL parameter
charID := c.Param("id")
if charID == "" {
respondWithError(c, http.StatusBadRequest, "Character ID is required")
return
}
// Load character from database
var char models.Char
err := database.DB.Preload("Eigenschaften").
Preload("Fertigkeiten").
Preload("Waffenfertigkeiten").
Preload("Zauber").
Preload("Waffen").
Preload("Ausruestung").
Preload("Behaeltnisse").
Preload("Transportmittel").
First(&char, charID).Error
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Export to VTT format
vttChar, err := ExportCharToVTT(&char)
if err != nil {
respondWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to export character: %s", err.Error()))
return
}
// Return as JSON
c.JSON(http.StatusOK, vttChar)
}
// ExportCharacterVTTFileHandler exports a character to VTT JSON file
// @Summary Export character to VTT JSON file
// @Description Exports a character to VTT JSON file and returns it as a download
// @Tags importer
// @Produce json
// @Param id path int true "Character ID"
// @Success 200 {file} file "VTT JSON file"
// @Failure 400 {object} map[string]interface{} "Bad request - invalid character ID"
// @Failure 404 {object} map[string]interface{} "Character not found"
// @Failure 500 {object} map[string]interface{} "Internal server error - export failed"
// @Router /api/importer/export/vtt/{id}/file [get]
func ExportCharacterVTTFileHandler(c *gin.Context) {
// Get character ID from URL parameter
charID := c.Param("id")
if charID == "" {
respondWithError(c, http.StatusBadRequest, "Character ID is required")
return
}
// Load character from database
var char models.Char
err := database.DB.Preload("Eigenschaften").
Preload("Fertigkeiten").
Preload("Waffenfertigkeiten").
Preload("Zauber").
Preload("Waffen").
Preload("Ausruestung").
Preload("Behaeltnisse").
Preload("Transportmittel").
First(&char, charID).Error
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Create temp file
tempFile, err := os.CreateTemp("", fmt.Sprintf("vtt_export_%s_*.json", char.Name))
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create temp file")
return
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Export to file
err = ExportCharToVTTFile(&char, tempFile.Name())
if err != nil {
respondWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to export character: %s", err.Error()))
return
}
// Send file as download
filename := fmt.Sprintf("%s_vtt_export.json", char.Name)
c.FileAttachment(tempFile.Name(), filename)
}
// ExportSpellsCSVHandler exports spell master data to CSV file
// @Summary Export spells to CSV file
// @Description Exports spell master data to CSV format
// @Tags importer
// @Produce text/csv
// @Param game_system query string false "Game system filter (e.g., 'midgard')"
// @Success 200 {file} file "CSV file"
// @Failure 500 {object} map[string]interface{} "Internal server error - export failed"
// @Router /api/importer/export/spells/csv [get]
func ExportSpellsCSVHandler(c *gin.Context) {
gameSystem := c.Query("game_system")
// Load spells from database
var spells []models.Spell
query := database.DB
if gameSystem != "" {
query = query.Where("game_system = ?", gameSystem)
}
err := query.Find(&spells).Error
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to load spells")
return
}
// Create temp file
tempFile, err := os.CreateTemp("", "spells_export_*.csv")
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create temp file")
return
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Export to CSV
err = ExportSpellsToCSV(spells, tempFile.Name())
if err != nil {
respondWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to export spells: %s", err.Error()))
return
}
// Send file as download
filename := "spells_export.csv"
if gameSystem != "" {
filename = fmt.Sprintf("spells_%s_export.csv", gameSystem)
}
c.FileAttachment(tempFile.Name(), filename)
}
// ExportCharacterCSVHandler exports a character to CSV file
// @Summary Export character to CSV file
// @Description Exports a character to CSV format (MOAM-compatible)
// @Tags importer
// @Produce text/csv
// @Param id path int true "Character ID"
// @Success 200 {file} file "CSV file"
// @Failure 400 {object} map[string]interface{} "Bad request - invalid character ID"
// @Failure 404 {object} map[string]interface{} "Character not found"
// @Failure 500 {object} map[string]interface{} "Internal server error - export failed"
// @Router /api/importer/export/csv/{id} [get]
func ExportCharacterCSVHandler(c *gin.Context) {
// Get character ID from URL parameter
charID := c.Param("id")
if charID == "" {
respondWithError(c, http.StatusBadRequest, "Character ID is required")
return
}
// Load character from database
var char models.Char
err := database.DB.Preload("Eigenschaften").
Preload("Fertigkeiten").
Preload("Waffenfertigkeiten").
Preload("Zauber").
Preload("Waffen").
Preload("Ausruestung").
Preload("Behaeltnisse").
Preload("Transportmittel").
First(&char, charID).Error
if err != nil {
respondWithError(c, http.StatusNotFound, "Character not found")
return
}
// Create temp file
tempFile, err := os.CreateTemp("", fmt.Sprintf("csv_export_%s_*.csv", char.Name))
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create temp file")
return
}
defer os.Remove(tempFile.Name())
tempFile.Close()
// Export to CSV
err = ExportCharToCSV(&char, tempFile.Name())
if err != nil {
respondWithError(c, http.StatusInternalServerError, fmt.Sprintf("Failed to export character: %s", err.Error()))
return
}
// Send file as download
filename := fmt.Sprintf("%s_export.csv", char.Name)
c.FileAttachment(tempFile.Name(), filename)
}
+9
View File
@@ -6,6 +6,15 @@ import (
func RegisterRoutes(r *gin.RouterGroup) {
charGrp := r.Group("/importer")
// Import routes
charGrp.POST("/upload", UploadFiles)
charGrp.POST("/spells/csv", ImportSpellCSVHandler)
// Export routes
exportGrp := charGrp.Group("/export")
exportGrp.GET("/vtt/:id", ExportCharacterVTTHandler)
exportGrp.GET("/vtt/:id/file", ExportCharacterVTTFileHandler)
exportGrp.GET("/csv/:id", ExportCharacterCSVHandler)
exportGrp.GET("/spells/csv", ExportSpellsCSVHandler)
}