Updated Got to 1.25.5

made spells display correct spell information
This commit is contained in:
2025-12-19 17:04:20 +01:00
parent 59755c4516
commit f6f0b334c2
20 changed files with 222 additions and 187 deletions
+27 -27
View File
@@ -210,9 +210,9 @@ func GetCharacterExperienceAndWealth(c *gin.Context) {
// Berechne Gesamtvermögen in Silbergroschen
// Annahme: 1 GS = 10 SS, 1 SS = 10 KS (typische Midgard Währung)
gs := character.Vermoegen.Goldstücke
ss := character.Vermoegen.Silberstücke
ks := character.Vermoegen.Kupferstücke
gs := character.Vermoegen.Goldstuecke
ss := character.Vermoegen.Silberstuecke
ks := character.Vermoegen.Kupferstuecke
totalInSS := (gs * 10) + ss + (ks / 10)
response := ExperienceAndWealthResponse{
@@ -355,9 +355,9 @@ func UpdateCharacterWealth(c *gin.Context) {
oldSilver := 0
oldCopper := 0
if character.Vermoegen.ID != 0 {
oldGold = character.Vermoegen.Goldstücke
oldSilver = character.Vermoegen.Silberstücke
oldCopper = character.Vermoegen.Kupferstücke
oldGold = character.Vermoegen.Goldstuecke
oldSilver = character.Vermoegen.Silberstuecke
oldCopper = character.Vermoegen.Kupferstuecke
}
// Aktualisiere oder erstelle Vermögen
@@ -369,9 +369,9 @@ func UpdateCharacterWealth(c *gin.Context) {
CharacterID: character.ID,
UserID: userID,
},
Goldstücke: getValueOrDefault(req.Goldstücke, 0),
Silberstücke: getValueOrDefault(req.Silberstücke, 0),
Kupferstücke: getValueOrDefault(req.Kupferstücke, 0),
Goldstuecke: getValueOrDefault(req.Goldstücke, 0),
Silberstuecke: getValueOrDefault(req.Silberstücke, 0),
Kupferstuecke: getValueOrDefault(req.Kupferstücke, 0),
}
if err := database.DB.Create(&character.Vermoegen).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to create wealth record")
@@ -380,13 +380,13 @@ func UpdateCharacterWealth(c *gin.Context) {
} else {
// Aktualisiere existierendes Vermögen
if req.Goldstücke != nil {
character.Vermoegen.Goldstücke = *req.Goldstücke
character.Vermoegen.Goldstuecke = *req.Goldstücke
}
if req.Silberstücke != nil {
character.Vermoegen.Silberstücke = *req.Silberstücke
character.Vermoegen.Silberstuecke = *req.Silberstücke
}
if req.Kupferstücke != nil {
character.Vermoegen.Kupferstücke = *req.Kupferstücke
character.Vermoegen.Kupferstuecke = *req.Kupferstücke
}
if err := database.DB.Save(&character.Vermoegen).Error; err != nil {
respondWithError(c, http.StatusInternalServerError, "Failed to update wealth")
@@ -398,36 +398,36 @@ func UpdateCharacterWealth(c *gin.Context) {
// TODO: User-ID aus dem Authentifizierungs-Context holen
userID := uint(0) // Placeholder
if req.Goldstücke != nil && oldGold != character.Vermoegen.Goldstücke {
if req.Goldstücke != nil && oldGold != character.Vermoegen.Goldstuecke {
CreateAuditLogEntry(
character.ID,
"gold",
oldGold,
character.Vermoegen.Goldstücke,
character.Vermoegen.Goldstuecke,
AuditLogReason(req.Reason),
userID,
req.Notes,
)
}
if req.Silberstücke != nil && oldSilver != character.Vermoegen.Silberstücke {
if req.Silberstücke != nil && oldSilver != character.Vermoegen.Silberstuecke {
CreateAuditLogEntry(
character.ID,
"silver",
oldSilver,
character.Vermoegen.Silberstücke,
character.Vermoegen.Silberstuecke,
AuditLogReason(req.Reason),
userID,
req.Notes,
)
}
if req.Kupferstücke != nil && oldCopper != character.Vermoegen.Kupferstücke {
if req.Kupferstücke != nil && oldCopper != character.Vermoegen.Kupferstuecke {
CreateAuditLogEntry(
character.ID,
"copper",
oldCopper,
character.Vermoegen.Kupferstücke,
character.Vermoegen.Kupferstuecke,
AuditLogReason(req.Reason),
userID,
req.Notes,
@@ -437,9 +437,9 @@ func UpdateCharacterWealth(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Wealth updated successfully",
"wealth": gin.H{
"goldstücke": character.Vermoegen.Goldstücke,
"silberstücke": character.Vermoegen.Silberstücke,
"kupferstücke": character.Vermoegen.Kupferstücke,
"goldstücke": character.Vermoegen.Goldstuecke,
"silberstücke": character.Vermoegen.Silberstuecke,
"kupferstücke": character.Vermoegen.Kupferstuecke,
},
})
}
@@ -742,7 +742,7 @@ func deductResourcesForLearning(char *models.Char, skillName string, finalLevel,
// deductResourcesWithAuditReason zieht EP, Gold und PP ab und erstellt entsprechende Audit-Log-Einträge
func deductResourcesWithAuditReason(char *models.Char, itemName string, finalLevel, totalEP, totalGold, totalPP int, auditReason AuditLogReason) (int, int, error) {
currentEP := char.Erfahrungsschatz.EP
currentGold := char.Vermoegen.Goldstücke
currentGold := char.Vermoegen.Goldstuecke
// EP abziehen und Audit-Log erstellen
newEP := currentEP - totalEP
@@ -780,7 +780,7 @@ func deductResourcesWithAuditReason(char *models.Char, itemName string, finalLev
if err != nil {
return 0, 0, fmt.Errorf("fehler beim Erstellen des Audit-Log-Eintrags: %v", err)
}
char.Vermoegen.Goldstücke = newGold
char.Vermoegen.Goldstuecke = newGold
if err := database.DB.Save(&char.Vermoegen).Error; err != nil {
return 0, 0, fmt.Errorf("fehler beim Speichern des Vermögens: %v", err)
}
@@ -920,7 +920,7 @@ func validateResources(char *models.Char, skillName string, totalEP, totalGold,
}
// Prüfe, ob genügend Gold vorhanden ist
currentGold := char.Vermoegen.Goldstücke
currentGold := char.Vermoegen.Goldstuecke
if currentGold < totalGold {
return fmt.Errorf("Nicht genügend Gold vorhanden")
}
@@ -953,7 +953,7 @@ func validateResources(char *models.Char, skillName string, totalEP, totalGold,
// TODO Fehlerbehandlung (Falls Tabelle nicht vorhanden ist)
func deductResources(char *models.Char, skillName string, currentLevel, finalLevel, totalEP, totalGold, totalPP int) (int, int, error) {
currentEP := char.Erfahrungsschatz.EP
currentGold := char.Vermoegen.Goldstücke
currentGold := char.Vermoegen.Goldstuecke
// EP abziehen und Audit-Log erstellen
newEP := currentEP - totalEP
@@ -986,7 +986,7 @@ func deductResources(char *models.Char, skillName string, currentLevel, finalLev
if err != nil {
return newEP, newGold, fmt.Errorf("Fehler beim Erstellen des Audit-Log-Eintrags: %v", err)
}
char.Vermoegen.Goldstücke = newGold
char.Vermoegen.Goldstuecke = newGold
if err := database.DB.Save(&char.Vermoegen).Error; err != nil {
return newEP, newGold, fmt.Errorf("Fehler beim Speichern des Vermögens: %v", err)
}
@@ -2769,7 +2769,7 @@ func FinalizeCharacterCreation(c *gin.Context) {
BamortCharTrait: models.BamortCharTrait{
UserID: userID,
},
Goldstücke: 80,
Goldstuecke: 80,
},
// Bennies (Glückspunkte, etc.)
+2 -2
View File
@@ -154,11 +154,11 @@ func TestImproveSkillHandler(t *testing.T) {
assert.Equal(t, 250, updatedChar.Erfahrungsschatz.EP, "Character should have 316 EP remaining")
// Check that Gold was deducted correctly
assert.Equal(t, 290, updatedChar.Vermoegen.Goldstücke, "Character should have 370 Gold remaining")
assert.Equal(t, 290, updatedChar.Vermoegen.Goldstuecke, "Character should have 370 Gold remaining")
t.Logf("Test completed successfully!")
t.Logf("EP: %d -> %d (cost: %.0f)", 326, updatedChar.Erfahrungsschatz.EP, response["ep_cost"])
t.Logf("Gold: %d -> %d (cost: %.0f)", 390, updatedChar.Vermoegen.Goldstücke, response["gold_cost"])
t.Logf("Gold: %d -> %d (cost: %.0f)", 390, updatedChar.Vermoegen.Goldstuecke, response["gold_cost"])
})
}
+4 -4
View File
@@ -37,14 +37,14 @@ func TestLearnSpell(t *testing.T) {
database.DB.Model(&character).Where("id = ?", 18).Update("erfahrungsschatz", character.Erfahrungsschatz)
}
if character.Vermoegen.Goldstücke < 200 {
character.Vermoegen.Goldstücke = 500
if character.Vermoegen.Goldstuecke < 200 {
character.Vermoegen.Goldstuecke = 500
database.DB.Model(&character).Where("id = ?", 18).Update("vermoegen", character.Vermoegen)
}
// Store initial resources for comparison
initialEP := character.Erfahrungsschatz.EP
initialGold := character.Vermoegen.Goldstücke
initialGold := character.Vermoegen.Goldstuecke
// Create LernCostRequest (new format)
request := map[string]interface{}{
@@ -294,7 +294,7 @@ func TestLearnSpell(t *testing.T) {
BamortCharTrait: models.BamortCharTrait{
CharacterID: 22,
},
Goldstücke: 10, // Insufficient gold
Goldstuecke: 10, // Insufficient gold
},
}
+3 -3
View File
@@ -6,8 +6,11 @@ go 1.24.0
toolchain go1.24.4
require (
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
github.com/chromedp/chromedp v0.14.2
github.com/gin-contrib/cors v1.7.3
github.com/gin-gonic/gin v1.10.0
github.com/pdfcpu/pdfcpu v0.11.1
github.com/stretchr/testify v1.10.0
gorm.io/driver/mysql v1.5.7
gorm.io/driver/sqlite v1.5.7
@@ -18,8 +21,6 @@ require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bytedance/sonic v1.12.8 // indirect
github.com/bytedance/sonic/loader v0.2.3 // indirect
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d // indirect
github.com/chromedp/chromedp v0.14.2 // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
@@ -49,7 +50,6 @@ require (
github.com/mattn/go-sqlite3 v1.14.24 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pdfcpu/pdfcpu v0.11.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
+4 -10
View File
@@ -72,6 +72,8 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -85,6 +87,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas=
github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
@@ -113,25 +117,15 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/arch v0.13.0 h1:KCkqVVV1kGg0X87TFysjCJ8MxtZEIU4Ja/yXGeoECdA=
golang.org/x/arch v0.13.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ=
golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+3 -3
View File
@@ -69,9 +69,9 @@ type Bennies struct {
type Vermoegen struct {
BamortCharTrait
Goldstücke int `json:"goldstücke"` // GS
Silberstücke int `json:"silberstücke"` // SS
Kupferstücke int `json:"kupferstücke"` // KS
Goldstuecke int `json:"goldstücke"` // GS
Silberstuecke int `json:"silberstücke"` // SS
Kupferstuecke int `json:"kupferstücke"` // KS
}
type Char struct {
+10 -10
View File
@@ -79,9 +79,9 @@ func createTestChar(name string) *Char {
Sg: 0,
},
Vermoegen: Vermoegen{
Goldstücke: 100,
Silberstücke: 50,
Kupferstücke: 25,
Goldstuecke: 100,
Silberstuecke: 50,
Kupferstuecke: 25,
},
Erfahrungsschatz: Erfahrungsschatz{
ES: 150,
@@ -435,7 +435,7 @@ func TestChar_CreateWithCompleteData(t *testing.T) {
assert.Equal(t, testChar.B.Max, foundChar.B.Max)
assert.Equal(t, testChar.Merkmale.Augenfarbe, foundChar.Merkmale.Augenfarbe)
assert.Equal(t, testChar.Bennies.Gg, foundChar.Bennies.Gg)
assert.Equal(t, testChar.Vermoegen.Goldstücke, foundChar.Vermoegen.Goldstücke)
assert.Equal(t, testChar.Vermoegen.Goldstuecke, foundChar.Vermoegen.Goldstuecke)
assert.Equal(t, testChar.Erfahrungsschatz.ES, foundChar.Erfahrungsschatz.ES)
}
@@ -467,9 +467,9 @@ func TestChar_WealthManagement(t *testing.T) {
setupCharacterTestDB(t)
testChar := createTestChar("Wealthy Character")
testChar.Vermoegen.Goldstücke = 1000
testChar.Vermoegen.Silberstücke = 500
testChar.Vermoegen.Kupferstücke = 100
testChar.Vermoegen.Goldstuecke = 1000
testChar.Vermoegen.Silberstuecke = 500
testChar.Vermoegen.Kupferstuecke = 100
err := testChar.Create()
require.NoError(t, err, "Character creation should succeed")
@@ -479,9 +479,9 @@ func TestChar_WealthManagement(t *testing.T) {
err = foundChar.First(testChar.Name)
require.NoError(t, err, "Character should be found")
assert.Equal(t, 1000, foundChar.Vermoegen.Goldstücke, "Gold should match")
assert.Equal(t, 500, foundChar.Vermoegen.Silberstücke, "Silver should match")
assert.Equal(t, 100, foundChar.Vermoegen.Kupferstücke, "Copper should match")
assert.Equal(t, 1000, foundChar.Vermoegen.Goldstuecke, "Gold should match")
assert.Equal(t, 500, foundChar.Vermoegen.Silberstuecke, "Silver should match")
assert.Equal(t, 100, foundChar.Vermoegen.Kupferstuecke, "Copper should match")
}
func TestChar_EdgeCases(t *testing.T) {
+29 -20
View File
@@ -1,6 +1,7 @@
package pdfrender
import (
"bamort/database"
"bamort/models"
"os"
"strings"
@@ -48,6 +49,11 @@ func TestIntegration_FullPDFGeneration(t *testing.T) {
B: models.B{
Value: 15,
},
Vermoegen: models.Vermoegen{
Goldstuecke: 100,
Silberstuecke: 50,
Kupferstuecke: 25,
},
Fertigkeiten: []models.SkFertigkeit{
{
BamortCharTrait: models.BamortCharTrait{
@@ -151,9 +157,9 @@ func TestIntegration_TemplateMetadata(t *testing.T) {
expectedMax int
}{
{"page1_stats.html", "skills_column1", 29},
{"page2_play.html", "skills_learned", 18}, // From template: MAX: 18
{"page3_spell.html", "spells_left", 26}, // From template: MAX: 26
{"page3_spell.html", "spells_right", 15}, // From template: MAX: 15
{"page2_play.html", "skills_learned", 17}, // From template: MAX: 17
{"page3_spell.html", "spells_left", 15}, // From template: MAX: 15
{"page3_spell.html", "spells_right", 10}, // From template: MAX: 10
{"page4_equip.html", "equipment_worn", 10},
}
@@ -279,10 +285,10 @@ func TestIntegration_MultiPageSpellList(t *testing.T) {
for i := 0; i < 30; i++ {
spells[i] = SpellViewModel{
Name: "Zauber Nr. " + string(rune('A'+i%26)),
AP: 5,
Category: 1,
Duration: "1 Minute",
CastTime: "1 sec",
AP: "5",
Stufe: 1,
Wirkungsdauer: "1 Minute",
Zauberdauer: "1 sec",
}
}
@@ -296,21 +302,21 @@ func TestIntegration_MultiPageSpellList(t *testing.T) {
t.Fatalf("Failed to paginate spells: %v", err)
}
// With 30 capacity (20+10), should create 1 page for 30 spells
// With 25 capacity (15+10), 30 spells should need 2 pages
// Verify distribution
// With 20+10 capacity, 30 spells should fit on 1 page
if len(pages) != 1 {
t.Fatalf("Expected 1 page for 30 spells, got %d", len(pages))
// With 15+10 capacity, 30 spells should need 2 pages
if len(pages) != 2 {
t.Fatalf("Expected 2 pages for 30 spells, got %d", len(pages))
}
// Page 1: 20 (left) + 10 (right) = 30 spells
// Page 1: 15 (left) + 10 (right) = 25 spells
leftPage1 := pages[0].Data["spells_left"].([]SpellViewModel)
rightPage1 := pages[0].Data["spells_right"].([]SpellViewModel)
totalPage1 := len(leftPage1) + len(rightPage1)
if totalPage1 != 30 {
t.Errorf("Expected 30 spells on page 1 (20+10), got %d", totalPage1)
if totalPage1 != 25 {
t.Errorf("Expected 25 spells on page 1 (15+10), got %d", totalPage1)
}
t.Logf("Successfully distributed 30 spells: Page 1 has %d (left %d, right %d)", totalPage1, len(leftPage1), len(rightPage1))
@@ -451,6 +457,7 @@ func TestIntegration_CompleteWorkflow(t *testing.T) {
// TestVisualInspection_AllPages generates all 4 page types and saves them to disk
// Run with: go test -v ./pdfrender/... -run TestVisualInspection
func TestVisualInspection_AllPages(t *testing.T) {
database.SetupTestDB()
// Create a rich character with data for all page types
char := &models.Char{
BamortBase: models.BamortBase{
@@ -479,6 +486,11 @@ func TestVisualInspection_AllPages(t *testing.T) {
Lp: models.Lp{Value: 42, Max: 48},
Ap: models.Ap{Value: 28, Max: 32},
B: models.B{Value: 18},
Vermoegen: models.Vermoegen{
Goldstuecke: 100,
Silberstuecke: 50,
Kupferstuecke: 2,
},
}
// Add skills
@@ -486,11 +498,9 @@ func TestVisualInspection_AllPages(t *testing.T) {
"Schwimmen", "Klettern", "Reiten", "Laufen", "Springen",
"Balancieren", "Schleichen", "Sich Verstecken", "Singen",
"Tanzen", "Musizieren", "Malen", "Kochen", "Erste Hilfe",
"Himmelskunde", "Pflanzenkunde", "Tierkunde", "Geografie",
"Himmelskunde", "Pflanzenkunde", "Tierkunde", "Heilkunde",
"Geschichte", "Lesen/Schreiben", "Rechnen", "Schätzen",
"Heilkunde", "Giftmischen", "Alchimie", "Schmieden",
"Lederarbeiten", "Holzbearbeitung", "Steinmetzkunst",
"Schlösser öffnen", "Fallen entschärfen", "Taschendiebstahl",
"Heilkunde", "Giftmischen", "Alchimie", "Schlösser öffnen",
}
char.Fertigkeiten = make([]models.SkFertigkeit, len(skillNames))
for i, name := range skillNames {
@@ -526,8 +536,7 @@ func TestVisualInspection_AllPages(t *testing.T) {
"Macht über die belebte Natur", "Macht über das Selbst",
"Erkennen von Gift", "Heilen von Wunden", "Bannen von Zauberwerk",
"Schutz vor Dämonen", "Macht über Unbelebtes", "Angst",
"Unsichtbarkeit", "Feuerlanze", "Eisstrahl", "Blitz",
"Verwandlung", "Teleportation", "Hellsicht",
"Unsichtbarkeit", "Feuerlanze",
}
char.Zauber = make([]models.SkZauber, len(spellNames))
for i, name := range spellNames {
+47 -4
View File
@@ -1,6 +1,9 @@
package pdfrender
import (
"fmt"
"bamort/database"
"bamort/models"
)
@@ -28,6 +31,11 @@ func MapCharacterToViewModel(char *models.Char) (*CharacterSheetViewModel, error
Religion: char.Glaube,
Stand: char.SocialClass,
IconBase64: "", // Will be set later if image exists
Vermoegen: WealthInfo{
Goldstuecke: char.Vermoegen.Goldstuecke,
Silberstuecke: char.Vermoegen.Silberstuecke,
Kupferstuecke: char.Vermoegen.Kupferstuecke,
},
}
// Map attributes
@@ -129,10 +137,45 @@ func mapWeapons(char *models.Char) []WeaponViewModel {
func mapSpells(char *models.Char) []SpellViewModel {
spells := make([]SpellViewModel, 0, len(char.Zauber))
for _, spell := range char.Zauber {
spells = append(spells, SpellViewModel{
Name: spell.Name,
})
for _, charSpell := range char.Zauber {
vm := SpellViewModel{
Name: charSpell.Name,
Bonus: charSpell.Bonus,
}
// Try to load spell details from gsmaster (only if database is available)
// In test environments without DB, we skip this enrichment
if database.DB != nil {
masterSpell := &models.Spell{}
err := masterSpell.First(charSpell.Name)
// If master spell found, add all details
if err == nil && masterSpell.ID > 0 {
vm.Stufe = masterSpell.Stufe
vm.AP = masterSpell.AP
vm.Art = masterSpell.Art
vm.Zauberdauer = masterSpell.Zauberdauer
vm.Reichweite = masterSpell.Reichweite
vm.Wirkungsziel = masterSpell.Wirkungsziel
vm.Wirkungsbereich = masterSpell.Wirkungsbereich
vm.Wirkungsdauer = masterSpell.Wirkungsdauer
vm.Ursprung = masterSpell.Ursprung
vm.Category = masterSpell.Category
vm.LearningCategory = masterSpell.LearningCategory
vm.Beschreibung = masterSpell.Beschreibung
// If description is empty, use source code and page number
if vm.Beschreibung == "" && masterSpell.SourceID > 0 && masterSpell.PageNumber > 0 {
source := &models.Source{}
if err := database.DB.First(source, masterSpell.SourceID).Error; err == nil {
vm.Beschreibung = source.Code + " S." + fmt.Sprintf("%d", masterSpell.PageNumber)
}
}
}
}
// If spell details not found or DB not available, just use name and bonus from character
spells = append(spells, vm)
}
return spells
+23 -23
View File
@@ -243,20 +243,20 @@ func TestPaginateSpells_MultiPage(t *testing.T) {
t.Fatalf("Expected no error, got %v", err)
}
// With capacity of 26+15=41 (from template), all 30 spells fit on 1 page
if len(pages) != 1 {
t.Fatalf("Expected 1 page, got %d", len(pages))
// With capacity of 15+10=25 (from template), 30 spells require 2 pages
if len(pages) != 2 {
t.Fatalf("Expected 2 pages, got %d", len(pages))
}
// Page 1 should have all 30 spells (26 left + 4 right, since we only have 30 total)
// Page 1 should have 25 spells (15 left + 10 right)
page1 := pages[0]
leftPage1 := page1.Data["spells_left"].([]SpellViewModel)
rightPage1 := page1.Data["spells_right"].([]SpellViewModel)
if len(leftPage1) != 26 {
t.Errorf("Page 1 left: expected 26 spells (template capacity), got %d", len(leftPage1))
if len(leftPage1) != 15 {
t.Errorf("Page 1 left: expected 15 spells (template capacity), got %d", len(leftPage1))
}
if len(rightPage1) != 4 {
t.Errorf("Page 1 right: expected 4 spells (remaining from 30), got %d", len(rightPage1))
if len(rightPage1) != 10 {
t.Errorf("Page 1 right: expected 10 spells (template capacity), got %d", len(rightPage1))
}
}
@@ -294,7 +294,7 @@ func TestPaginateWeapons_MultiPage(t *testing.T) {
templateSet := DefaultA4QuerTemplateSet()
paginator := NewPaginator(templateSet)
// Create 50 weapons - should span 3 pages (24 capacity per page from template)
// Create 50 weapons - should span 3 pages (22 capacity per page from template)
weapons := make([]WeaponViewModel, 50)
for i := 0; i < 50; i++ {
weapons[i] = WeaponViewModel{Name: "Weapon" + string(rune(i))}
@@ -309,25 +309,25 @@ func TestPaginateWeapons_MultiPage(t *testing.T) {
}
if len(pages) != 3 {
t.Fatalf("Expected 3 pages (24+24+2 from template capacity), got %d", len(pages))
t.Fatalf("Expected 3 pages (22+22+6 from template capacity), got %d", len(pages))
}
// Page 1 should have 24 weapons
// Page 1 should have 22 weapons
page1Weapons := pages[0].Data["weapons_main"].([]WeaponViewModel)
if len(page1Weapons) != 24 {
t.Errorf("Page 1: expected 24 weapons (template capacity), got %d", len(page1Weapons))
if len(page1Weapons) != 22 {
t.Errorf("Page 1: expected 22 weapons (template capacity), got %d", len(page1Weapons))
}
// Page 2 should have 24 weapons
// Page 2 should have 22 weapons
page2Weapons := pages[1].Data["weapons_main"].([]WeaponViewModel)
if len(page2Weapons) != 24 {
t.Errorf("Page 2: expected 24 weapons (template capacity), got %d", len(page2Weapons))
if len(page2Weapons) != 22 {
t.Errorf("Page 2: expected 22 weapons (template capacity), got %d", len(page2Weapons))
}
// Page 3 should have 2 weapons (remaining)
// Page 3 should have 6 weapons (remaining)
page3Weapons := pages[2].Data["weapons_main"].([]WeaponViewModel)
if len(page3Weapons) != 2 {
t.Errorf("Page 3: expected 2 weapons (remaining), got %d", len(page3Weapons))
if len(page3Weapons) != 6 {
t.Errorf("Page 3: expected 6 weapons (remaining), got %d", len(page3Weapons))
}
}
@@ -348,11 +348,11 @@ func TestCalculatePagesNeeded(t *testing.T) {
{"59 skills on page1", "page1_stats.html", "skills", 59, 2}, // 59 requires 2 pages
{"100 skills on page1", "page1_stats.html", "skills", 100, 2},
{"10 weapons on page2", "page2_play.html", "weapons", 10, 1},
{"24 weapons on page2", "page2_play.html", "weapons", 24, 1}, // MAX:24 from template
{"25 weapons on page2", "page2_play.html", "weapons", 25, 2}, // exceeds capacity
{"22 weapons on page2", "page2_play.html", "weapons", 22, 1}, // MAX:22 from template
{"23 weapons on page2", "page2_play.html", "weapons", 23, 2}, // exceeds capacity
{"10 spells on page3", "page3_spell.html", "spells", 10, 1},
{"41 spells on page3", "page3_spell.html", "spells", 41, 1}, // 26+15 = 41 fits on 1 page (from template)
{"42 spells on page3", "page3_spell.html", "spells", 42, 2}, // 42 requires 2 pages
{"25 spells on page3", "page3_spell.html", "spells", 25, 1}, // 15+10 = 25 fits on 1 page (from template)
{"26 spells on page3", "page3_spell.html", "spells", 26, 2}, // 26 requires 2 pages
}
for _, tc := range testCases {
+2 -2
View File
@@ -153,7 +153,7 @@ func getHardcodedTemplateSet() TemplateSet {
{
Name: "weapons_main",
ListType: "weapons",
MaxItems: 30,
MaxItems: 22,
},
},
},
@@ -168,7 +168,7 @@ func getHardcodedTemplateSet() TemplateSet {
{
Name: "spells_left",
ListType: "spells",
MaxItems: 20,
MaxItems: 15,
Column: 1,
},
{
@@ -89,9 +89,9 @@ func TestDefaultA4QuerTemplateSet_LoadsFromFiles(t *testing.T) {
if spellsLeft == nil {
t.Error("spells_left block not found")
} else {
// Should be 26 from the template file (<!-- BLOCK: spells_left, TYPE: spells, MAX: 26 -->)
if spellsLeft.MaxItems != 26 {
t.Errorf("Expected spells_left MaxItems 26 (from template file), got %d", spellsLeft.MaxItems)
// Should be 15 from the template file (<!-- BLOCK: spells_left, TYPE: spells, MAX: 15 -->)
if spellsLeft.MaxItems != 15 {
t.Errorf("Expected spells_left MaxItems 15 (from template file), got %d", spellsLeft.MaxItems)
}
}
}
+6 -6
View File
@@ -82,23 +82,23 @@ func TestGetTemplateMetadata(t *testing.T) {
t.Fatal("Expected metadata blocks, got none")
}
// Check for spells_left block (template says MAX: 26)
// Check for spells_left block (template says MAX: 15)
leftBlock := GetBlockByName(metadata, "spells_left")
if leftBlock == nil {
t.Error("Expected to find 'spells_left' block")
} else {
if leftBlock.MaxItems != 26 {
t.Errorf("Expected spells_left max 26 (from template), got %d", leftBlock.MaxItems)
if leftBlock.MaxItems != 15 {
t.Errorf("Expected spells_left max 15 (from template), got %d", leftBlock.MaxItems)
}
}
// Check for spells_right block (template says MAX: 15)
// Check for spells_right block (template says MAX: 10)
rightBlock := GetBlockByName(metadata, "spells_right")
if rightBlock == nil {
t.Error("Expected to find 'spells_right' block")
} else {
if rightBlock.MaxItems != 15 {
t.Errorf("Expected spells_right max 15 (from template), got %d", rightBlock.MaxItems)
if rightBlock.MaxItems != 10 {
t.Errorf("Expected spells_right max 10 (from template), got %d", rightBlock.MaxItems)
}
}
}
+1 -25
View File
@@ -1,29 +1,5 @@
* ✓ Template meta data must NOT be hard coded (func DefaultA4QuerTemplateSet ) but must be read from theTemplate itself.
OR if default values must be defined they must be overwritten from the statements fount in the template
**COMPLETED**: DefaultA4QuerTemplateSet() now calls LoadTemplateSetFromFiles() which parses HTML comments.
Falls back to hardcoded values if files can't be read.
* ✓ fill table with empty lines up to the my max value
**COMPLETED**: Added FillToCapacity() function, integrated into PreparePaginatedPageData().
All lists now filled to MAX capacity from template metadata.
* ✓ the tests for pagination must take the max values to test against from the template or from the metadata already updated by loading from template
**COMPLETED**: All tests updated to read MAX values from templates dynamically using GetBlockCapacity().
Tests no longer hardcode expected values.
* ✓ paginating does not work in page 2 for
<!-- BLOCK: skills_learned, TYPE: skills, MAX: 18, FILTER: learned -->
<!-- BLOCK: skills_unlearned, TYPE: skills, MAX: 15, FILTER: unlearned -->
**COMPLETED**: PreparePaginatedPageData() now reads capacities from template metadata using GetBlockCapacity().
Page2 correctly uses MAX:18 for skills_learned, MAX:15 for skills_unlearned, MAX:5 for skills_languages, MAX:24 for weapons_main.
* ✓ FillToCapacity() does not work
in page 2 for <!-- BLOCK: skills_languages, TYPE: skills, MAX: 5, FILTER: language -->
in page 3 for <!-- BLOCK: magic_items, TYPE: magicItems, MAX: 8 -->
**COMPLETED**: Fixed PreparePaginatedPageData() to read correct MAX values from templates.
skills_languages now uses MAX:5 (not 11), magic_items now uses MAX:8 (not 5).
All blocks properly filled to capacity with empty rows.
* weapons_main list currently uses Waffenfertigkeiten (weapon skills).
NOTE: Equipment.Weapons (EqWaffe) contains physical weapons with metadata like Abwb/Schb.
Waffenfertigkeiten already provides EW (Fertigkeitswert) which is the skill value needed for the character sheet.
Current implementation is correct - weapons_main shows weapon skills with their EW values.
* The implementation is NOT correct, because the eapons_main shows weapon skills. But it should show the weapons list from the equipment
+14 -14
View File
@@ -28,24 +28,24 @@ func TestPaginationUsesTemplateMetadata(t *testing.T) {
if skillsLearned == nil {
t.Fatal("skills_learned block not found")
}
if skillsLearned.MaxItems != 18 {
t.Errorf("skills_learned: expected MAX 18 from template, got %d", skillsLearned.MaxItems)
if skillsLearned.MaxItems != 17 {
t.Errorf("skills_learned: expected MAX 17 from template, got %d", skillsLearned.MaxItems)
}
skillsLanguages := GetBlockByName(page2.Metadata.Blocks, "skills_languages")
if skillsLanguages == nil {
t.Fatal("skills_languages block not found")
}
if skillsLanguages.MaxItems != 5 {
t.Errorf("skills_languages: expected MAX 5 from template, got %d", skillsLanguages.MaxItems)
if skillsLanguages.MaxItems != 4 {
t.Errorf("skills_languages: expected MAX 4 from template, got %d", skillsLanguages.MaxItems)
}
weaponsMain := GetBlockByName(page2.Metadata.Blocks, "weapons_main")
if weaponsMain == nil {
t.Fatal("weapons_main block not found")
}
if weaponsMain.MaxItems != 24 {
t.Errorf("weapons_main: expected MAX 24 from template, got %d", weaponsMain.MaxItems)
if weaponsMain.MaxItems != 22 {
t.Errorf("weapons_main: expected MAX 22 from template, got %d", weaponsMain.MaxItems)
}
}
@@ -70,16 +70,16 @@ func TestPage2PaginationWithCorrectCapacities(t *testing.T) {
}
// Verify capacities match template (18, 5, 24)
if len(pageData.SkillsLearned) != 18 {
t.Errorf("SkillsLearned should be filled to 18, got %d", len(pageData.SkillsLearned))
if len(pageData.SkillsLearned) != 17 {
t.Errorf("SkillsLearned should be filled to 17, got %d", len(pageData.SkillsLearned))
}
if len(pageData.SkillsLanguage) != 5 {
t.Errorf("SkillsLanguage should be filled to 5, got %d", len(pageData.SkillsLanguage))
if len(pageData.SkillsLanguage) != 4 {
t.Errorf("SkillsLanguage should be filled to 4, got %d", len(pageData.SkillsLanguage))
}
if len(pageData.Weapons) != 24 {
t.Errorf("Weapons should be filled to 24, got %d", len(pageData.Weapons))
if len(pageData.Weapons) != 22 {
t.Errorf("Weapons should be filled to 22, got %d", len(pageData.Weapons))
}
}
@@ -101,8 +101,8 @@ func TestPage3MagicItemsCapacity(t *testing.T) {
}
// Template says MAX: 8 for magic_items
if len(pageData.MagicItems) != 8 {
t.Errorf("MagicItems should be filled to 8, got %d", len(pageData.MagicItems))
if len(pageData.MagicItems) != 5 {
t.Errorf("MagicItems should be filled to 5, got %d", len(pageData.MagicItems))
}
}
+21 -10
View File
@@ -31,6 +31,14 @@ type CharacterInfo struct {
Homeland string
Religion string
Stand string // Sozialer Stand
Vermoegen WealthInfo
}
// WealthInfo contains character wealth/money
type WealthInfo struct {
Goldstuecke int
Silberstuecke int
Kupferstuecke int
}
// AttributeValues contains all character attributes
@@ -111,16 +119,19 @@ type WeaponViewModel struct {
// SpellViewModel represents a spell for display
type SpellViewModel struct {
Name string
AP int // Abenteuerpunkte
Category int // Kategorie (z.B. "Beherrschen", "Erkennen")
//CastValue int // Zauberwert
CastTime string // Zauberdauer (z.B. "1 sec", "10 min")
Range string // Reichweite (z.B. "0", "30m")
Scope string // Wirkungsbereich (z.B. "1-10 Wesen", "Kegel 5m", "Zauberer", "m²", ...)
Duration string // Wirkungsdauer (z.B. "0", "10 min")
Objective string // wirkungsziel (z.B. Körper, Geist, Umgebung)
CastingType string // Art des Zaubers (z.B. "Geste", "Wort", "Gedanke")
Notes string // Notizen/Besonderheiten
Bonus int // Character's bonus for this spell
Stufe int // Spell level
AP string // Abenteuerpunkte cost
Art string // Art des Zaubers (z.B. "Gestenzauber", "Wortzauber")
Zauberdauer string // Zauberdauer (z.B. "1 sec", "10 min")
Reichweite string // Reichweite (z.B. "0", "30m")
Wirkungsziel string // Wirkungsziel (z.B. Körper, Geist, Umgebung)
Wirkungsbereich string // Wirkungsbereich (z.B. "1-10 Wesen", "Kegel 5m")
Wirkungsdauer string // Wirkungsdauer (z.B. "0", "10 min")
Ursprung string // Origin/source of the spell
Category string // Spell school/category
LearningCategory string // Learning category
Beschreibung string // Description
}
// MagicItemViewModel represents a magical item
@@ -27,7 +27,7 @@
<!-- Left section: Character icon, currency, and small containers -->
<div class="equipment-left">
<div class="char-icon-section">
<img src="token_bebe.png" alt="Charakter" class="char-icon">
<img src="shared/images/token_bebe.png" alt="Charakter" class="char-icon">
</div>
<table class="currency-table">
<tr>
@@ -26,7 +26,7 @@
<div class="flex main-content">
<!-- Left spell table -->
<div class="left-section">
<!-- BLOCK: spells_left, TYPE: spells, MAX: 26 -->
<!-- BLOCK: spells_left, TYPE: spells, MAX: 15 -->
<table class="spells-table">
<tr>
<th>AP<hr>Prozess *</th>
@@ -38,12 +38,12 @@
</tr>
{{range .SpellsLeft}}
<tr>
<td>{{.AP}}<hr>{{.Notes}}</td>
<td>{{.AP}}<hr>{{.Category}}&nbsp;</td>
<td>{{.Name}}</td>
<td>{{.CastTime}}<hr>{{.Range}}</td>
<td>{{.Scope}}<hr>{{.Duration}}</td>
<td>{{.Notes}}</td>
<td>{{.Objective}}<hr>{{.CastingType}}</td>
<td>{{.Zauberdauer}}&nbsp;<hr>{{.Reichweite}}&nbsp;</td>
<td>{{.Wirkungsbereich}}&nbsp;<hr>{{.Wirkungsdauer}}&nbsp;</td>
<td>{{.Beschreibung}}&nbsp;</td>
<td>{{.Wirkungsziel}}&nbsp;<hr>{{.Art}}&nbsp;</td>
</tr>
{{end}}
</table>
@@ -51,7 +51,7 @@
<!-- Right spell table -->
<div class="right-section">
<!-- BLOCK: spells_right, TYPE: spells, MAX: 15 -->
<!-- BLOCK: spells_right, TYPE: spells, MAX: 10 -->
<table class="spells-table">
<tr>
<th>AP<br>Prozess *</th>
@@ -63,18 +63,18 @@
</tr>
{{range .SpellsRight}}
<tr>
<td>{{.AP}}<hr>{{.Notes}}</td>
<td>{{.AP}}&nbsp;<hr>{{.Category}}&nbsp;</td>
<td>{{.Name}}</td>
<td>{{.CastTime}}<hr>{{.Range}}</td>
<td>{{.Scope}}<hr>{{.Duration}}</td>
<td>{{.Notes}}</td>
<td>{{.Objective}}<hr>{{.CastingType}}</td>
<td>{{.Zauberdauer}}&nbsp;<hr>{{.Reichweite}}&nbsp;</td>
<td>{{.Wirkungsbereich}}&nbsp;<hr>{{.Wirkungsdauer}}&nbsp;</td>
<td>{{.Beschreibung}}&nbsp;</td>
<td>{{.Wirkungsziel}}&nbsp;<hr>{{.Art}}&nbsp;</td>
</tr>
{{end}}
</table>
<div class="spell-footer">
<p><em>wichtige magische Gegenstände, Tränke, Schriftrollen</em></p>
<!-- BLOCK: magic_items, TYPE: magicItems, MAX: 8 -->
<!-- BLOCK: magic_items, TYPE: magicItems, MAX: 5 -->
<table class="items-table">
<tr>
<th>Gegenstand</th>
@@ -29,22 +29,24 @@
<div class="char-icon-section">
{{if .Character.IconBase64}}
<img src="{{.Character.IconBase64}}" alt="Charakter" class="char-icon">
{{else}}
<img src="shared/images/token_bebe.png" alt="Charakter" class="char-icon">
{{end}}
</div>
<table class="currency-table">
<tr>
<td><strong>GS</strong></td>
<td>0</td>
<td>{{.Character.Vermoegen.Goldstuecke}}</td>
{{range $i := iterate 9}}<td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</td>{{end}}
</tr>
<tr>
<td><strong>SS</strong></td>
<td>0</td>
<td>{{.Character.Vermoegen.Silberstuecke}}</td>
{{range $i := iterate 9}}<td></td>{{end}}
</tr>
<tr>
<td><strong>KS</strong></td>
<td>0</td>
<td>{{.Character.Vermoegen.Kupferstuecke}}</td>
{{range $i := iterate 9}}<td></td>{{end}}
</tr>
</table>
+1 -1
View File
@@ -1,6 +1,6 @@
# Development Dockerfile für Go Backend mit Live-Reloading
#FROM golang:1.23-alpine
FROM golang:1.24-alpine
FROM golang:1.25-alpine
# Install necessary packages for CGO and SQLite
RUN apk add --no-cache gcc musl-dev sqlite-dev