Merge pull request #14 from Bardioc26/skills_improvement

Improve maintenance dialogs and fixed some minor bugs
added an exporting/importing for Game system master data
This commit is contained in:
Bardioc26
2026-01-07 15:35:48 +01:00
committed by GitHub
19 changed files with 3748 additions and 52 deletions
+1
View File
@@ -35,6 +35,7 @@
dbURL = "bamort:bG4)efozrc@tcp(192.168.0.5:3306)/bamort?charset=utf8mb4&parseTime=True&loc=Local"
* fehlgeschlagene Tests ausbessern
./backend$ go test ./... -v 2>&1 |grep FAIL
* API endpunkte für Export/Import aus Commit 2dcb4e00faaf316b98eb28e83cc5137bf0d1385d
## Refaktor
+216
View File
@@ -0,0 +1,216 @@
# Master Data Export/Import
## Overview
The export/import mechanism allows exporting all master data from the `model_gsmaster` and `model_learning_costs` modules to JSON files and importing them back. The exported data is ID-independent, using natural keys (name + game_system) to identify records, making it suitable for:
- Migrating data between environments
- Version controlling master data
- Manually editing game data
- Sharing/distributing game systems
## Supported Entities
### From `model_learning_costs.go`:
- **Sources** (`learning_sources`) - Game books and source materials
- **SkillCategories** (`learning_skill_categories`) - Skill classification categories
- **SkillDifficulties** (`learning_skill_difficulties`) - Difficulty levels
- **SkillCategoryDifficulties** (`learning_skill_category_difficulties`) - Relationship between skills, categories, and difficulties with learning costs
### From `model_gsmaster.go`:
- **Skills** (`gsm_skills`) - Character skills
- **Spells** (`gsm_spells`) - Magic spells
## Excluded Entities
The following are NOT exported/imported:
- `AuditLogEntry` - Audit logs (transient data)
- `BamortBase` - Base character data (character-specific)
- `BamortCharTrait` - Character traits (character-specific)
- `Magisch` - Character magic data (character-specific)
- `LookupList` - System lookup lists (system config)
## File Format
All data is exported to JSON files with indentation for easy reading and editing:
```json
[
{
"name": "Skill Name",
"game_system": "midgard",
"source_code": "KOD",
...
}
]
```
## Natural Keys
Instead of database IDs, the following natural keys are used to identify records:
| Entity | Natural Key |
|--------|------------|
| Source | `code` |
| Skill | `name` + `game_system` |
| Spell | `name` + `game_system` |
| SkillCategory | `name` + `game_system` |
| SkillDifficulty | `name` + `game_system` |
| SkillCategoryDifficulty | `skill_name` + `skill_system` + `category_name` + `category_system` + `difficulty_name` + `difficulty_system` |
## Usage
### Export
```go
import "bamort/gsmaster"
// Export specific entity types
err := gsmaster.ExportSources("/path/to/output")
err := gsmaster.ExportSkills("/path/to/output")
err := gsmaster.ExportSpells("/path/to/output")
err := gsmaster.ExportSkillCategories("/path/to/output")
err := gsmaster.ExportSkillDifficulties("/path/to/output")
err := gsmaster.ExportSkillCategoryDifficulties("/path/to/output")
// Export all master data at once
err := gsmaster.ExportAll("/path/to/output")
```
### Import
```go
import "bamort/gsmaster"
// Import specific entity types
err := gsmaster.ImportSources("/path/to/input")
err := gsmaster.ImportSkills("/path/to/input")
err := gsmaster.ImportSpells("/path/to/input")
err := gsmaster.ImportSkillCategories("/path/to/input")
err := gsmaster.ImportSkillDifficulties("/path/to/input")
err := gsmaster.ImportSkillCategoryDifficulties("/path/to/input")
// Import all master data at once
err := gsmaster.ImportAll("/path/to/input")
```
## Import Behavior
The import mechanism follows an "upsert" pattern:
1. **Check if record exists** using natural keys
2. If **not found**: Create new record
3. If **found**: Update existing record with imported values
This allows for:
- Importing new data
- Updating existing data
- Safe re-import of previously exported data
## Dependency Order
When using `ExportAll()` and `ImportAll()`, entities are processed in dependency order:
**Export/Import Order:**
1. Sources (no dependencies)
2. SkillCategories (depends on Sources)
3. SkillDifficulties (no dependencies)
4. Skills (depends on Sources)
5. SkillCategoryDifficulties (depends on Skills, Categories, Difficulties)
6. Spells (depends on Sources)
## File Names
Each entity type is exported to its own file:
| Entity | Filename |
|--------|----------|
| Sources | `sources.json` |
| Skills | `skills.json` |
| Spells | `spells.json` |
| SkillCategories | `skill_categories.json` |
| SkillDifficulties | `skill_difficulties.json` |
| SkillCategoryDifficulties | `skill_category_difficulties.json` |
## Example Workflow
### Exporting Data
```go
package main
import (
"bamort/gsmaster"
"log"
)
func main() {
outputDir := "./exported_data"
if err := gsmaster.ExportAll(outputDir); err != nil {
log.Fatalf("Export failed: %v", err)
}
log.Println("All master data exported to", outputDir)
}
```
### Editing Exported Data
```bash
# Edit the JSON files manually
vim exported_data/skills.json
```
### Importing Modified Data
```go
package main
import (
"bamort/gsmaster"
"bamort/database"
"log"
)
func main() {
// Initialize database connection
database.InitDB()
inputDir := "./exported_data"
if err := gsmaster.ImportAll(inputDir); err != nil {
log.Fatalf("Import failed: %v", err)
}
log.Println("All master data imported from", inputDir)
}
```
## Error Handling
All export/import functions return errors that should be checked:
- **Export errors**: Usually file system issues (permissions, disk space)
- **Import errors**: Can be:
- File not found
- Invalid JSON format
- Missing dependencies (e.g., referenced source doesn't exist)
- Database constraint violations
## Testing
The export/import mechanism is fully tested with TDD. See `gsmaster/export_import_test.go` for comprehensive test coverage including:
- Export creates valid JSON files
- Import creates new records
- Import updates existing records
- Relationships are correctly restored using natural keys
- Full export/import cycle works correctly
## Notes
- **No Handlers/Routes**: This functionality is intentionally not exposed as API endpoints. Use it programmatically or via CLI tools.
- **ID Independence**: Exported data does not contain database IDs, making it portable across different database instances.
- **Idempotent**: Import can be run multiple times safely - it will update existing records rather than creating duplicates.
- **Transaction Safety**: Each import operation should ideally be wrapped in a database transaction for atomicity.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,594 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"fmt"
"path/filepath"
"gorm.io/gorm"
)
// ExportSkillImprovementCosts exports all skill improvement costs to a JSON file
func ExportSkillImprovementCosts(outputDir string) error {
var costs []models.SkillImprovementCost
if err := database.DB.Preload("SkillCategoryDifficulty.Skill").
Preload("SkillCategoryDifficulty.SkillCategory").
Preload("SkillCategoryDifficulty.SkillDifficulty").
Find(&costs).Error; err != nil {
return fmt.Errorf("failed to fetch skill improvement costs: %w", err)
}
exportable := make([]ExportableSkillImprovementCost, 0, len(costs))
for _, cost := range costs {
// Skip records with incomplete relationships
if cost.SkillCategoryDifficulty.Skill.Name == "" ||
cost.SkillCategoryDifficulty.SkillCategory.Name == "" ||
cost.SkillCategoryDifficulty.SkillDifficulty.Name == "" {
continue
}
exportable = append(exportable, ExportableSkillImprovementCost{
SkillName: cost.SkillCategoryDifficulty.Skill.Name,
SkillSystem: cost.SkillCategoryDifficulty.Skill.GameSystem,
CategoryName: cost.SkillCategoryDifficulty.SkillCategory.Name,
CategorySystem: cost.SkillCategoryDifficulty.SkillCategory.GameSystem,
DifficultyName: cost.SkillCategoryDifficulty.SkillDifficulty.Name,
DifficultySystem: cost.SkillCategoryDifficulty.SkillDifficulty.GameSystem,
CurrentLevel: cost.CurrentLevel,
TERequired: cost.TERequired,
})
}
return writeJSON(filepath.Join(outputDir, "skill_improvement_costs.json"), exportable)
}
// ImportSkillImprovementCosts imports skill improvement costs from a JSON file
func ImportSkillImprovementCosts(inputDir string) error {
var exportable []ExportableSkillImprovementCost
if err := readJSON(filepath.Join(inputDir, "skill_improvement_costs.json"), &exportable); err != nil {
return err
}
// Build lookup maps using helpers
skillMap := buildSkillMap()
categoryMap := buildCategoryMap()
difficultyMap := buildDifficultyMap()
for _, exp := range exportable {
// Find skill ID
skillID, ok := skillMap[exp.SkillSystem][exp.SkillName]
if !ok {
return fmt.Errorf("skill not found: %s (%s)", exp.SkillName, exp.SkillSystem)
}
// Find category ID
categoryID, ok := categoryMap[exp.CategorySystem][exp.CategoryName]
if !ok {
return fmt.Errorf("category not found: %s (%s)", exp.CategoryName, exp.CategorySystem)
}
// Find difficulty ID
difficultyID, ok := difficultyMap[exp.DifficultySystem][exp.DifficultyName]
if !ok {
return fmt.Errorf("difficulty not found: %s (%s)", exp.DifficultyName, exp.DifficultySystem)
}
// Find SkillCategoryDifficulty
var scd models.SkillCategoryDifficulty
if err := database.DB.Where("skill_id = ? AND skill_category_id = ? AND skill_difficulty_id = ?",
skillID, categoryID, difficultyID).First(&scd).Error; err != nil {
return fmt.Errorf("skill category difficulty not found for %s/%s/%s: %w",
exp.SkillName, exp.CategoryName, exp.DifficultyName, err)
}
// Find or create SkillImprovementCost
var cost models.SkillImprovementCost
result := database.DB.Where("skill_category_difficulty_id = ? AND current_level = ?",
scd.ID, exp.CurrentLevel).First(&cost)
if result.Error == gorm.ErrRecordNotFound {
cost = models.SkillImprovementCost{
SkillCategoryDifficultyID: scd.ID,
CurrentLevel: exp.CurrentLevel,
TERequired: exp.TERequired,
}
if err := database.DB.Create(&cost).Error; err != nil {
return fmt.Errorf("failed to create skill improvement cost: %w", err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query skill improvement cost: %w", result.Error)
} else {
cost.TERequired = exp.TERequired
if err := database.DB.Save(&cost).Error; err != nil {
return fmt.Errorf("failed to update skill improvement cost: %w", err)
}
}
}
return nil
}
// ExportWeaponSkills exports all weapon skills to a JSON file
func ExportWeaponSkills(outputDir string) error {
var skills []models.WeaponSkill
if err := database.DB.Find(&skills).Error; err != nil {
return fmt.Errorf("failed to fetch weapon skills: %w", err)
}
sourceMap := buildSourceMap()
exportable := make([]ExportableWeaponSkill, len(skills))
for i, skill := range skills {
exportable[i] = ExportableWeaponSkill{
Name: skill.Name,
GameSystem: skill.GameSystem,
Beschreibung: skill.Beschreibung,
SourceCode: sourceMap[skill.SourceID],
PageNumber: skill.PageNumber,
Initialwert: skill.Initialwert,
BasisWert: skill.BasisWert,
Bonuseigenschaft: skill.Bonuseigenschaft,
Improvable: skill.Improvable,
InnateSkill: skill.InnateSkill,
Category: skill.Category,
Difficulty: skill.Difficulty,
}
}
return writeJSON(filepath.Join(outputDir, "weapon_skills.json"), exportable)
}
// ImportWeaponSkills imports weapon skills from a JSON file
func ImportWeaponSkills(inputDir string) error {
var exportable []ExportableWeaponSkill
if err := readJSON(filepath.Join(inputDir, "weapon_skills.json"), &exportable); err != nil {
return err
}
sourceMap := buildSourceMapReverse()
for _, exp := range exportable {
var skill models.WeaponSkill
result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&skill)
sourceID := sourceMap[exp.SourceCode]
if result.Error == gorm.ErrRecordNotFound {
skill = models.WeaponSkill{
Skill: models.Skill{
Name: exp.Name,
GameSystem: exp.GameSystem,
Beschreibung: exp.Beschreibung,
SourceID: sourceID,
PageNumber: exp.PageNumber,
Initialwert: exp.Initialwert,
BasisWert: exp.BasisWert,
Bonuseigenschaft: exp.Bonuseigenschaft,
Improvable: exp.Improvable,
InnateSkill: exp.InnateSkill,
Category: exp.Category,
Difficulty: exp.Difficulty,
},
}
if err := database.DB.Create(&skill).Error; err != nil {
return fmt.Errorf("failed to create weapon skill %s: %w", exp.Name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query weapon skill %s: %w", exp.Name, result.Error)
} else {
skill.Beschreibung = exp.Beschreibung
skill.SourceID = sourceID
skill.PageNumber = exp.PageNumber
skill.Initialwert = exp.Initialwert
skill.BasisWert = exp.BasisWert
skill.Bonuseigenschaft = exp.Bonuseigenschaft
skill.Improvable = exp.Improvable
skill.InnateSkill = exp.InnateSkill
skill.Category = exp.Category
skill.Difficulty = exp.Difficulty
if err := database.DB.Save(&skill).Error; err != nil {
return fmt.Errorf("failed to update weapon skill %s: %w", exp.Name, err)
}
}
}
return nil
}
// ExportEquipment exports all equipment to a JSON file
func ExportEquipment(outputDir string) error {
var equipment []models.Equipment
if err := database.DB.Find(&equipment).Error; err != nil {
return fmt.Errorf("failed to fetch equipment: %w", err)
}
sourceMap := buildSourceMap()
exportable := make([]ExportableEquipment, len(equipment))
for i, eq := range equipment {
exportable[i] = ExportableEquipment{
Name: eq.Name,
GameSystem: eq.GameSystem,
Beschreibung: eq.Beschreibung,
SourceCode: sourceMap[eq.SourceID],
PageNumber: eq.PageNumber,
Gewicht: eq.Gewicht,
Wert: eq.Wert,
PersonalItem: eq.PersonalItem,
}
}
return writeJSON(filepath.Join(outputDir, "equipment.json"), exportable)
}
// ImportEquipment imports equipment from a JSON file
func ImportEquipment(inputDir string) error {
var exportable []ExportableEquipment
if err := readJSON(filepath.Join(inputDir, "equipment.json"), &exportable); err != nil {
return err
}
sourceMap := buildSourceMapReverse()
for _, exp := range exportable {
var eq models.Equipment
result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&eq)
sourceID := sourceMap[exp.SourceCode]
if result.Error == gorm.ErrRecordNotFound {
eq = models.Equipment{
Name: exp.Name,
GameSystem: exp.GameSystem,
Beschreibung: exp.Beschreibung,
SourceID: sourceID,
PageNumber: exp.PageNumber,
Gewicht: exp.Gewicht,
Wert: exp.Wert,
PersonalItem: exp.PersonalItem,
}
if err := database.DB.Create(&eq).Error; err != nil {
return fmt.Errorf("failed to create equipment %s: %w", exp.Name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query equipment %s: %w", exp.Name, result.Error)
} else {
eq.Beschreibung = exp.Beschreibung
eq.SourceID = sourceID
eq.PageNumber = exp.PageNumber
eq.Gewicht = exp.Gewicht
eq.Wert = exp.Wert
eq.PersonalItem = exp.PersonalItem
if err := database.DB.Save(&eq).Error; err != nil {
return fmt.Errorf("failed to update equipment %s: %w", exp.Name, err)
}
}
}
return nil
}
// ExportWeapons exports all weapons to a JSON file
func ExportWeapons(outputDir string) error {
var weapons []models.Weapon
if err := database.DB.Find(&weapons).Error; err != nil {
return fmt.Errorf("failed to fetch weapons: %w", err)
}
sourceMap := buildSourceMap()
exportable := make([]ExportableWeapon, len(weapons))
for i, weapon := range weapons {
exportable[i] = ExportableWeapon{
Name: weapon.Name,
GameSystem: weapon.GameSystem,
Beschreibung: weapon.Beschreibung,
SourceCode: sourceMap[weapon.SourceID],
PageNumber: weapon.PageNumber,
Gewicht: weapon.Gewicht,
Wert: weapon.Wert,
PersonalItem: weapon.PersonalItem,
SkillRequired: weapon.SkillRequired,
Damage: weapon.Damage,
RangeNear: weapon.RangeNear,
RangeMiddle: weapon.RangeMiddle,
RangeFar: weapon.RangeFar,
}
}
return writeJSON(filepath.Join(outputDir, "weapons.json"), exportable)
}
// ImportWeapons imports weapons from a JSON file
func ImportWeapons(inputDir string) error {
var exportable []ExportableWeapon
if err := readJSON(filepath.Join(inputDir, "weapons.json"), &exportable); err != nil {
return err
}
sourceMap := buildSourceMapReverse()
for _, exp := range exportable {
var weapon models.Weapon
result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&weapon)
sourceID := sourceMap[exp.SourceCode]
if result.Error == gorm.ErrRecordNotFound {
weapon = models.Weapon{
Equipment: models.Equipment{
Name: exp.Name,
GameSystem: exp.GameSystem,
Beschreibung: exp.Beschreibung,
SourceID: sourceID,
PageNumber: exp.PageNumber,
Gewicht: exp.Gewicht,
Wert: exp.Wert,
PersonalItem: exp.PersonalItem,
},
SkillRequired: exp.SkillRequired,
Damage: exp.Damage,
RangeNear: exp.RangeNear,
RangeMiddle: exp.RangeMiddle,
RangeFar: exp.RangeFar,
}
if err := database.DB.Create(&weapon).Error; err != nil {
return fmt.Errorf("failed to create weapon %s: %w", exp.Name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query weapon %s: %w", exp.Name, result.Error)
} else {
weapon.Beschreibung = exp.Beschreibung
weapon.SourceID = sourceID
weapon.PageNumber = exp.PageNumber
weapon.Gewicht = exp.Gewicht
weapon.Wert = exp.Wert
weapon.PersonalItem = exp.PersonalItem
weapon.SkillRequired = exp.SkillRequired
weapon.Damage = exp.Damage
weapon.RangeNear = exp.RangeNear
weapon.RangeMiddle = exp.RangeMiddle
weapon.RangeFar = exp.RangeFar
if err := database.DB.Save(&weapon).Error; err != nil {
return fmt.Errorf("failed to update weapon %s: %w", exp.Name, err)
}
}
}
return nil
}
// ExportContainers exports all containers to a JSON file
func ExportContainers(outputDir string) error {
var containers []models.Container
if err := database.DB.Find(&containers).Error; err != nil {
return fmt.Errorf("failed to fetch containers: %w", err)
}
sourceMap := buildSourceMap()
exportable := make([]ExportableContainer, len(containers))
for i, container := range containers {
exportable[i] = ExportableContainer{
Name: container.Name,
GameSystem: container.GameSystem,
Beschreibung: container.Beschreibung,
SourceCode: sourceMap[container.SourceID],
PageNumber: container.PageNumber,
Gewicht: container.Gewicht,
Wert: container.Wert,
PersonalItem: container.PersonalItem,
Tragkraft: container.Tragkraft,
Volumen: container.Volumen,
}
}
return writeJSON(filepath.Join(outputDir, "containers.json"), exportable)
}
// ImportContainers imports containers from a JSON file
func ImportContainers(inputDir string) error {
var exportable []ExportableContainer
if err := readJSON(filepath.Join(inputDir, "containers.json"), &exportable); err != nil {
return err
}
sourceMap := buildSourceMapReverse()
for _, exp := range exportable {
var container models.Container
result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&container)
sourceID := sourceMap[exp.SourceCode]
if result.Error == gorm.ErrRecordNotFound {
container = models.Container{
Equipment: models.Equipment{
Name: exp.Name,
GameSystem: exp.GameSystem,
Beschreibung: exp.Beschreibung,
SourceID: sourceID,
PageNumber: exp.PageNumber,
Gewicht: exp.Gewicht,
Wert: exp.Wert,
PersonalItem: exp.PersonalItem,
},
Tragkraft: exp.Tragkraft,
Volumen: exp.Volumen,
}
if err := database.DB.Create(&container).Error; err != nil {
return fmt.Errorf("failed to create container %s: %w", exp.Name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query container %s: %w", exp.Name, result.Error)
} else {
container.Beschreibung = exp.Beschreibung
container.SourceID = sourceID
container.PageNumber = exp.PageNumber
container.Gewicht = exp.Gewicht
container.Wert = exp.Wert
container.PersonalItem = exp.PersonalItem
container.Tragkraft = exp.Tragkraft
container.Volumen = exp.Volumen
if err := database.DB.Save(&container).Error; err != nil {
return fmt.Errorf("failed to update container %s: %w", exp.Name, err)
}
}
}
return nil
}
// ExportTransportation exports all transportation to a JSON file
func ExportTransportation(outputDir string) error {
var transportation []models.Transportation
if err := database.DB.Find(&transportation).Error; err != nil {
return fmt.Errorf("failed to fetch transportation: %w", err)
}
sourceMap := buildSourceMap()
exportable := make([]ExportableTransportation, len(transportation))
for i, trans := range transportation {
exportable[i] = ExportableTransportation{
Name: trans.Name,
GameSystem: trans.GameSystem,
Beschreibung: trans.Beschreibung,
SourceCode: sourceMap[trans.SourceID],
PageNumber: trans.PageNumber,
Gewicht: trans.Gewicht,
Wert: trans.Wert,
PersonalItem: trans.PersonalItem,
Tragkraft: trans.Tragkraft,
Volumen: trans.Volumen,
}
}
return writeJSON(filepath.Join(outputDir, "transportation.json"), exportable)
}
// ImportTransportation imports transportation from a JSON file
func ImportTransportation(inputDir string) error {
var exportable []ExportableTransportation
if err := readJSON(filepath.Join(inputDir, "transportation.json"), &exportable); err != nil {
return err
}
sourceMap := buildSourceMapReverse()
for _, exp := range exportable {
var trans models.Transportation
result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&trans)
sourceID := sourceMap[exp.SourceCode]
if result.Error == gorm.ErrRecordNotFound {
trans = models.Transportation{
Container: models.Container{
Equipment: models.Equipment{
Name: exp.Name,
GameSystem: exp.GameSystem,
Beschreibung: exp.Beschreibung,
SourceID: sourceID,
PageNumber: exp.PageNumber,
Gewicht: exp.Gewicht,
Wert: exp.Wert,
PersonalItem: exp.PersonalItem,
},
Tragkraft: exp.Tragkraft,
Volumen: exp.Volumen,
},
}
if err := database.DB.Create(&trans).Error; err != nil {
return fmt.Errorf("failed to create transportation %s: %w", exp.Name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query transportation %s: %w", exp.Name, result.Error)
} else {
trans.Beschreibung = exp.Beschreibung
trans.SourceID = sourceID
trans.PageNumber = exp.PageNumber
trans.Gewicht = exp.Gewicht
trans.Wert = exp.Wert
trans.PersonalItem = exp.PersonalItem
trans.Tragkraft = exp.Tragkraft
trans.Volumen = exp.Volumen
if err := database.DB.Save(&trans).Error; err != nil {
return fmt.Errorf("failed to update transportation %s: %w", exp.Name, err)
}
}
}
return nil
}
// ExportBelieves exports all beliefs to a JSON file
func ExportBelieves(outputDir string) error {
var believes []models.Believe
if err := database.DB.Find(&believes).Error; err != nil {
return fmt.Errorf("failed to fetch believes: %w", err)
}
sourceMap := buildSourceMap()
exportable := make([]ExportableBelieve, len(believes))
for i, believe := range believes {
exportable[i] = ExportableBelieve{
Name: believe.Name,
GameSystem: believe.GameSystem,
Beschreibung: believe.Beschreibung,
SourceCode: sourceMap[believe.SourceID],
PageNumber: believe.PageNumber,
}
}
return writeJSON(filepath.Join(outputDir, "believes.json"), exportable)
}
// ImportBelieves imports believes from a JSON file
func ImportBelieves(inputDir string) error {
var exportable []ExportableBelieve
if err := readJSON(filepath.Join(inputDir, "believes.json"), &exportable); err != nil {
return err
}
sourceMap := buildSourceMapReverse()
for _, exp := range exportable {
var believe models.Believe
result := database.DB.Where("name = ? AND game_system = ?", exp.Name, exp.GameSystem).First(&believe)
sourceID := sourceMap[exp.SourceCode]
if result.Error == gorm.ErrRecordNotFound {
believe = models.Believe{
Name: exp.Name,
GameSystem: exp.GameSystem,
Beschreibung: exp.Beschreibung,
SourceID: sourceID,
PageNumber: exp.PageNumber,
}
if err := database.DB.Create(&believe).Error; err != nil {
return fmt.Errorf("failed to create believe %s: %w", exp.Name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query believe %s: %w", exp.Name, result.Error)
} else {
believe.Beschreibung = exp.Beschreibung
believe.SourceID = sourceID
believe.PageNumber = exp.PageNumber
if err := database.DB.Save(&believe).Error; err != nil {
return fmt.Errorf("failed to update believe %s: %w", exp.Name, err)
}
}
}
return nil
}
+167
View File
@@ -0,0 +1,167 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"fmt"
"gorm.io/gorm"
)
// LookupMap builders - reusable functions to build ID<->Code/Name maps
// buildSourceMap creates a map from source ID to source code
func buildSourceMap() map[uint]string {
var sources []models.Source
database.DB.Find(&sources)
sourceMap := make(map[uint]string)
for _, s := range sources {
sourceMap[s.ID] = s.Code
}
return sourceMap
}
// buildSourceMapReverse creates a map from source code to source ID
func buildSourceMapReverse() map[string]uint {
var sources []models.Source
database.DB.Find(&sources)
sourceMap := make(map[string]uint)
for _, s := range sources {
sourceMap[s.Code] = s.ID
}
return sourceMap
}
// buildCategoryMap creates a nested map: game_system -> name -> id
func buildCategoryMap() map[string]map[string]uint {
var categories []models.SkillCategory
database.DB.Find(&categories)
categoryMap := make(map[string]map[string]uint)
for _, c := range categories {
if categoryMap[c.GameSystem] == nil {
categoryMap[c.GameSystem] = make(map[string]uint)
}
categoryMap[c.GameSystem][c.Name] = c.ID
}
return categoryMap
}
// buildDifficultyMap creates a nested map: game_system -> name -> id
func buildDifficultyMap() map[string]map[string]uint {
var difficulties []models.SkillDifficulty
database.DB.Find(&difficulties)
difficultyMap := make(map[string]map[string]uint)
for _, d := range difficulties {
if difficultyMap[d.GameSystem] == nil {
difficultyMap[d.GameSystem] = make(map[string]uint)
}
difficultyMap[d.GameSystem][d.Name] = d.ID
}
return difficultyMap
}
// buildCharacterClassMap creates a map from character class code to ID
func buildCharacterClassMap() map[string]uint {
var classes []models.CharacterClass
database.DB.Find(&classes)
classMap := make(map[string]uint)
for _, c := range classes {
classMap[c.Code] = c.ID
}
return classMap
}
// buildSpellSchoolMap creates a nested map: game_system -> name -> id
func buildSpellSchoolMap() map[string]map[string]uint {
var schools []models.SpellSchool
database.DB.Find(&schools)
schoolMap := make(map[string]map[string]uint)
for _, s := range schools {
if schoolMap[s.GameSystem] == nil {
schoolMap[s.GameSystem] = make(map[string]uint)
}
schoolMap[s.GameSystem][s.Name] = s.ID
}
return schoolMap
}
// buildSkillMap creates a nested map: game_system -> name -> id
func buildSkillMap() map[string]map[string]uint {
var skills []models.Skill
database.DB.Find(&skills)
skillMap := make(map[string]map[string]uint)
for _, s := range skills {
if skillMap[s.GameSystem] == nil {
skillMap[s.GameSystem] = make(map[string]uint)
}
skillMap[s.GameSystem][s.Name] = s.ID
}
return skillMap
}
// buildWeaponSkillMap creates a nested map: game_system -> name -> id
func buildWeaponSkillMap() map[string]map[string]uint {
var weaponSkills []models.WeaponSkill
database.DB.Find(&weaponSkills)
weaponSkillMap := make(map[string]map[string]uint)
for _, ws := range weaponSkills {
if weaponSkillMap[ws.GameSystem] == nil {
weaponSkillMap[ws.GameSystem] = make(map[string]uint)
}
weaponSkillMap[ws.GameSystem][ws.Name] = ws.ID
}
return weaponSkillMap
}
// Generic import helper for entities with name + game_system natural key
type ImportConfig struct {
EntityName string // For error messages, e.g., "skill category"
}
// findOrCreateByNameAndSystem is a helper for import operations
// It looks up an entity by name and game_system, creates if not found
func findOrCreateByNameAndSystem(
name string,
gameSystem string,
model interface{},
entityName string,
) error {
result := database.DB.Where("name = ? AND game_system = ?", name, gameSystem).First(model)
if result.Error == gorm.ErrRecordNotFound {
if err := database.DB.Create(model).Error; err != nil {
return fmt.Errorf("failed to create %s %s: %w", entityName, name, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query %s %s: %w", entityName, name, result.Error)
} else {
if err := database.DB.Save(model).Error; err != nil {
return fmt.Errorf("failed to update %s %s: %w", entityName, name, err)
}
}
return nil
}
// findOrCreateByCode is a helper for import operations with code as natural key
func findOrCreateByCode(
code string,
model interface{},
entityName string,
) error {
result := database.DB.Where("code = ?", code).First(model)
if result.Error == gorm.ErrRecordNotFound {
if err := database.DB.Create(model).Error; err != nil {
return fmt.Errorf("failed to create %s %s: %w", entityName, code, err)
}
} else if result.Error != nil {
return fmt.Errorf("failed to query %s %s: %w", entityName, code, result.Error)
} else {
if err := database.DB.Save(model).Error; err != nil {
return fmt.Errorf("failed to update %s %s: %w", entityName, code, err)
}
}
return nil
}
+914
View File
@@ -0,0 +1,914 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)
func TestExportSkills(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data
source := getOrCreateSource("KOD", "Kodex")
skill := models.Skill{
Name: "Schwimmen",
GameSystem: "midgard",
Beschreibung: "Schwimmen im Wasser",
Initialwert: 12,
BasisWert: 0,
Bonuseigenschaft: "Gw",
Improvable: true,
InnateSkill: false,
SourceID: source.ID,
PageNumber: 42,
}
database.DB.Create(&skill)
// Export skills
tmpDir := t.TempDir()
err := ExportSkills(tmpDir)
if err != nil {
t.Fatalf("ExportSkills failed: %v", err)
}
// Verify file exists
exportFile := filepath.Join(tmpDir, "skills.json")
if _, err := os.Stat(exportFile); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", exportFile)
}
}
func TestImportSkills(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create source that will be referenced
source := getOrCreateSource("KOD", "Kodex")
// Export first
tmpDir := t.TempDir()
skill := models.Skill{
Name: "Klettern",
GameSystem: "midgard",
Beschreibung: "Klettern an Wänden",
Initialwert: 10,
BasisWert: 0,
Bonuseigenschaft: "Gw",
Improvable: true,
InnateSkill: false,
SourceID: source.ID,
PageNumber: 50,
}
database.DB.Create(&skill)
err := ExportSkills(tmpDir)
if err != nil {
t.Fatalf("ExportSkills failed: %v", err)
}
// Delete the skill
database.DB.Delete(&skill)
// Import back
err = ImportSkills(tmpDir)
if err != nil {
t.Fatalf("ImportSkills failed: %v", err)
}
// Verify skill was imported
var importedSkill models.Skill
err = database.DB.Where("name = ? AND game_system = ?", "Klettern", "midgard").First(&importedSkill).Error
if err != nil {
t.Fatalf("Imported skill not found: %v", err)
}
if importedSkill.Beschreibung != "Klettern an Wänden" {
t.Errorf("Expected beschreibung 'Klettern an Wänden', got '%s'", importedSkill.Beschreibung)
}
if importedSkill.Initialwert != 10 {
t.Errorf("Expected initialwert 10, got %d", importedSkill.Initialwert)
}
}
func TestImportSkillsUpdate(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("KOD", "Kodex")
// Create existing skill
skill := models.Skill{
Name: "Reiten",
GameSystem: "midgard",
Beschreibung: "Alte Beschreibung",
Initialwert: 8,
SourceID: source.ID,
PageNumber: 30,
}
database.DB.Create(&skill)
// Export, modify, and re-import
tmpDir := t.TempDir()
err := ExportSkills(tmpDir)
if err != nil {
t.Fatalf("ExportSkills failed: %v", err)
}
// Update skill manually
skill.Beschreibung = "Neue Beschreibung"
skill.Initialwert = 12
database.DB.Save(&skill)
// Export again with updated values
err = ExportSkills(tmpDir)
if err != nil {
t.Fatalf("ExportSkills failed: %v", err)
}
// Reset to old values
skill.Beschreibung = "Alte Beschreibung"
skill.Initialwert = 8
database.DB.Save(&skill)
// Import should update to exported values
err = ImportSkills(tmpDir)
if err != nil {
t.Fatalf("ImportSkills failed: %v", err)
}
// Verify update
var updatedSkill models.Skill
err = database.DB.Where("name = ? AND game_system = ?", "Reiten", "midgard").First(&updatedSkill).Error
if err != nil {
t.Fatalf("Updated skill not found: %v", err)
}
if updatedSkill.Beschreibung != "Neue Beschreibung" {
t.Errorf("Expected updated beschreibung 'Neue Beschreibung', got '%s'", updatedSkill.Beschreibung)
}
if updatedSkill.Initialwert != 12 {
t.Errorf("Expected updated initialwert 12, got %d", updatedSkill.Initialwert)
}
}
func TestExportImportSources(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test source
source := models.Source{
Code: "ARK",
Name: "Arkanum",
GameSystem: "midgard",
IsActive: true,
}
database.DB.Create(&source)
// Export
tmpDir := t.TempDir()
err := ExportSources(tmpDir)
if err != nil {
t.Fatalf("ExportSources failed: %v", err)
}
// Delete
database.DB.Delete(&source)
// Import
err = ImportSources(tmpDir)
if err != nil {
t.Fatalf("ImportSources failed: %v", err)
}
// Verify
var imported models.Source
err = database.DB.Where("code = ?", "ARK").First(&imported).Error
if err != nil {
t.Fatalf("Imported source not found: %v", err)
}
if imported.Name != "Arkanum" {
t.Errorf("Expected name 'Arkanum', got '%s'", imported.Name)
}
}
func TestExportImportSkillCategoryDifficulty(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create dependencies
source := getOrCreateSource("KOD", "Kodex")
skill := models.Skill{
Name: "Tanzen",
GameSystem: "midgard",
SourceID: source.ID,
}
database.DB.Create(&skill)
category := getOrCreateCategory("Alltag", source.ID)
difficulty := getOrCreateDifficulty("leicht")
// Create relationship
scd := models.SkillCategoryDifficulty{
SkillID: skill.ID,
SkillCategoryID: category.ID,
SkillDifficultyID: difficulty.ID,
LearnCost: 5,
SCategory: category.Name,
SDifficulty: difficulty.Name,
}
database.DB.Create(&scd)
// Export
tmpDir := t.TempDir()
err := ExportSkillCategoryDifficulties(tmpDir)
if err != nil {
t.Fatalf("ExportSkillCategoryDifficulties failed: %v", err)
}
// Delete relationship
database.DB.Delete(&scd)
// Import
err = ImportSkillCategoryDifficulties(tmpDir)
if err != nil {
t.Fatalf("ImportSkillCategoryDifficulties failed: %v", err)
}
// Verify relationship was recreated
var imported models.SkillCategoryDifficulty
err = database.DB.Where("skill_id = ? AND skill_category_id = ?", skill.ID, category.ID).First(&imported).Error
if err != nil {
t.Fatalf("Imported relationship not found: %v", err)
}
if imported.LearnCost != 5 {
t.Errorf("Expected learn_cost 5, got %d", imported.LearnCost)
}
}
func TestExportImportSkillCategories(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data
source := getOrCreateSource("TEST_SC", "Test Source")
category := models.SkillCategory{
Name: "TestCategory",
GameSystem: "midgard",
SourceID: source.ID,
}
database.DB.Create(&category)
// Export
tempDir := t.TempDir()
err := ExportSkillCategories(tempDir)
if err != nil {
t.Fatalf("ExportSkillCategories failed: %v", err)
}
// Verify file was created
filename := filepath.Join(tempDir, "skill_categories.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
// Delete the category
database.DB.Unscoped().Delete(&category)
// Import
err = ImportSkillCategories(tempDir)
if err != nil {
t.Fatalf("ImportSkillCategories failed: %v", err)
}
// Verify the category was recreated
var imported models.SkillCategory
result := database.DB.Where("name = ? AND game_system = ?", "TestCategory", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Category not found after import: %v", result.Error)
}
if imported.SourceID != source.ID {
t.Errorf("Expected SourceID %d, got %d", source.ID, imported.SourceID)
}
}
func TestExportImportSkillDifficulties(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data
difficulty := models.SkillDifficulty{
Name: "TestDifficulty",
GameSystem: "midgard",
}
database.DB.Create(&difficulty)
// Export
tempDir := t.TempDir()
err := ExportSkillDifficulties(tempDir)
if err != nil {
t.Fatalf("ExportSkillDifficulties failed: %v", err)
}
// Verify file was created
filename := filepath.Join(tempDir, "skill_difficulties.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
// Delete the difficulty
database.DB.Unscoped().Delete(&difficulty)
// Import
err = ImportSkillDifficulties(tempDir)
if err != nil {
t.Fatalf("ImportSkillDifficulties failed: %v", err)
}
// Verify the difficulty was recreated
var imported models.SkillDifficulty
result := database.DB.Where("name = ? AND game_system = ?", "TestDifficulty", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Difficulty not found after import: %v", result.Error)
}
}
func TestExportImportSpells(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data
source := getOrCreateSource("TEST_SP", "Test Spell Source")
spell := models.Spell{
Name: "TestSpell",
GameSystem: "midgard",
Beschreibung: "Test description",
SourceID: source.ID,
PageNumber: 42,
Bonus: 5,
Stufe: 3,
AP: "2",
Art: "Gestenzauber",
Zauberdauer: "10 sec",
Reichweite: "10m",
Wirkungsziel: "Person",
Wirkungsbereich: "1 Person",
Wirkungsdauer: "1h",
Ursprung: "Elben",
Category: "normal",
LearningCategory: "default",
}
database.DB.Create(&spell)
// Export
tempDir := t.TempDir()
err := ExportSpells(tempDir)
if err != nil {
t.Fatalf("ExportSpells failed: %v", err)
}
// Verify file was created
filename := filepath.Join(tempDir, "spells.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
// Modify the spell
spell.Beschreibung = "Old description"
spell.Bonus = 3
database.DB.Save(&spell)
// Import (should update)
err = ImportSpells(tempDir)
if err != nil {
t.Fatalf("ImportSpells failed: %v", err)
}
// Verify the spell was updated
var imported models.Spell
result := database.DB.Where("name = ? AND game_system = ?", "TestSpell", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Spell not found after import: %v", result.Error)
}
if imported.Beschreibung != "Test description" {
t.Errorf("Expected description 'Test description', got '%s'", imported.Beschreibung)
}
if imported.Bonus != 5 {
t.Errorf("Expected bonus 5, got %d", imported.Bonus)
}
if imported.Stufe != 3 {
t.Errorf("Expected level 3, got %d", imported.Stufe)
}
}
func TestExportImportAll(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data
source := getOrCreateSource("TEST_ALL", "Test All Source")
category := models.SkillCategory{Name: "AllCategory", GameSystem: "midgard", SourceID: source.ID}
database.DB.Create(&category)
difficulty := models.SkillDifficulty{Name: "AllDifficulty", GameSystem: "midgard"}
database.DB.Create(&difficulty)
skill := models.Skill{
Name: "AllSkill",
GameSystem: "midgard",
SourceID: source.ID,
Initialwert: 10,
}
database.DB.Create(&skill)
spell := models.Spell{
Name: "AllSpell",
GameSystem: "midgard",
SourceID: source.ID,
Stufe: 2,
}
database.DB.Create(&spell)
// Export all
tempDir := t.TempDir()
err := ExportAll(tempDir)
if err != nil {
t.Fatalf("ExportAll failed: %v", err)
}
// Verify all files were created
files := []string{
"sources.json",
"character_classes.json",
"skill_categories.json",
"skill_difficulties.json",
"spell_schools.json",
"skills.json",
"skill_category_difficulties.json",
"spells.json",
"class_category_ep_costs.json",
"class_spell_school_ep_costs.json",
"spell_level_le_costs.json",
"skill_improvement_costs.json",
"weapon_skills.json",
"equipment.json",
"weapons.json",
"containers.json",
"transportation.json",
"believes.json",
}
for _, file := range files {
filename := filepath.Join(tempDir, file)
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Errorf("Export file not created: %s", filename)
}
}
// Delete all test data
database.DB.Unscoped().Delete(&spell)
database.DB.Unscoped().Delete(&skill)
database.DB.Unscoped().Delete(&difficulty)
database.DB.Unscoped().Delete(&category)
// Don't delete source to avoid FK constraints
// Import all
err = ImportAll(tempDir)
if err != nil {
t.Fatalf("ImportAll failed: %v", err)
}
// Verify all data was recreated
var importedCategory models.SkillCategory
if err := database.DB.Where("name = ? AND game_system = ?", "AllCategory", "midgard").First(&importedCategory).Error; err != nil {
t.Errorf("Category not found after import: %v", err)
}
var importedDifficulty models.SkillDifficulty
if err := database.DB.Where("name = ? AND game_system = ?", "AllDifficulty", "midgard").First(&importedDifficulty).Error; err != nil {
t.Errorf("Difficulty not found after import: %v", err)
}
var importedSkill models.Skill
if err := database.DB.Where("name = ? AND game_system = ?", "AllSkill", "midgard").First(&importedSkill).Error; err != nil {
t.Errorf("Skill not found after import: %v", err)
}
var importedSpell models.Spell
if err := database.DB.Where("name = ? AND game_system = ?", "AllSpell", "midgard").First(&importedSpell).Error; err != nil {
t.Errorf("Spell not found after import: %v", err)
}
}
func TestExportAll_live(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB(false)
// Export all
tempDir := t.TempDir()
err := ExportAll(tempDir)
if err != nil {
t.Fatalf("ExportAll failed: %v", err)
}
// Verify all files were created
files := []string{"sources.json", "skill_categories.json", "skill_difficulties.json", "skills.json", "spells.json", "skill_category_difficulties.json"}
for _, file := range files {
filename := filepath.Join(tempDir, file)
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Errorf("Export file not created: %s", filename)
}
}
assert.Equal(t, len(files), 6)
}
func TestExportImportWeaponSkills(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("TEST_WS", "Test Weapon Source")
weaponSkill := models.WeaponSkill{
Skill: models.Skill{
Name: "Langschwert",
GameSystem: "midgard",
Beschreibung: "Langschwert Waffenfertigkeiten",
SourceID: source.ID,
PageNumber: 50,
Initialwert: 10,
BasisWert: 5,
Bonuseigenschaft: "St",
Improvable: true,
InnateSkill: false,
Category: "Waffen",
Difficulty: "normal",
},
}
database.DB.Create(&weaponSkill)
tempDir := t.TempDir()
err := ExportWeaponSkills(tempDir)
if err != nil {
t.Fatalf("ExportWeaponSkills failed: %v", err)
}
filename := filepath.Join(tempDir, "weapon_skills.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
database.DB.Unscoped().Delete(&weaponSkill)
err = ImportWeaponSkills(tempDir)
if err != nil {
t.Fatalf("ImportWeaponSkills failed: %v", err)
}
var imported models.WeaponSkill
result := database.DB.Where("name = ? AND game_system = ?", "Langschwert", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Weapon skill not found after import: %v", result.Error)
}
assert.Equal(t, "Langschwert Waffenfertigkeiten", imported.Beschreibung)
assert.Equal(t, 10, imported.Initialwert)
assert.Equal(t, 5, imported.BasisWert)
}
func TestExportImportEquipment(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("TEST_EQ", "Test Equipment Source")
equipment := models.Equipment{
Name: "Seil",
GameSystem: "midgard",
Beschreibung: "10m langes Hanfseil",
SourceID: source.ID,
PageNumber: 75,
Gewicht: 2.5,
Wert: 15.0,
PersonalItem: false,
}
database.DB.Create(&equipment)
tempDir := t.TempDir()
err := ExportEquipment(tempDir)
if err != nil {
t.Fatalf("ExportEquipment failed: %v", err)
}
filename := filepath.Join(tempDir, "equipment.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
equipment.Wert = 10.0
database.DB.Save(&equipment)
err = ImportEquipment(tempDir)
if err != nil {
t.Fatalf("ImportEquipment failed: %v", err)
}
var imported models.Equipment
result := database.DB.Where("name = ? AND game_system = ?", "Seil", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Equipment not found after import: %v", result.Error)
}
assert.Equal(t, 15.0, imported.Wert)
assert.Equal(t, 2.5, imported.Gewicht)
}
func TestExportImportWeapons(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("TEST_WP", "Test Weapon Source")
weapon := models.Weapon{
Equipment: models.Equipment{
Name: "Kurzschwert",
GameSystem: "midgard",
Beschreibung: "Einhändiges Kurzschwert",
SourceID: source.ID,
PageNumber: 80,
Gewicht: 1.5,
Wert: 50.0,
PersonalItem: false,
},
SkillRequired: "Langschwert",
Damage: "1W6+1",
RangeNear: 0,
RangeMiddle: 0,
RangeFar: 0,
}
database.DB.Create(&weapon)
tempDir := t.TempDir()
err := ExportWeapons(tempDir)
if err != nil {
t.Fatalf("ExportWeapons failed: %v", err)
}
filename := filepath.Join(tempDir, "weapons.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
weapon.Damage = "1W6"
database.DB.Save(&weapon)
err = ImportWeapons(tempDir)
if err != nil {
t.Fatalf("ImportWeapons failed: %v", err)
}
var imported models.Weapon
result := database.DB.Where("name = ? AND game_system = ?", "Kurzschwert", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Weapon not found after import: %v", result.Error)
}
assert.Equal(t, "1W6+1", imported.Damage)
assert.Equal(t, "Langschwert", imported.SkillRequired)
}
func TestExportImportContainers(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("TEST_CT", "Test Container Source")
container := models.Container{
Equipment: models.Equipment{
Name: "Rucksack",
GameSystem: "midgard",
Beschreibung: "Großer Lederrucksack",
SourceID: source.ID,
PageNumber: 85,
Gewicht: 1.0,
Wert: 20.0,
PersonalItem: false,
},
Tragkraft: 30.0,
Volumen: 50.0,
}
database.DB.Create(&container)
tempDir := t.TempDir()
err := ExportContainers(tempDir)
if err != nil {
t.Fatalf("ExportContainers failed: %v", err)
}
filename := filepath.Join(tempDir, "containers.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
container.Tragkraft = 25.0
database.DB.Save(&container)
err = ImportContainers(tempDir)
if err != nil {
t.Fatalf("ImportContainers failed: %v", err)
}
var imported models.Container
result := database.DB.Where("name = ? AND game_system = ?", "Rucksack", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Container not found after import: %v", result.Error)
}
assert.Equal(t, 30.0, imported.Tragkraft)
assert.Equal(t, 50.0, imported.Volumen)
}
func TestExportImportTransportation(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("TEST_TR", "Test Transport Source")
transportation := models.Transportation{
Container: models.Container{
Equipment: models.Equipment{
Name: "Pferdewagen",
GameSystem: "midgard",
Beschreibung: "Zweirädriger Wagen",
SourceID: source.ID,
PageNumber: 90,
Gewicht: 100.0,
Wert: 200.0,
PersonalItem: false,
},
Tragkraft: 500.0,
Volumen: 1000.0,
},
}
database.DB.Create(&transportation)
tempDir := t.TempDir()
err := ExportTransportation(tempDir)
if err != nil {
t.Fatalf("ExportTransportation failed: %v", err)
}
filename := filepath.Join(tempDir, "transportation.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
transportation.Tragkraft = 450.0
database.DB.Save(&transportation)
err = ImportTransportation(tempDir)
if err != nil {
t.Fatalf("ImportTransportation failed: %v", err)
}
var imported models.Transportation
result := database.DB.Where("name = ? AND game_system = ?", "Pferdewagen", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Transportation not found after import: %v", result.Error)
}
assert.Equal(t, 500.0, imported.Tragkraft)
assert.Equal(t, 1000.0, imported.Volumen)
}
func TestExportImportBelieves(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
source := getOrCreateSource("TEST_BL", "Test Believe Source")
believe := models.Believe{
Name: "Kirche des Lichts",
GameSystem: "midgard",
Beschreibung: "Hauptreligion in Valian",
SourceID: source.ID,
PageNumber: 95,
}
database.DB.Create(&believe)
tempDir := t.TempDir()
err := ExportBelieves(tempDir)
if err != nil {
t.Fatalf("ExportBelieves failed: %v", err)
}
filename := filepath.Join(tempDir, "believes.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
believe.Beschreibung = "Alte Beschreibung"
database.DB.Save(&believe)
err = ImportBelieves(tempDir)
if err != nil {
t.Fatalf("ImportBelieves failed: %v", err)
}
var imported models.Believe
result := database.DB.Where("name = ? AND game_system = ?", "Kirche des Lichts", "midgard").First(&imported)
if result.Error != nil {
t.Fatalf("Believe not found after import: %v", result.Error)
}
assert.Equal(t, "Hauptreligion in Valian", imported.Beschreibung)
assert.Equal(t, 95, imported.PageNumber)
}
func TestExportImportSkillImprovementCosts(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create dependencies that already exist in test DB
// Use existing skill, category, difficulty from test database
var skill models.Skill
if err := database.DB.Where("name = ?", "Abrichten").First(&skill).Error; err != nil {
t.Skip("Test skill not found in database, skipping test")
}
var category models.SkillCategory
if err := database.DB.First(&category).Error; err != nil {
t.Skip("No skill category found in database, skipping test")
}
var difficulty models.SkillDifficulty
if err := database.DB.First(&difficulty).Error; err != nil {
t.Skip("No skill difficulty found in database, skipping test")
}
// Find or create SkillCategoryDifficulty
var scd models.SkillCategoryDifficulty
err := database.DB.Where("skill_id = ? AND skill_category_id = ? AND skill_difficulty_id = ?",
skill.ID, category.ID, difficulty.ID).First(&scd).Error
if err == gorm.ErrRecordNotFound {
scd = models.SkillCategoryDifficulty{
SkillID: skill.ID,
SkillCategoryID: category.ID,
SkillDifficultyID: difficulty.ID,
LearnCost: 10,
SCategory: category.Name,
SDifficulty: difficulty.Name,
}
database.DB.Create(&scd)
} else if err != nil {
t.Fatalf("Failed to query SkillCategoryDifficulty: %v", err)
}
// Create SkillImprovementCost
improvementCost := models.SkillImprovementCost{
SkillCategoryDifficultyID: scd.ID,
CurrentLevel: 15, // Use unique level to avoid conflicts
TERequired: 5,
}
database.DB.Create(&improvementCost)
// Export
tempDir := t.TempDir()
err = ExportSkillImprovementCosts(tempDir)
if err != nil {
t.Fatalf("ExportSkillImprovementCosts failed: %v", err)
}
filename := filepath.Join(tempDir, "skill_improvement_costs.json")
if _, err := os.Stat(filename); os.IsNotExist(err) {
t.Fatalf("Export file not created: %s", filename)
}
// Modify the record
improvementCost.TERequired = 7
database.DB.Save(&improvementCost)
// Import should restore original value
err = ImportSkillImprovementCosts(tempDir)
if err != nil {
t.Fatalf("ImportSkillImprovementCosts failed: %v", err)
}
var imported models.SkillImprovementCost
result := database.DB.Where("skill_category_difficulty_id = ? AND current_level = ?", scd.ID, 15).First(&imported)
if result.Error != nil {
t.Fatalf("SkillImprovementCost not found after import: %v", result.Error)
}
// Should be restored to original value from export
assert.Equal(t, 5, imported.TERequired)
assert.Equal(t, 15, imported.CurrentLevel)
}
+1
View File
@@ -13,6 +13,7 @@ func RegisterRoutes(r *gin.RouterGroup) {
maintGrp.GET("", GetMasterData)
maintGrp.GET("/skills", GetMDSkills)
maintGrp.GET("/skills-enhanced", GetEnhancedMDSkills) // New enhanced endpoint
maintGrp.POST("/skills-enhanced", CreateEnhancedMDSkill) // Create new skill
maintGrp.GET("/skills/:id", GetMDSkill)
maintGrp.GET("/skills-enhanced/:id", GetEnhancedMDSkill) // New enhanced endpoint
maintGrp.PUT("/skills/:id", UpdateMDSkill)
+149
View File
@@ -0,0 +1,149 @@
package gsmaster
import (
"bamort/database"
"bamort/models"
"testing"
)
func TestCreateSkillWithCategories(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test dependencies
source := getOrCreateSource("TSTCRT", "TestCreate")
category := getOrCreateCategory("Alltag", source.ID)
difficulty := getOrCreateDifficulty("normal")
// Prepare create request
req := SkillUpdateRequest{
Skill: models.Skill{
Name: "Neue Fertigkeit",
GameSystem: "midgard",
Beschreibung: "Test Fertigkeit",
Initialwert: 5,
BasisWert: 0,
Bonuseigenschaft: "In",
Improvable: true,
InnateSkill: false,
SourceID: source.ID,
PageNumber: 42,
},
CategoryDifficulties: []CategoryDifficultyPair{
{
CategoryID: category.ID,
DifficultyID: difficulty.ID,
},
},
}
// Test creating new skill
skillID, err := CreateSkillWithCategories(req)
if err != nil {
t.Fatalf("CreateSkillWithCategories failed: %v", err)
}
if skillID == 0 {
t.Fatalf("Expected non-zero skill ID, got 0")
}
// Verify skill was created
var skill models.Skill
if err := database.DB.First(&skill, skillID).Error; err != nil {
t.Fatalf("Failed to retrieve created skill: %v", err)
}
if skill.Name != "Neue Fertigkeit" {
t.Errorf("Expected name 'Neue Fertigkeit', got '%s'", skill.Name)
}
if skill.Initialwert != 5 {
t.Errorf("Expected initialwert 5, got %d", skill.Initialwert)
}
if skill.BasisWert != 0 {
t.Errorf("Expected basiswert 0, got %d", skill.BasisWert)
}
// Verify category-difficulty relationship
var scd models.SkillCategoryDifficulty
if err := database.DB.Where("skill_id = ?", skillID).First(&scd).Error; err != nil {
t.Fatalf("Failed to retrieve skill category difficulty: %v", err)
}
if scd.SkillCategoryID != category.ID {
t.Errorf("Expected category ID %d, got %d", category.ID, scd.SkillCategoryID)
}
if scd.SkillDifficultyID != difficulty.ID {
t.Errorf("Expected difficulty ID %d, got %d", difficulty.ID, scd.SkillDifficultyID)
}
}
func TestCreateSkillWithMultipleCategories(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test dependencies
source := getOrCreateSource("TSTMLT", "TestMultiple")
category1 := getOrCreateCategory("Körper", source.ID)
category2 := getOrCreateCategory("Geist", source.ID)
difficulty1 := getOrCreateDifficulty("leicht")
difficulty2 := getOrCreateDifficulty("schwer")
// Prepare create request with multiple categories
req := SkillUpdateRequest{
Skill: models.Skill{
Name: "Multi-Kategorie Fertigkeit",
GameSystem: "midgard",
Initialwert: 10,
Improvable: true,
SourceID: source.ID,
},
CategoryDifficulties: []CategoryDifficultyPair{
{
CategoryID: category1.ID,
DifficultyID: difficulty1.ID,
},
{
CategoryID: category2.ID,
DifficultyID: difficulty2.ID,
},
},
}
// Test creating skill with multiple categories
skillID, err := CreateSkillWithCategories(req)
if err != nil {
t.Fatalf("CreateSkillWithCategories failed: %v", err)
}
// Verify both category-difficulty relationships exist
var scds []models.SkillCategoryDifficulty
if err := database.DB.Where("skill_id = ?", skillID).Find(&scds).Error; err != nil {
t.Fatalf("Failed to retrieve skill category difficulties: %v", err)
}
if len(scds) != 2 {
t.Fatalf("Expected 2 category-difficulty relationships, got %d", len(scds))
}
}
func TestCreateSkillValidation(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Test creating skill without name
req := SkillUpdateRequest{
Skill: models.Skill{
GameSystem: "midgard",
Initialwert: 5,
},
CategoryDifficulties: []CategoryDifficultyPair{},
}
_, err := CreateSkillWithCategories(req)
if err == nil {
t.Error("Expected error when creating skill without name, got nil")
}
}
+92 -2
View File
@@ -95,12 +95,77 @@ type CategoryDifficultyPair struct {
LearnCost int `json:"learn_cost,omitempty"`
}
// CreateSkillWithCategories creates a new skill with category-difficulty relationships
func CreateSkillWithCategories(req SkillUpdateRequest) (uint, error) {
// Validate required fields
if req.Skill.Name == "" {
return 0, fmt.Errorf("skill name is required")
}
var skillID uint
// Start transaction
err := database.DB.Transaction(func(tx *gorm.DB) error {
// Create skill
if err := tx.Create(&req.Skill).Error; err != nil {
return err
}
skillID = req.Skill.ID
// Create category-difficulty relationships
for _, cd := range req.CategoryDifficulties {
// Get category and difficulty names for denormalized fields
var category models.SkillCategory
if err := tx.First(&category, cd.CategoryID).Error; err != nil {
return fmt.Errorf("category not found: %w", err)
}
var difficulty models.SkillDifficulty
if err := tx.First(&difficulty, cd.DifficultyID).Error; err != nil {
return fmt.Errorf("difficulty not found: %w", err)
}
learnCost := cd.LearnCost
if learnCost == 0 {
// Use default based on difficulty
learnCost = getDefaultLearnCost(difficulty.Name)
}
scd := models.SkillCategoryDifficulty{
SkillID: skillID,
SkillCategoryID: cd.CategoryID,
SkillDifficultyID: cd.DifficultyID,
LearnCost: learnCost,
SCategory: category.Name,
SDifficulty: difficulty.Name,
}
if err := tx.Create(&scd).Error; err != nil {
return err
}
}
return nil
})
if err != nil {
return 0, err
}
return skillID, nil
}
// UpdateSkillWithCategories updates a skill and its category-difficulty relationships
func UpdateSkillWithCategories(skillID uint, req SkillUpdateRequest) error {
// Start transaction
return database.DB.Transaction(func(tx *gorm.DB) error {
// Update skill basic info
if err := tx.Model(&models.Skill{}).Where("id = ?", skillID).Updates(req.Skill).Error; err != nil {
// Update skill basic info - use Select to explicitly include boolean fields
// This ensures false values are also updated (GORM skips zero values by default in Updates)
if err := tx.Model(&models.Skill{}).Where("id = ?", skillID).
Select("name", "beschreibung", "game_system", "initialwert", "basis_wert",
"bonuseigenschaft", "improvable", "innate_skill", "source_id", "page_number").
Updates(req.Skill).Error; err != nil {
return err
}
@@ -240,3 +305,28 @@ func UpdateEnhancedMDSkill(c *gin.Context) {
c.JSON(http.StatusOK, skill)
}
// CreateEnhancedMDSkill creates a new skill with categories
func CreateEnhancedMDSkill(c *gin.Context) {
var req SkillUpdateRequest
if err := c.ShouldBindJSON(&req); err != nil {
respondWithError(c, http.StatusBadRequest, "Invalid request: "+err.Error())
return
}
// Create the skill
skillID, err := CreateSkillWithCategories(req)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create skill: "+err.Error())
return
}
// Return created skill
skill, err := GetSkillWithCategories(skillID)
if err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to retrieve created skill")
return
}
c.JSON(http.StatusCreated, skill)
}
@@ -292,6 +292,73 @@ func TestUpdateSkillWithCategories(t *testing.T) {
t.Error("Expected to find 'Alltag/normal' category after update")
}
}
func TestUpdateSkillBooleanFields(t *testing.T) {
setupTestEnvironment(t)
database.SetupTestDB()
// Create test data with improvable=true and innateskill=false
source := getOrCreateSource("TSTBOOL", "TestBoolean")
skill := models.Skill{
Name: "TestBooleanSkill",
GameSystem: "midgard",
Initialwert: 5,
Improvable: true,
InnateSkill: false,
SourceID: source.ID,
}
database.DB.Create(&skill)
category := getOrCreateCategory("Alltag", source.ID)
difficulty := getOrCreateDifficulty("normal")
scd := models.SkillCategoryDifficulty{
SkillID: skill.ID,
SkillCategoryID: category.ID,
SkillDifficultyID: difficulty.ID,
LearnCost: 10,
SCategory: category.Name,
SDifficulty: difficulty.Name,
}
database.DB.Create(&scd)
// Update to set improvable=false and innateskill=true
updateReq := SkillUpdateRequest{
Skill: models.Skill{
ID: skill.ID,
Name: "TestBooleanSkill",
GameSystem: "midgard",
Initialwert: 5,
Improvable: false, // Change to false
InnateSkill: true, // Change to true
SourceID: source.ID,
},
CategoryDifficulties: []CategoryDifficultyPair{
{
CategoryID: category.ID,
DifficultyID: difficulty.ID,
},
},
}
err := UpdateSkillWithCategories(skill.ID, updateReq)
if err != nil {
t.Fatalf("UpdateSkillWithCategories failed: %v", err)
}
// Verify boolean fields were updated correctly
var updatedSkill models.Skill
if err := database.DB.First(&updatedSkill, skill.ID).Error; err != nil {
t.Fatalf("Failed to retrieve updated skill: %v", err)
}
if updatedSkill.Improvable != false {
t.Errorf("Expected improvable to be false, got %v", updatedSkill.Improvable)
}
if updatedSkill.InnateSkill != true {
t.Errorf("Expected innateskill to be true, got %v", updatedSkill.InnateSkill)
}
}
func TestGetDefaultLearnCost(t *testing.T) {
tests := []struct {
+1 -45
View File
@@ -52,6 +52,7 @@ type Skill struct {
SourceID uint `gorm:"index" json:"source_id,omitempty"` // Verweis auf strukturierte Quelle
PageNumber int `json:"page_number,omitempty"` // Seitenzahl im Quellenbuch
Initialwert int `gorm:"default:5" json:"initialwert"`
BasisWert int `gorm:"default:0" json:"basiswert"`
Bonuseigenschaft string `json:"bonuseigenschaft,omitempty"`
Improvable bool `gorm:"default:true" json:"improvable"`
InnateSkill bool `gorm:"default:false" json:"innateskill"`
@@ -127,51 +128,6 @@ type Believe struct {
PageNumber int `json:"page_number,omitempty"` // Seitenzahl im Quellenbuch
}
/*
func (object *LookupList) Create() error {
gameSystem := "midgard"
object.GameSystem = gameSystem
err := database.DB.Transaction(func(tx *gorm.DB) error {
// Save the main character record
if err := tx.Create(&object).Error; err != nil {
return fmt.Errorf("failed to save Lookup: %w", err)
}
return nil
})
return err
}
func (object *LookupList) First(value string) error {
gameSystem := "midgard"
err := database.DB.First(&object, "game_system=? AND name!='Placeholder' AND name = ?", gameSystem, value).Error
if err != nil {
// zauber found
return err
}
return nil
}
func (object *LookupList) FirstId(value uint) error {
gameSystem := "midgard"
err := database.DB.First(&object, "game_system=? AND name!='Placeholder' AND id = ?", gameSystem, value).Error
if err != nil {
// zauber found
return err
}
return nil
}
func (object *LookupList) Save() error {
err := database.DB.Save(&object).Error
if err != nil {
// zauber found
return err
}
return nil
}
*/
func (object *Skill) TableName() string {
dbPrefix := "gsm"
return dbPrefix + "_" + "skills"
@@ -0,0 +1,49 @@
package models
import (
"testing"
)
func TestSkillBasisWert(t *testing.T) {
// Test that BasisWert defaults to 0 in master skill data
skill := Skill{
Name: "TestSkill",
GameSystem: "midgard",
Initialwert: 5,
}
// BasisWert should default to 0
if skill.BasisWert != 0 {
t.Errorf("Expected BasisWert to default to 0, got %d", skill.BasisWert)
}
}
func TestSkillBasisWertSet(t *testing.T) {
// Test that BasisWert can be set in master skill data
skill := Skill{
Name: "TestSkill",
GameSystem: "midgard",
Initialwert: 5,
BasisWert: 3,
}
if skill.BasisWert != 3 {
t.Errorf("Expected BasisWert to be 3, got %d", skill.BasisWert)
}
}
func TestWeaponSkillBasisWert(t *testing.T) {
// Test that BasisWert works for WeaponSkill (inherited from Skill)
weaponSkill := WeaponSkill{
Skill: Skill{
Name: "TestWeaponSkill",
GameSystem: "midgard",
Initialwert: 5,
BasisWert: 2,
},
}
if weaponSkill.BasisWert != 2 {
t.Errorf("Expected BasisWert to be 2, got %d", weaponSkill.BasisWert)
}
}
+1
View File
@@ -6,6 +6,7 @@ type SkFertigkeit struct {
BamortCharTrait
Beschreibung string `json:"beschreibung"`
Fertigkeitswert int `json:"fertigkeitswert"`
BasisWert int `json:"basiswert"`
Bonus int `json:"bonus,omitempty"`
Pp int `json:"pp,omitempty"` //Praxispunkte
Bemerkung string `json:"bemerkung"`
@@ -0,0 +1,61 @@
package models
import (
"testing"
)
func TestSkFertigkeitBasisWert(t *testing.T) {
// Test that BasisWert defaults to 0
skill := SkFertigkeit{
Fertigkeitswert: 10,
}
skill.Name = "TestSkill"
// BasisWert should default to 0
if skill.BasisWert != 0 {
t.Errorf("Expected BasisWert to default to 0, got %d", skill.BasisWert)
}
}
func TestSkFertigkeitBasisWertSet(t *testing.T) {
// Test that BasisWert can be set
skill := SkFertigkeit{
Fertigkeitswert: 10,
BasisWert: 5,
}
skill.Name = "TestSkill"
if skill.BasisWert != 5 {
t.Errorf("Expected BasisWert to be 5, got %d", skill.BasisWert)
}
}
func TestSkWaffenfertigkeitBasisWert(t *testing.T) {
// Test that BasisWert works for SkWaffenfertigkeit (inherited from SkFertigkeit)
weaponSkill := SkWaffenfertigkeit{
SkFertigkeit: SkFertigkeit{
Fertigkeitswert: 8,
BasisWert: 3,
},
}
weaponSkill.Name = "TestWeaponSkill"
if weaponSkill.BasisWert != 3 {
t.Errorf("Expected BasisWert to be 3, got %d", weaponSkill.BasisWert)
}
}
func TestSkAngeboreneFertigkeitBasisWert(t *testing.T) {
// Test that BasisWert works for SkAngeboreneFertigkeit (inherited from SkFertigkeit)
innateSkill := SkAngeboreneFertigkeit{
SkFertigkeit: SkFertigkeit{
Fertigkeitswert: 12,
BasisWert: 7,
},
}
innateSkill.Name = "TestInnateSkill"
if innateSkill.BasisWert != 7 {
t.Errorf("Expected BasisWert to be 7, got %d", innateSkill.BasisWert)
}
}
+214 -24
View File
@@ -7,6 +7,7 @@
v-model="searchTerm"
:placeholder="`${$t('search')} ${$t('Skill')}...`"
/>
<button @click="startCreate" class="btn-primary">{{ $t('newSkill') }}</button>
</div>
</div>
@@ -69,6 +70,7 @@
</th>
<th class="cd-table-header">{{ $t('skill.difficulty') }}</th>
<th class="cd-table-header">{{ $t('skill.initialwert') }}</th>
<th class="cd-table-header">{{ $t('skill.basiswert') }}</th>
<th class="cd-table-header">{{ $t('skill.improvable') }}</th>
<th class="cd-table-header">{{ $t('skill.innateskill') }}</th>
<th class="cd-table-header">{{ $t('skill.description') }}</th>
@@ -79,30 +81,11 @@
</tr>
</thead>
<tbody>
<template v-for="(dtaItem, index) in filteredAndSortedSkills" :key="dtaItem.id">
<!-- Display Mode -->
<tr v-if="editingIndex !== index">
<td>{{ dtaItem.id || '' }}</td>
<td>{{ formatCategories(dtaItem.categories) }}</td>
<td>{{ dtaItem.name || '-' }}</td>
<td>{{ formatDifficulties(dtaItem.difficulties) }}</td>
<td>{{ dtaItem.initialwert || '0' }}</td>
<td><input type="checkbox" :checked="dtaItem.improvable" disabled /></td>
<td><input type="checkbox" :checked="dtaItem.innateskill" disabled /></td>
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ dtaItem.bonuseigenschaft || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td>{{ dtaItem.game_system || 'midgard' }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
</tr>
<!-- Edit Mode -->
<tr v-else>
<td><input v-model="editedItem.id" style="width:20px;" disabled /></td>
<!-- Create New Skill Row -->
<tr v-if="creatingNew">
<td>New</td>
<td colspan="11">
<!-- Expanded edit form -->
<!-- Create form -->
<div class="edit-form">
<div class="edit-row">
<div class="edit-field">
@@ -113,6 +96,10 @@
<label>{{ $t('skill.initialwert') }}:</label>
<input v-model.number="editedItem.initialwert" type="number" style="width:60px;" />
</div>
<div class="edit-field">
<label>{{ $t('skill.basiswert') }}:</label>
<input v-model.number="editedItem.basiswert" type="number" style="width:60px;" />
</div>
<div class="edit-field">
<label>{{ $t('skill.bonusskill') }}:</label>
<select v-model="editedItem.bonuseigenschaft" style="width:80px;">
@@ -183,7 +170,138 @@
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.difficulties') || 'Difficulties' }}:</label>
<label>{{ $t('skill.difficulty') || 'Difficulty' }}:</label>
<div class="difficulty-selects">
<div v-for="catId in editedItem.selectedCategories" :key="catId" class="difficulty-select">
<span>{{ getCategoryName(catId) }}:</span>
<select v-model="editedItem.categoryDifficulties[catId]" style="width:120px;">
<option v-for="diff in mdata.difficulties" :key="diff.id" :value="diff.id">
{{ diff.name }}
</option>
</select>
</div>
</div>
</div>
</div>
<div class="edit-actions">
<button @click="saveCreate" class="btn-save">{{ $t('createSkill') }}</button>
<button @click="cancelCreate" class="btn-cancel">Cancel</button>
</div>
</div>
</td>
</tr>
<template v-for="(dtaItem, index) in filteredAndSortedSkills" :key="dtaItem.id">
<!-- Display Mode -->
<tr v-if="editingIndex !== index">
<td>{{ dtaItem.id || '' }}</td>
<td>{{ formatCategories(dtaItem.categories) }}</td>
<td>{{ dtaItem.name || '-' }}</td>
<td>{{ formatDifficulties(dtaItem.difficulties) }}</td>
<td>{{ dtaItem.initialwert || '0' }}</td>
<td>{{ dtaItem.basiswert || '0' }}</td>
<td><input type="checkbox" :checked="dtaItem.improvable" disabled /></td>
<td><input type="checkbox" :checked="dtaItem.innateskill" disabled /></td>
<td>{{ dtaItem.beschreibung || '-' }}</td>
<td>{{ dtaItem.bonuseigenschaft || '-' }}</td>
<td>{{ formatQuelle(dtaItem) }}</td>
<td>{{ dtaItem.game_system || 'midgard' }}</td>
<td>
<button @click="startEdit(index)">Edit</button>
</td>
</tr>
<!-- Edit Mode -->
<tr v-else>
<td><input v-model="editedItem.id" style="width:20px;" disabled /></td>
<td colspan="11">
<!-- Expanded edit form -->
<div class="edit-form">
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.name') }}:</label>
<input v-model="editedItem.name" />
</div>
<div class="edit-field">
<label>{{ $t('skill.initialwert') }}:</label>
<input v-model.number="editedItem.initialwert" type="number" style="width:60px;" />
</div>
<div class="edit-field">
<label>{{ $t('skill.basiswert') }}:</label>
<input v-model.number="editedItem.basiswert" type="number" style="width:60px;" />
</div>
<div class="edit-field">
<label>{{ $t('skill.bonusskill') }}:</label>
<select v-model="editedItem.bonuseigenschaft" style="width:80px;">
<option value="">-</option>
<option value="St">St</option>
<option value="Gs">Gs</option>
<option value="Gw">Gw</option>
<option value="Ko">Ko</option>
<option value="In">In</option>
<option value="Zt">Zt</option>
<option value="Au">Au</option>
<option value="pA">pA</option>
<option value="Wk">Wk</option>
<option value="B">B</option>
</select>
</div>
</div>
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.improvable') }}:</label>
<input type="checkbox" v-model="editedItem.improvable" />
</div>
<div class="edit-field">
<label>{{ $t('skill.innateskill') }}:</label>
<input type="checkbox" v-model="editedItem.innateskill" />
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.description') }}:</label>
<input v-model="editedItem.beschreibung" />
</div>
</div>
<div class="edit-row">
<div class="edit-field">
<label>{{ $t('skill.quelle') }}:</label>
<select v-model="editedItem.sourceCode" style="width:100px;">
<option v-for="source in availableSources" :key="source.code" :value="source.code">
{{ source.code }}
</option>
</select>
</div>
<div class="edit-field">
<label>{{ $t('skill.page') || 'Page' }}:</label>
<input v-model.number="editedItem.page_number" type="number" style="width:60px;" />
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.categories') || 'Categories' }}:</label>
<div class="category-checkboxes">
<div v-for="category in mdata.categories" :key="category.id" class="category-checkbox">
<input
type="checkbox"
:value="category.id"
v-model="editedItem.selectedCategories"
@change="onCategoryToggle(category.id)"
/>
<label>{{ category.name }}</label>
</div>
</div>
</div>
</div>
<div class="edit-row">
<div class="edit-field full-width">
<label>{{ $t('skill.difficulty') || 'Difficulty' }}:</label>
<div class="difficulty-selects">
<div v-for="catId in editedItem.selectedCategories" :key="catId" class="difficulty-select">
<span>{{ getCategoryName(catId) }}:</span>
@@ -240,6 +358,7 @@ export default {
sortAsc: true,
editingIndex: -1,
editedItem: null,
creatingNew: false,
filterCategory: '',
filterDifficulty: '',
filterImprovable: '',
@@ -440,6 +559,7 @@ export default {
beschreibung: this.editedItem.beschreibung,
game_system: this.editedItem.game_system || 'midgard',
initialwert: this.editedItem.initialwert,
basiswert: this.editedItem.basiswert || 0,
bonuseigenschaft: this.editedItem.bonuseigenschaft,
improvable: this.editedItem.improvable,
innateskill: this.editedItem.innateskill,
@@ -485,6 +605,76 @@ export default {
this.filterImprovable = ''
this.filterInnateskill = ''
this.filterBonuseigenschaft = ''
},
startCreate() {
// Initialize new skill object with defaults
this.editedItem = {
name: '',
beschreibung: '',
game_system: 'midgard',
initialwert: 5,
basiswert: 0,
bonuseigenschaft: '',
improvable: true,
innateskill: false,
sourceCode: this.availableSources.length > 0 ? this.availableSources[0].code : '',
page_number: 0,
selectedCategories: [],
categoryDifficulties: {}
}
this.creatingNew = true
},
async saveCreate() {
try {
// Validate required fields
if (!this.editedItem.name) {
alert('Skill name is required')
return
}
// Find source ID from code
const source = this.availableSources.find(s => s.code === this.editedItem.sourceCode)
// Build category_difficulties array
const categoryDifficulties = this.editedItem.selectedCategories.map(catId => ({
category_id: catId,
difficulty_id: this.editedItem.categoryDifficulties[catId]
}))
const createData = {
name: this.editedItem.name,
beschreibung: this.editedItem.beschreibung,
game_system: this.editedItem.game_system || 'midgard',
initialwert: this.editedItem.initialwert,
basiswert: this.editedItem.basiswert || 0,
bonuseigenschaft: this.editedItem.bonuseigenschaft,
improvable: this.editedItem.improvable,
innateskill: this.editedItem.innateskill,
source_id: source ? source.id : null,
page_number: this.editedItem.page_number || 0,
category_difficulties: categoryDifficulties
}
const response = await API.post(
'/api/maintenance/skills-enhanced',
createData
)
// Add the new skill to the list
this.enhancedSkills.push(response.data)
// Hide the create dialog
this.creatingNew = false
this.editedItem = null
} catch (error) {
console.error('Failed to create skill:', error)
alert('Failed to create skill: ' + (error.response?.data?.error || error.message))
// Don't close dialog on error so user can fix the issue
}
},
cancelCreate() {
this.creatingNew = false
this.editedItem = null
}
}
}
+5
View File
@@ -107,7 +107,10 @@ export default {
improvable:'verbesserbar',
category:'Kategorie',
initialwert:'Startwert',
basiswert:'Basiswert (ungelernt)',
difficulty:'Schwierigkeit',
page:'Seite',
categories:'Kategorien',
},
spell:{
id:'ID',
@@ -232,6 +235,8 @@ export default {
},
search:'Suche',
Skill:'Fertigkeit',
newSkill:'Neue Fertigkeit',
createSkill:'Fertigkeit erstellen',
common: {
loading: 'Laden...',
cancel: 'Abbrechen',
+6 -1
View File
@@ -104,6 +104,9 @@ export default {
improvable:'improvable',
category:'Category',
initialwert:'Initial value',
basiswert:'Base value (untrained)',
page:'Page',
categories:'Categories',
},
spell:{
id:'ID',
@@ -124,7 +127,7 @@ export default {
quelle:'Source',
import: 'Import',
selectCsv: 'select CSV',
system: 'System'
system: 'System',
},
spells: {
learn: {
@@ -228,6 +231,8 @@ export default {
},
search:'Suche',
Skill:'Fertigkeit',
newSkill:'New Skill',
createSkill:'Create Skill',
common: {
loading: 'Laden...',
cancel: 'Abbrechen',
+36 -2
View File
@@ -15,7 +15,7 @@
</div>
<div class="action-links">
<router-link to="/login" class="btn btn-primary">
<router-link to="/login" class="btn btn-primary" :class="{ disabled: !isBackendAvailable }" :event="isBackendAvailable ? 'click' : ''">
{{ $t('landing.login') }}
</router-link>
<a :href="githubUrl" target="_blank" rel="noopener noreferrer" class="btn btn-secondary">
@@ -43,12 +43,28 @@ export default {
frontendCommit: getGitCommit(),
backendVersion: "Loading...",
backendCommit: "Loading...",
githubUrl: "https://github.com/Bardioc26/bamort"
githubUrl: "https://github.com/Bardioc26/bamort",
retryCount: 0,
maxRetries: 24,
retryInterval: null
}
},
mounted() {
this.fetchBackendVersion()
},
beforeUnmount() {
if (this.retryInterval) {
clearInterval(this.retryInterval)
}
},
computed: {
isBackendAvailable() {
return this.backendVersion !== "Loading..." &&
this.backendVersion !== "Unavailable" &&
this.backendVersion !== "Unreachable" &&
this.backendVersion !== "Unknown"
}
},
methods: {
async fetchBackendVersion() {
try {
@@ -58,11 +74,29 @@ export default {
if (response.data) {
this.backendVersion = response.data.version || "Unknown"
this.backendCommit = response.data.gitCommit || "Unknown"
if (this.retryInterval) {
clearInterval(this.retryInterval)
this.retryInterval = null
}
}
} catch (error) {
console.warn("Could not fetch backend version:", error)
this.backendVersion = "Unavailable"
this.backendCommit = "N/A"
if (this.retryCount < this.maxRetries && !this.retryInterval) {
this.retryInterval = setInterval(() => {
this.retryCount++
if (this.retryCount >= this.maxRetries) {
clearInterval(this.retryInterval)
this.retryInterval = null
console.warn("Max retries reached for backend version")
this.backendVersion = "Unreachable"
return
}
this.fetchBackendVersion()
}, 5000)
}
}
}
}
+4
View File
@@ -69,8 +69,12 @@ echo "1. Review changes: git diff"
if [ "$BACKEND_VERSION" = "$FRONTEND_VERSION" ]; then
echo "2. Commit changes: git commit -am 'Bump version to $BACKEND_VERSION'"
echo "3. Tag release: git tag v$BACKEND_VERSION"
git tag v$BACKEND_VERSION
else
echo "2. Commit changes: git commit -am 'Bump backend to $BACKEND_VERSION, frontend to $FRONTEND_VERSION'"
echo "3. Tag releases: git tag backend-v$BACKEND_VERSION && git tag frontend-v$FRONTEND_VERSION"
git tag backend-v$BACKEND_VERSION
git tag frontend-v$FRONTEND_VERSION
git tag v$BACKEND_VERSION -m "Backend version $BACKEND_VERSION, Frontend version $FRONTEND_VERSION"
fi
echo "4. Push: git push && git push --tags"