CalculateStaticFieldsLogic

calculation of Bonus values defence zaubern and other values that depend on base stats
This commit is contained in:
2025-12-27 08:33:42 +01:00
parent fb63ef72fb
commit 3a7e699507
17 changed files with 858 additions and 142 deletions
+102 -44
View File
@@ -23,6 +23,7 @@ type CalculateStaticFieldsRequest struct {
// Charakterdaten
Rasse string `json:"rasse" binding:"required"`
Typ string `json:"typ" binding:"required"`
Grad int `json:"grad" binding:"omitempty,min=1,max=100"` // Charaktergrad (optional, default 1)
}
// StaticFieldsResponse für berechnete Felder ohne Würfelwürfe
@@ -69,17 +70,8 @@ type RolledFieldResponse struct {
Details interface{} `json:"details"`
}
// CalculateStaticFields berechnet alle Felder ohne Würfelwürfe
func CalculateStaticFields(c *gin.Context) {
var req CalculateStaticFieldsRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("Fehler beim Parsen der Static Fields Anfrage: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Anfrage"})
return
}
logger.Info("Berechne statische Felder für %s %s", req.Rasse, req.Typ)
// CalculateStaticFieldsLogic contains the pure business logic for static field calculation
func CalculateStaticFieldsLogic(req CalculateStaticFieldsRequest) StaticFieldsResponse {
response := StaticFieldsResponse{}
// Ausdauer Bonus: Ko/10 + St/20
@@ -103,20 +95,42 @@ func CalculateStaticFields(c *gin.Context) {
// Resistenz Bonus Geist
response.ResistenzBonusGeist = calculateResistenzBonusGeist(req.In, req.Rasse, req.Typ)
// Finale Resistenzwerte
response.ResistenzKoerper = 11 + response.ResistenzBonusKoerper
response.ResistenzGeist = 11 + response.ResistenzBonusGeist
// Grad standardmäßig auf 1 setzen wenn nicht angegeben
grad := req.Grad
if grad < 1 {
grad = 1
}
// Finale Kampfwerte
response.Abwehr = 11 + response.AbwehrBonus
response.Zaubern = 11 + response.ZauberBonus
// Finale Resistenzwerte (mit Gradbonus)
response.ResistenzKoerper = getResistenzBaseByGrade(grad) // + response.ResistenzBonusKoerper
response.ResistenzGeist = getResistenzBaseByGrade(grad) //+ response.ResistenzBonusGeist
// Finale Kampfwerte (mit Gradbonus)
response.Abwehr = getAbwehrBaseByGrade(grad) //+ response.AbwehrBonus
response.Zaubern = getZaubernBaseByGrade(grad) //+ response.ZauberBonus
// Raufen: (St + GW)/20 + angriffs_bonus + Rassenboni
raceBonus := 0
if req.Rasse == "Zwerge" {
if req.Rasse == "Zwerg" {
raceBonus = 1
}
response.Raufen = (req.St+req.Gw)/20 + response.AngriffsBonus + raceBonus
response.Raufen = (req.St+req.Gw)/20 + raceBonus //+ response.AngriffsBonus
return response
}
// CalculateStaticFields berechnet alle Felder ohne Würfelwürfe (HTTP Handler)
func CalculateStaticFields(c *gin.Context) {
var req CalculateStaticFieldsRequest
if err := c.ShouldBindJSON(&req); err != nil {
logger.Error("Fehler beim Parsen der Static Fields Anfrage: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Ungültige Anfrage"})
return
}
logger.Info("Berechne statische Felder für %s %s", req.Rasse, req.Typ)
response := CalculateStaticFieldsLogic(req)
c.JSON(http.StatusOK, response)
}
@@ -291,19 +305,17 @@ func calculateAttributeBonus(value int) int {
// calculateResistenzBonusKoerper berechnet den Körper-Resistenzbonus
func calculateResistenzBonusKoerper(ko int, rasse string, typ string) int {
bonus := 0
bonus := calculateAttributeBonus(ko)
if rasse == "Menschen" {
bonus = calculateAttributeBonus(ko)
} else {
switch rasse {
case "Elfen":
bonus = 2
case "Gnome", "Halblinge":
bonus = 4
case "Zwerge":
bonus = 3
}
switch rasse {
//case "Mensch":
// bonus = calculateAttributeBonus(ko)
case "Elf":
bonus = 2
case "Gnom", "Halbling":
bonus = 4
case "Zwerg":
bonus = 3
}
// Klassenmodifikator
@@ -318,19 +330,17 @@ func calculateResistenzBonusKoerper(ko int, rasse string, typ string) int {
// calculateResistenzBonusGeist berechnet den Geist-Resistenzbonus
func calculateResistenzBonusGeist(in int, rasse string, typ string) int {
bonus := 0
bonus := calculateAttributeBonus(in)
if rasse == "Menschen" {
bonus = calculateAttributeBonus(in)
} else {
switch rasse {
case "Elfen":
bonus = 2
case "Gnome", "Halblinge":
bonus = 4
case "Zwerge":
bonus = 3
}
switch rasse {
//case "Mensch":
//bonus = calculateAttributeBonus(in)
case "Elf":
bonus = 2
case "Gnom", "Halbling":
bonus = 4
case "Zwerg":
bonus = 3
}
// Klassenmodifikator (nur Zauberer bekommen Geist-Bonus)
@@ -354,7 +364,7 @@ func isKaempfer(typ string) bool {
// isZauberer prüft ob eine Klasse als Zauberer gilt
func isZauberer(typ string) bool {
zaubererKlassen := []string{"Magier", "Druide", "Priester", "Schamane"}
zaubererKlassen := []string{"Magier", "Druide", "Priester", "Schamane", "Hexer"}
for _, z := range zaubererKlassen {
if z == typ {
return true
@@ -400,3 +410,51 @@ func getMovementBaseAndFormula(rasse string) (int, string) {
return 16, "4d3 + 16"
}
}
// getAbwehrBaseByGrade gibt den Basis-Fertigkeitswert für Abwehr nach Grad zurück
// Grad: 1->11, 2->12, 5->13, 10->14, 15->15, 20->16, 25->17, 30->18
func getAbwehrBaseByGrade(grad int) int {
if grad >= 30 {
return 18
} else if grad >= 25 {
return 17
} else if grad >= 20 {
return 16
} else if grad >= 15 {
return 15
} else if grad >= 10 {
return 14
} else if grad >= 5 {
return 13
} else if grad >= 2 {
return 12
}
return 11
}
// getResistenzBaseByGrade gibt den Basis-Fertigkeitswert für Resistenz nach Grad zurück
// Verwendet dieselbe Tabelle wie Abwehr
func getResistenzBaseByGrade(grad int) int {
return getAbwehrBaseByGrade(grad)
}
// getZaubernBaseByGrade gibt den Basis-Fertigkeitswert für Zaubern nach Grad zurück
// Grad: 1->11, 2->12, 4->13, 6->14, 8->15, 10->16, 15->17, 20->18
func getZaubernBaseByGrade(grad int) int {
if grad >= 20 {
return 18
} else if grad >= 15 {
return 17
} else if grad >= 10 {
return 16
} else if grad >= 8 {
return 15
} else if grad >= 6 {
return 14
} else if grad >= 4 {
return 13
} else if grad >= 2 {
return 12
}
return 11
}
@@ -0,0 +1,182 @@
package character
import (
"testing"
)
func TestGetGradeBonus_Abwehr(t *testing.T) {
tests := []struct {
name string
grad int
expected int
}{
{"Grad 1", 1, 11},
{"Grad 2", 2, 12},
{"Grad 3", 3, 12},
{"Grad 4", 4, 12},
{"Grad 5", 5, 13},
{"Grad 9", 9, 13},
{"Grad 10", 10, 14},
{"Grad 14", 14, 14},
{"Grad 15", 15, 15},
{"Grad 19", 19, 15},
{"Grad 20", 20, 16},
{"Grad 24", 24, 16},
{"Grad 25", 25, 17},
{"Grad 29", 29, 17},
{"Grad 30", 30, 18},
{"Grad 35", 35, 18},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getAbwehrBaseByGrade(tt.grad)
if result != tt.expected {
t.Errorf("Expected %d for Grad %d, got %d", tt.expected, tt.grad, result)
}
})
}
}
func TestGetGradeBonus_Resistenz(t *testing.T) {
tests := []struct {
name string
grad int
expected int
}{
{"Grad 1", 1, 11},
{"Grad 2", 2, 12},
{"Grad 3", 3, 12},
{"Grad 4", 4, 12},
{"Grad 5", 5, 13},
{"Grad 9", 9, 13},
{"Grad 10", 10, 14},
{"Grad 14", 14, 14},
{"Grad 15", 15, 15},
{"Grad 19", 19, 15},
{"Grad 20", 20, 16},
{"Grad 24", 24, 16},
{"Grad 25", 25, 17},
{"Grad 29", 29, 17},
{"Grad 30", 30, 18},
{"Grad 35", 35, 18},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getResistenzBaseByGrade(tt.grad)
if result != tt.expected {
t.Errorf("Expected %d for Grad %d, got %d", tt.expected, tt.grad, result)
}
})
}
}
func TestGetGradeBonus_Zaubern(t *testing.T) {
tests := []struct {
name string
grad int
expected int
}{
{"Grad 1", 1, 11},
{"Grad 2", 2, 12},
{"Grad 3", 3, 12},
{"Grad 4", 4, 13},
{"Grad 5", 5, 13},
{"Grad 6", 6, 14},
{"Grad 7", 7, 14},
{"Grad 8", 8, 15},
{"Grad 9", 9, 15},
{"Grad 10", 10, 16},
{"Grad 14", 14, 16},
{"Grad 15", 15, 17},
{"Grad 19", 19, 17},
{"Grad 20", 20, 18},
{"Grad 25", 25, 18},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getZaubernBaseByGrade(tt.grad)
if result != tt.expected {
t.Errorf("Expected %d for Grad %d, got %d", tt.expected, tt.grad, result)
}
})
}
}
func TestCalculateStaticFieldsLogic_WithGrade(t *testing.T) {
tests := []struct {
name string
grad int
expectedAbwehr int
expectedResistenzBody int
expectedResistenzMind int
expectedZaubern int
}{
{
name: "Grad 1",
grad: 1,
expectedAbwehr: 11, // base 11 + bonus 0
expectedResistenzBody: 12, // base 11 + bonus 1 (Krieger)
expectedResistenzMind: 11, // base 11 + bonus 0 (Krieger gets no mind bonus)
expectedZaubern: 11, // base 11 + bonus 0
},
{
name: "Grad 5",
grad: 5,
expectedAbwehr: 13, // base 13 + bonus 0
expectedResistenzBody: 14, // base 13 + bonus 1 (Krieger)
expectedResistenzMind: 13, // base 13 + bonus 0
expectedZaubern: 13, // base 13 + bonus 0
},
{
name: "Grad 10",
grad: 10,
expectedAbwehr: 14, // base 14 + bonus 0
expectedResistenzBody: 15, // base 14 + bonus 1 (Krieger)
expectedResistenzMind: 14, // base 14 + bonus 0
expectedZaubern: 16, // base 16 + bonus 0
},
{
name: "Grad 20",
grad: 20,
expectedAbwehr: 16, // base 16 + bonus 0
expectedResistenzBody: 17, // base 16 + bonus 1 (Krieger)
expectedResistenzMind: 16, // base 16 + bonus 0
expectedZaubern: 18, // base 18 + bonus 0
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := CalculateStaticFieldsRequest{
St: 50,
Gs: 50,
Gw: 50,
Ko: 50,
In: 50,
Zt: 50,
Au: 50,
Rasse: "Menschen",
Typ: "Krieger",
Grad: tt.grad,
}
result := CalculateStaticFieldsLogic(req)
if result.Abwehr != tt.expectedAbwehr {
t.Errorf("Expected Abwehr %d for Grad %d, got %d", tt.expectedAbwehr, tt.grad, result.Abwehr)
}
if result.ResistenzKoerper != tt.expectedResistenzBody {
t.Errorf("Expected ResistenzKoerper %d for Grad %d, got %d", tt.expectedResistenzBody, tt.grad, result.ResistenzKoerper)
}
if result.ResistenzGeist != tt.expectedResistenzMind {
t.Errorf("Expected ResistenzGeist %d for Grad %d, got %d", tt.expectedResistenzMind, tt.grad, result.ResistenzGeist)
}
if result.Zaubern != tt.expectedZaubern {
t.Errorf("Expected Zaubern %d for Grad %d, got %d", tt.expectedZaubern, tt.grad, result.Zaubern)
}
})
}
}
+154
View File
@@ -0,0 +1,154 @@
package character
import (
"bamort/database"
"bamort/models"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDerivedValuesStorage(t *testing.T) {
// Setup test environment
original := os.Getenv("ENVIRONMENT")
os.Setenv("ENVIRONMENT", "test")
t.Cleanup(func() {
if original != "" {
os.Setenv("ENVIRONMENT", original)
} else {
os.Unsetenv("ENVIRONMENT")
}
})
// Setup test database
database.SetupTestDB(true, true)
defer database.ResetTestDB()
err := models.MigrateStructure()
assert.NoError(t, err)
t.Run("Derived values are stored in database", func(t *testing.T) {
// Create a character with derived values
char := models.Char{
BamortBase: models.BamortBase{
Name: "Test Character",
},
UserID: 1,
Rasse: "Mensch",
Typ: "Krieger",
Grad: 1,
ResistenzKoerper: 12,
ResistenzGeist: 11,
Abwehr: 13,
Zaubern: 10,
Raufen: 14,
}
// Save character
err := char.Create()
assert.NoError(t, err, "Should create character successfully")
// Reload character from database
var loadedChar models.Char
err = database.DB.First(&loadedChar, char.ID).Error
assert.NoError(t, err, "Should load character from database")
// Verify derived values are persisted
assert.Equal(t, 12, loadedChar.ResistenzKoerper, "ResistenzKoerper should be persisted")
assert.Equal(t, 11, loadedChar.ResistenzGeist, "ResistenzGeist should be persisted")
assert.Equal(t, 13, loadedChar.Abwehr, "Abwehr should be persisted")
assert.Equal(t, 10, loadedChar.Zaubern, "Zaubern should be persisted")
assert.Equal(t, 14, loadedChar.Raufen, "Raufen should be persisted")
})
}
func TestCalculateBonuses(t *testing.T) {
// Setup test environment
original := os.Getenv("ENVIRONMENT")
os.Setenv("ENVIRONMENT", "test")
t.Cleanup(func() {
if original != "" {
os.Setenv("ENVIRONMENT", original)
} else {
os.Unsetenv("ENVIRONMENT")
}
})
t.Run("Calculate bonuses from character attributes", func(t *testing.T) {
char := models.Char{
BamortBase: models.BamortBase{ID: 1},
Rasse: "Mensch",
Typ: "Krieger",
Eigenschaften: []models.Eigenschaft{
{CharacterID: 1, Name: "St", Value: 85},
{CharacterID: 1, Name: "Gs", Value: 70},
{CharacterID: 1, Name: "Gw", Value: 65},
{CharacterID: 1, Name: "Ko", Value: 75},
{CharacterID: 1, Name: "In", Value: 50},
{CharacterID: 1, Name: "Zt", Value: 30},
},
}
bonuses := char.CalculateBonuses()
// Ausdauer Bonus: Ko/10 + St/20 = 75/10 + 85/20 = 7 + 4 = 11
assert.Equal(t, 11, bonuses.AusdauerBonus, "AusdauerBonus should be calculated correctly")
// Schadens Bonus: St/20 + Gs/30 - 3 = 85/20 + 70/30 - 3 = 4 + 2 - 3 = 3
assert.Equal(t, 3, bonuses.SchadensBonus, "SchadensBonus should be calculated correctly")
// Angriffs Bonus: Gs 70 -> bonus 2 (61-80 range)
assert.Equal(t, 2, bonuses.AngriffsBonus, "AngriffsBonus should be calculated correctly")
// Abwehr Bonus: Gw 65 -> bonus 2 (61-80 range)
assert.Equal(t, 2, bonuses.AbwehrBonus, "AbwehrBonus should be calculated correctly")
// Zauber Bonus: Zt 30 -> bonus 0 (21-40 range)
assert.Equal(t, 0, bonuses.ZauberBonus, "ZauberBonus should be calculated correctly")
// Resistenz Bonus Körper: Ko 75 -> bonus 2 (61-80), Mensch Krieger +1 = 3
assert.Equal(t, 3, bonuses.ResistenzBonusKoerper, "ResistenzBonusKoerper should be calculated correctly")
// Resistenz Bonus Geist: In 50 -> bonus 1 (41-60), Mensch = 1
assert.Equal(t, 1, bonuses.ResistenzBonusGeist, "ResistenzBonusGeist should be calculated correctly")
})
t.Run("Calculate bonuses for Zwerg Kämpfer", func(t *testing.T) {
char := models.Char{
Rasse: "Zwerge",
Typ: "Krieger",
Eigenschaften: []models.Eigenschaft{
{Name: "Ko", Value: 85},
{Name: "In", Value: 50},
},
}
bonuses := char.CalculateBonuses()
// Zwerge get +3 base, Kämpfer +1 = 4
assert.Equal(t, 4, bonuses.ResistenzBonusKoerper, "Zwerge Kämpfer ResistenzBonusKoerper should be 4")
// Zwerge get +3 base
assert.Equal(t, 3, bonuses.ResistenzBonusGeist, "Zwerge ResistenzBonusGeist should be 3")
})
t.Run("Calculate bonuses for Elf Magier", func(t *testing.T) {
char := models.Char{
Rasse: "Elfen",
Typ: "Magier",
Eigenschaften: []models.Eigenschaft{
{Name: "Ko", Value: 50},
{Name: "In", Value: 85},
},
}
bonuses := char.CalculateBonuses()
// Elfen get +2 base, Magier (Zauberer) +2 = 4
assert.Equal(t, 4, bonuses.ResistenzBonusKoerper, "Elfen Magier ResistenzBonusKoerper should be 4")
// Elfen get +2 base, Magier (Zauberer) +2 = 4
assert.Equal(t, 4, bonuses.ResistenzBonusGeist, "Elfen Magier ResistenzBonusGeist should be 4")
})
}
+7
View File
@@ -2748,6 +2748,13 @@ func FinalizeCharacterCreation(c *gin.Context) {
Public: false, // Default to private
Grad: 1, // Default starting grade
// Static derived values (can increase with grade)
ResistenzKoerper: session.DerivedValues.ResistenzKoerper,
ResistenzGeist: session.DerivedValues.ResistenzGeist,
Abwehr: session.DerivedValues.Abwehr,
Zaubern: session.DerivedValues.Zaubern,
Raufen: session.DerivedValues.Raufen,
// Lebenspunkte
Lp: models.Lp{
Max: session.DerivedValues.LPMax,
+8 -1
View File
@@ -716,7 +716,7 @@ func TestFinalizeCharacterCreation(t *testing.T) {
// Verify character exists in database
var createdChar models.Char
err = database.DB.Preload("Fertigkeiten").Preload("Waffenfertigkeiten").Preload("Zauber").
err = database.DB.Preload("Lp").Preload("Ap").Preload("B").Preload("Eigenschaften").Preload("Fertigkeiten").Preload("Waffenfertigkeiten").Preload("Zauber").
First(&createdChar, "id = ?", characterID).Error
assert.NoError(t, err, "Created character should exist in database")
@@ -756,6 +756,13 @@ func TestFinalizeCharacterCreation(t *testing.T) {
assert.Equal(t, 8, createdChar.B.Max, "B Max should match session")
assert.Equal(t, 8, createdChar.B.Value, "B Value should equal Max initially")
// Validate static derived values (Resistenz, Abwehr, Zaubern, Raufen)
assert.Equal(t, 11, createdChar.ResistenzKoerper, "Resistenz Körper should match session")
assert.Equal(t, 11, createdChar.ResistenzGeist, "Resistenz Geist should match session")
assert.Equal(t, 11, createdChar.Abwehr, "Abwehr should match session")
assert.Equal(t, 11, createdChar.Zaubern, "Zaubern should match session")
assert.Equal(t, 8, createdChar.Raufen, "Raufen should match session")
// Validate skills were transferred (session has 10 skills: 6 regular skills + 4 weapon skills)
// Regular skills: Klettern, Reiten, Sprache, Athletik, Heilkunde, Naturkunde (6)
// Weapon skills: Spießwaffen, Stielwurfwaffen, Waffenloser Kampf, Stichwaffen (4)
+63
View File
@@ -0,0 +1,63 @@
# Derived Values Implementation
## Summary
Static derived values (Resistenz Körper, Resistenz Geist, Abwehr, Zaubern, Raufen) are now stored in the database as part of the `Char` model. These values can increase when a new grade is reached.
Bonus values are calculated on-demand from the character's attributes using the `CalculateBonuses()` method.
## Database Fields Added to `Char`
- `ResistenzKoerper` (int) - Resistenz Körper base value
- `ResistenzGeist` (int) - Resistenz Geist base value
- `Abwehr` (int) - Abwehr base value
- `Zaubern` (int) - Zaubern base value
- `Raufen` (int) - Raufen base value
## Calculated Bonuses (Not Stored)
The following bonuses are calculated from attributes and are NOT stored in the database:
- `AusdauerBonus` = Ko/10 + St/20
- `SchadensBonus` = St/20 + Gs/30 - 3
- `AngriffsBonus` = attribute bonus from Gs
- `AbwehrBonus` = attribute bonus from Gw
- `ZauberBonus` = attribute bonus from Zt
- `ResistenzBonusKoerper` = attribute/race bonus from Ko
- `ResistenzBonusGeist` = attribute/race bonus from In
## Usage Example
```go
// Load character from database
var char models.Char
err := char.FirstID("123")
// Get static derived values (stored in DB)
resistenzKoerper := char.ResistenzKoerper
abwehr := char.Abwehr
// Calculate bonuses from attributes (on-demand)
bonuses := char.CalculateBonuses()
abwehrBonus := bonuses.AbwehrBonus
zauberBonus := bonuses.ZauberBonus
// Total values
totalAbwehr := char.Abwehr + bonuses.AbwehrBonus
totalZaubern := char.Zaubern + bonuses.ZauberBonus
```
## When Grade Increases
When a character reaches a new grade, the static derived values may increase. To update them:
```go
// After grade increase, recalculate and update static values
char.Grad++
char.ResistenzKoerper++ // or apply the appropriate grade bonus
char.Abwehr++
// Save updated character
database.DB.Save(&char)
```
Bonuses will automatically reflect any attribute changes without needing to update the database, as they are calculated on-demand from the `Eigenschaften` values.
+32
View File
@@ -510,6 +510,38 @@ func SetupCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Setup Check OK"})
}
func SetupCheckDev(c *gin.Context) {
logger.Info("Starte Setup-Check... PreparedTestDB")
// Use the prepared test database for development setup check
db, dberr := gorm.Open(sqlite.Open(database.PreparedTestDB), &gorm.Config{})
if dberr != nil {
logger.Error("SetupTestDB: Fehler beim Verbinden mit der Test-Datenbank: %s", dberr.Error())
panic("failed to connect to the test database: " + dberr.Error())
}
database.DB = db
logger.Debug("Erfolgreich mit Datenbank für Setup-Check verbunden")
logger.Debug("Führe Strukturmigration durch...")
err := migrateAllStructures(db)
if err != nil {
logger.Error("Fehler bei der Strukturmigration: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
logger.Debug("Führe Datenmigration durch...")
err = migrateDataIfNeeded(db)
if err != nil {
logger.Error("Fehler bei der Datenmigration: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to migrate data: " + err.Error()})
return
}
logger.Info("Setup-Check erfolgreich abgeschlossen")
c.JSON(http.StatusOK, gin.H{"message": "Setup Check OK"})
}
func ReconnectDataBase(c *gin.Context) {
logger.Info("Führe Datenbank-Reconnect durch...")
+1
View File
@@ -7,6 +7,7 @@ import (
func RegisterRoutes(r *gin.RouterGroup) {
charGrp := r.Group("/maintenance")
charGrp.GET("/setupcheck", SetupCheck)
charGrp.GET("/setupcheck-dev", SetupCheckDev)
charGrp.GET("/mktestdata", MakeTestdataFromLive)
charGrp.GET("/reconndb", ReconnectDataBase) // Datenbank neu verbinden
charGrp.GET("/reloadenv", ReloadENV)
+173 -15
View File
@@ -76,21 +76,27 @@ type Vermoegen struct {
type Char struct {
BamortBase
UserID uint `gorm:"index;not null;default:1" json:"user_id"`
User user.User `gorm:"foreignKey:UserID;references:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"user"`
Rasse string `json:"rasse"`
Typ string `json:"typ"`
Alter int `json:"alter"`
Anrede string `json:"anrede"`
Grad int `json:"grad"`
Gender string `json:"gender"`
SocialClass string `json:"social_class"`
Groesse int `json:"groesse"`
Gewicht int `json:"gewicht"`
Herkunft string `json:"origin"`
Glaube string `json:"glaube"`
Hand string `json:"hand"`
Public bool `json:"public"`
UserID uint `gorm:"index;not null;default:1" json:"user_id"`
User user.User `gorm:"foreignKey:UserID;references:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"user"`
Rasse string `json:"rasse"`
Typ string `json:"typ"`
Alter int `json:"alter"`
Anrede string `json:"anrede"`
Grad int `json:"grad"`
Gender string `json:"gender"`
SocialClass string `json:"social_class"`
Groesse int `json:"groesse"`
Gewicht int `json:"gewicht"`
Herkunft string `json:"origin"`
Glaube string `json:"glaube"`
Hand string `json:"hand"`
Public bool `json:"public"`
// Static derived values (can increase with grade)
ResistenzKoerper int `json:"resistenz_koerper"`
ResistenzGeist int `json:"resistenz_geist"`
Abwehr int `json:"abwehr"`
Zaubern int `json:"zaubern"`
Raufen int `json:"raufen"`
Lp Lp `gorm:"foreignKey:CharacterID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"lp"`
Ap Ap `gorm:"foreignKey:CharacterID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"ap"`
B B `gorm:"foreignKey:CharacterID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE" json:"b"`
@@ -293,3 +299,155 @@ func (object *Vermoegen) TableName() string {
dbPrefix := "char"
return dbPrefix + "_" + "wealth"
}
// DerivedBonuses contains all calculated bonus values
type DerivedBonuses struct {
AusdauerBonus int
SchadensBonus int
AngriffsBonus int
AbwehrBonus int
ZauberBonus int
ResistenzBonusKoerper int
ResistenzBonusGeist int
}
// GetAttributeValue returns the value of an attribute by name
func (char *Char) GetAttributeValue(name string) int {
for _, attr := range char.Eigenschaften {
if attr.Name == name {
return attr.Value
}
}
return 0
}
// CalculateBonuses calculates all derived bonuses from attributes
func (char *Char) CalculateBonuses() DerivedBonuses {
st := char.GetAttributeValue("St")
gs := char.GetAttributeValue("Gs")
gw := char.GetAttributeValue("Gw")
ko := char.GetAttributeValue("Ko")
in := char.GetAttributeValue("In")
zt := char.GetAttributeValue("Zt")
bonuses := DerivedBonuses{
// Ausdauer Bonus: Ko/10 + St/20
AusdauerBonus: (ko / 10) + (st / 20),
// Schadens Bonus: St/20 + Gs/30 - 3
SchadensBonus: (st / 20) + (gs / 30) - 3,
// Angriffs Bonus basierend auf GS
AngriffsBonus: calculateAttributeBonus(gs),
// Abwehr Bonus basierend auf GW
AbwehrBonus: calculateAttributeBonus(gw),
// Zauber Bonus basierend auf Zt
ZauberBonus: calculateAttributeBonus(zt),
}
// Resistenz Bonus Körper
bonuses.ResistenzBonusKoerper = calculateResistenzBonusKoerper(ko, char.Rasse, char.Typ)
// Resistenz Bonus Geist
bonuses.ResistenzBonusGeist = calculateResistenzBonusGeist(in, char.Rasse, char.Typ)
return bonuses
}
// Helper functions for bonus calculation
func calculateAttributeBonus(value int) int {
if value <= 5 {
return -5
} else if value <= 20 {
return -1
} else if value <= 40 {
return 0
} else if value <= 60 {
return 1
} else if value <= 80 {
return 2
} else if value <= 95 {
return 3
} else {
return 4
}
}
func calculateResistenzBonusKoerper(ko int, rasse string, typ string) int {
bonus := 0
if rasse == "Mensch" || rasse == "Menschen" {
bonus = calculateAttributeBonus(ko)
} else {
switch rasse {
case "Elfen":
bonus = 2
case "Gnome", "Halblinge":
bonus = 4
case "Zwerge":
bonus = 3
}
}
// Klassenmodifikator
if isKaempfer(typ) {
bonus += 1
} else if isZauberer(typ) {
bonus += 2
}
return bonus
}
func calculateResistenzBonusGeist(in int, rasse string, typ string) int {
bonus := 0
if rasse == "Mensch" || rasse == "Menschen" {
bonus = calculateAttributeBonus(in)
} else {
switch rasse {
case "Elfen":
bonus = 2
case "Gnome", "Halblinge":
bonus = 4
case "Zwerge":
bonus = 3
}
}
// Klassenmodifikator (nur Zauberer bekommen Geist-Bonus)
if isZauberer(typ) {
bonus += 2
}
return bonus
}
func isKaempfer(typ string) bool {
kaempferClasses := []string{
"Krieger", "Barbar", "Spitzbube", "Assassine",
"Streiter", "Waldläufer", "Krieger Magier",
}
for _, class := range kaempferClasses {
if typ == class {
return true
}
}
return false
}
func isZauberer(typ string) bool {
zaubererClasses := []string{
"Magier", "Hexer", "Thaumaturg", "Krieger Magier",
"Priester", "Druide", "Schamane",
}
for _, class := range zaubererClasses {
if typ == class {
return true
}
}
return false
}
+66 -26
View File
@@ -3,6 +3,7 @@ package pdfrender
import (
"fmt"
"bamort/character"
"bamort/database"
"bamort/models"
)
@@ -20,18 +21,18 @@ func MapCharacterToViewModel(char *models.Char) (*CharacterSheetViewModel, error
// Map basic character info
vm.Character = CharacterInfo{
Name: char.Name,
Type: char.Typ,
Grade: char.Grad,
Age: char.Alter,
Height: char.Groesse,
Weight: char.Gewicht,
Gender: char.Gender,
Hand: char.Hand,
Homeland: char.Herkunft,
Religion: char.Glaube,
Stand: char.SocialClass,
IconBase64: "", // Will be set later if image exists
Name: char.Name,
Type: char.Typ,
Grade: char.Grad,
Age: char.Alter,
Height: char.Groesse,
Weight: char.Gewicht,
Gender: char.Gender,
Hand: char.Hand,
Herkunft: char.Herkunft,
Glaube: char.Glaube,
SocialClass: char.SocialClass,
IconBase64: "", // Will be set later if image exists
Vermoegen: WealthInfo{
Goldstuecke: char.Vermoegen.Goldstuecke,
Silberstuecke: char.Vermoegen.Silberstuecke,
@@ -80,14 +81,14 @@ func mapAttributes(char *models.Char) AttributeValues {
attrs.Zt = e.Value
case "Au":
attrs.Au = e.Value
case "pA":
case "PA":
attrs.PA = e.Value
case "Wk":
attrs.Wk = e.Value
}
}
attrs.B = char.B.Value
attrs.B = char.B.Max
return attrs
}
@@ -96,14 +97,43 @@ func mapAttributes(char *models.Char) AttributeValues {
func mapDerivedValues(char *models.Char) DerivedValueSet {
// Get attributes for bonus calculations
attrs := mapAttributes(char)
// Get Bonus values from character logic
bonusValues := character.CalculateStaticFieldsLogic(character.CalculateStaticFieldsRequest{
St: attrs.St,
Gs: attrs.Gs,
Gw: attrs.Gw,
Ko: attrs.Ko,
In: attrs.In,
Zt: attrs.Zt,
Au: attrs.Au,
Rasse: char.Rasse,
Typ: char.Typ,
Grad: char.Grad,
})
return DerivedValueSet{
LPMax: char.Lp.Max,
LPAktuell: char.Lp.Value,
APMax: char.Ap.Max,
APAktuell: char.Ap.Value,
AngriffBonus: calculateAttributeBonus(attrs.Gs),
SchadenBonus: (attrs.St / 20) + (attrs.Gs / 30) - 3,
LPMax: char.Lp.Max,
LPAktuell: char.Lp.Value,
APMax: char.Ap.Max,
APAktuell: char.Ap.Value,
AusdauerBonus: bonusValues.AusdauerBonus,
SchadenBonus: bonusValues.SchadensBonus,
AngriffBonus: bonusValues.AngriffsBonus,
Abwehr: bonusValues.Abwehr,
AbwehrBonus: bonusValues.AbwehrBonus,
Zaubern: bonusValues.Zaubern,
ZauberBonus: bonusValues.ZauberBonus,
ResistenzKoerper: bonusValues.ResistenzKoerper,
ResistenzBonusKoerper: bonusValues.ResistenzBonusKoerper,
ResistenzGeist: bonusValues.ResistenzGeist,
ResistenzBonusGeist: bonusValues.ResistenzBonusGeist,
Raufen: bonusValues.Raufen,
GG: char.B.Max,
SG: char.B.Max / 2,
Sehen: 0, // TODO: Add to character model
Horen: 0, // TODO: Add to character model
Riechen: 0, // TODO: Add to character model
Sechster: 0, // TODO: Add to character model
}
}
@@ -135,10 +165,20 @@ func mapSkills(char *models.Char) []SkillViewModel {
func mapWeapons(char *models.Char) []WeaponViewModel {
weapons := make([]WeaponViewModel, 0, len(char.Waffen))
// Calculate character's bonuses once
// Calculate character's bonuses using character logic
attrs := mapAttributes(char)
angriffsBonus := calculateAttributeBonus(attrs.Gs)
schadenBonus := calculateAttributeBonus(attrs.St)
bonusValues := character.CalculateStaticFieldsLogic(character.CalculateStaticFieldsRequest{
St: attrs.St,
Gs: attrs.Gs,
Gw: attrs.Gw,
Ko: attrs.Ko,
In: attrs.In,
Zt: attrs.Zt,
Au: attrs.Au,
Rasse: char.Rasse,
Typ: char.Typ,
Grad: char.Grad,
})
// Create a map of weapon skills for quick lookup
weaponSkills := make(map[string]int)
@@ -162,10 +202,10 @@ func mapWeapons(char *models.Char) []WeaponViewModel {
if baseWeapon.SkillRequired != "" {
skillValue = weaponSkills[baseWeapon.SkillRequired]
}
vm.Value = skillValue + angriffsBonus + equippedWeapon.Anb
vm.Value = skillValue + bonusValues.AngriffsBonus + equippedWeapon.Anb
// Calculate damage: Base weapon damage + character bonus + weapon damage bonus
vm.Damage = calculateWeaponDamageWithBase(baseWeapon, schadenBonus, equippedWeapon.Schb)
vm.Damage = calculateWeaponDamageWithBase(baseWeapon, bonusValues.SchadensBonus, equippedWeapon.Schb)
// Add range information for ranged weapons
if baseWeapon.IsRanged() {
@@ -177,7 +217,7 @@ func mapWeapons(char *models.Char) []WeaponViewModel {
}
} else {
// Weapon not found in gsm_weapons, use basic info
vm.Value = angriffsBonus + equippedWeapon.Anb
vm.Value = bonusValues.AngriffsBonus + equippedWeapon.Anb
}
weapons = append(weapons, vm)
+13 -1
View File
@@ -44,6 +44,18 @@ func TestMapCharacterToViewModel_BasicInfo(t *testing.T) {
if vm.Character.Grade != 5 {
t.Errorf("Expected grade 5, got %d", vm.Character.Grade)
}
if vm.Character.Herkunft != "Clanngadarn" {
t.Errorf("Expected Herkunft 'Clanngadarn', got '%s'", vm.Character.Herkunft)
}
if vm.Character.Glaube != "Druide" {
t.Errorf("Expected Glaube 'Druide', got '%s'", vm.Character.Glaube)
}
if vm.Character.SocialClass != "Mittelschicht" {
t.Errorf("Expected SocialClass 'Mittelschicht', got '%s'", vm.Character.SocialClass)
}
}
func TestMapCharacterToViewModel_Attributes(t *testing.T) {
@@ -58,7 +70,7 @@ func TestMapCharacterToViewModel_Attributes(t *testing.T) {
{Name: "Gs", Value: 65},
{Name: "Gw", Value: 70},
},
B: models.B{Value: 10},
B: models.B{Max: 10},
}
// Act
-17
View File
@@ -1,17 +0,0 @@
## TODO (Remaining)
* 1. in RenderPageWithContinuations use a switch to select a templatename and the ncall a paginator function that is designed explicitly for this template page with the current content. This will not work when we add more and different templates. We nned a solution where the code can find out which data is present on the template page and if these datastructures are to be continued on a continuation page or on the next regular page in row. it needs to find out how many elements can be rendered into the page or it's continuation page. It needs to handle if there are multiple datastructures present on one page.
for this to be accomplished we need some kind of template metadata or inline datastructure(Block) configuration.
I want to change the templateName from page1_stats.html to page_1.html and accordingly the continuation page from page1.2_stats.html to page_1.2.html this makes clear that a page is not tight to one or another datastructure.
Page 4 has a complex container-based layout we leave this out for the moment.
Plan a refacturation of PaginateSkills, PaginatePage2PlayLists and PaginateSpells into one unified function.
### Later
* continuation of lists does not work as expected but good enough for a first shot
* generalize handling so that only on set of functions can handle ALL kinds of templates. Needs massive refactoring
* currently the template fetched for rendering is set to Default_A4_Quer
* remove inline css as far as possible
* make pdf download popup an own view
* func CleanupExportTemp move maxAge := 7 * 24 * time.Hour definition to Config struct n config.go
+24 -20
View File
@@ -18,21 +18,21 @@ type CharacterSheetViewModel struct {
// CharacterInfo contains basic character information
type CharacterInfo struct {
Name string
Player string
Type string // Charaktertyp (z.B. "Krieger", "Magier")
Grade int
Birthdate string
Age int
Hand string // "rechts." oder "links." händig
Height int // in cm
Weight int // in kg
IconBase64 string // base64-kodiertes Charakterbild als Data-URI
Gender string
Homeland string
Religion string
Stand string // Sozialer Stand
Vermoegen WealthInfo
Name string
Player string
Type string // Charaktertyp (z.B. "Krieger", "Magier")
Grade int
Birthdate string
Age int
Hand string // "rechts." oder "links." händig
Height int // in cm
Weight int // in kg
IconBase64 string // base64-kodiertes Charakterbild als Data-URI
Gender string
Herkunft string
Glaube string
SocialClass string // Sozialer SocialClass
Vermoegen WealthInfo
}
// WealthInfo contains character wealth/money
@@ -72,17 +72,21 @@ type DerivedValueSet struct {
// Kampfwerte
Abwehr int // z.B. "Abwehr+12"
AbwehrBonus int // Abwehr-Bonus
SchadenBonus int
AngriffBonus int
Raufen int // Raufen-Wert
// Resistenzen
ResistenzGift int
ResistenzKoerper int
ResistenzGeist int
ResistenzGift int
ResistenzKoerper int
ResistenzBonusKoerper int // Resistenz Körper Bonus
ResistenzGeist int
ResistenzBonusGeist int // Resistenz Geist Bonus
// Zauberwerte
Zaubern int // z.B. "+10/+9"
ZaubernBonus int // Erster Zauberbonus
Zaubern int // z.B. "+10/+9"
ZauberBonus int // Zauber-Bonus
// Sonstige
Sehen int // Sehen-Wert
@@ -52,14 +52,14 @@
<!-- left column block stats4 -->
<div class="margin-bottom-3">
<div class="attr-box attr-box-80"><div class="attr-label">B</div><div class="attr-value">{{.Attributes.B}}</div></div>
<div class="attr-box attr-box-100"><div class="attr-label">Raufen</div><div class="attr-value">+ 5</div></div>
<div class="attr-box attr-box-100"><div class="attr-label">Raufen</div><div class="attr-value">+ {{.DerivedValues.Raufen}}</div></div>
</div>
<!-- left column block boni -->
<div class="margin-top-2">
<div><strong>persönlicher Bonus für:</strong></div>
<div class="margin-top-2">
<div class="bonus-row"><span>Ausdauer <u>{{.DerivedValues.AusdauerBonus}}</u></span><span>Schaden <u>{{.DerivedValues.SchadenBonus}}</u></span><span>Angriff <u>{{.DerivedValues.AngriffBonus}}</u></span></div>
<div class="bonus-row"><span>Abwehr <u>{{.DerivedValues.Abwehr}}</u></span><span>Resistenz <u>{{.DerivedValues.ResistenzGift}}/{{.DerivedValues.ResistenzGeist}}</u></span><span>Zaubern <u>{{.DerivedValues.Zaubern}}</u></span></div>
<div class="bonus-row"><span>Abwehr <u>{{.DerivedValues.AbwehrBonus}}</u></span><span>Resistenz <u>{{.DerivedValues.ResistenzBonusKoerper}}/{{.DerivedValues.ResistenzBonusGeist}}</u></span><span>Zaubern <u>{{.DerivedValues.ZauberBonus}}</u></span></div>
</div>
</div>
</div>
@@ -84,15 +84,15 @@
</tr>
<tr>
<td><strong>Stand</strong></td>
<td>{{.Character.Stand}}</td>
<td>{{.Character.SocialClass}}</td>
</tr>
<tr>
<td><strong>Heimat</strong></td>
<td>{{.Character.Homeland}}</td>
<td>{{.Character.Herkunft}}</td>
</tr>
<tr>
<td><strong>Glaube</strong></td>
<td>{{.Character.Religion}}</td>
<td>{{if .Character.Glaube}}{{.Character.Glaube}}{{else}}---{{end}}</td>
</tr>
<tr>
<td colspan="2"><strong>Besondere Merkmale:</strong><br/><br/><br/><br/><br/><br/><br/><br/></td>
@@ -41,10 +41,10 @@
</div>
<!-- Combat stats -->
<div class="combat-stats">
<div><strong>Abwehr + {{.DerivedValues.Abwehr}}</strong></div>
<div><strong>Abwehr + {{.DerivedValues.Abwehr}}+{{.DerivedValues.AbwehrBonus}}</strong></div>
<div><strong>+</strong> <span class="combat-stats-small">mit Vert.<br>waffe</span></div>
<div><strong>Resistenz + {{.DerivedValues.ResistenzKoerper}}/{{.DerivedValues.ResistenzGeist}}</strong></div>
<div><strong>Zaubern + {{.DerivedValues.Zaubern}}</strong></div>
<div><strong>Resistenz + {{.DerivedValues.ResistenzKoerper}}+{{.DerivedValues.ResistenzBonusKoerper}}/{{.DerivedValues.ResistenzGeist}}+{{.DerivedValues.ResistenzBonusGeist}}</strong></div>
<div><strong>Zaubern + {{.DerivedValues.Zaubern}}+{{.DerivedValues.ZauberBonus}}</strong></div>
</div>
<!-- Skills tables -->
<div class="skills-row">
@@ -121,29 +121,29 @@
<td></td>
<td>{{.DerivedValues.AngriffBonus}}</td>
<td></td>
<td>{{.DerivedValues.Abwehr}}</td>
<td>{{.DerivedValues.AbwehrBonus}}</td>
<td></td>
<td>{{.DerivedValues.SchadenBonus}}</td>
<td></td>
</tr>
<tr>
<td>Situativ</td>
<td></td>
<td></td>
<td>Wert</td>
<td>{{.DerivedValues.APMax}}</td>
<td></td>
<td></td>
<td></td>
<td>{{.DerivedValues.Abwehr}}</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>Summe</td>
<td>{{.DerivedValues.APMax}}</td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>{{.DerivedValues.Abwehr}}+{{.DerivedValues.AbwehrBonus}}</td>
<td></td>
<td></td>
<td></td>
+15
View File
@@ -0,0 +1,15 @@
## TODO (Remaining)
### Done
### Later
* generalize PDF Export handling so that only on set of functions can handle ALL kinds of templates. Needs massive refactoring
* currently the template fetched for rendering is set to Default_A4_Quer
* remove inline css as far as possible
* make pdf download popup an own view
* func CleanupExportTemp move maxAge := 7 * 24 * time.Hour definition to Config struct in config.go
* CalculateStaticFieldsLogic calc e.g response.ResistenzKoerper and others without bonus
+4 -4
View File
@@ -21,11 +21,11 @@
<strong>Aktuelle Kampagne:</strong> Melzindar
</p>
<p>
{{ character.typ || 'x' }} ({{ character.geschlecht || 'x' }}nännlich),
{{ character.typ || 'x' }} ({{ character.gender || 'x' }}),
Grad: {{ character.grad || 'x' }},
Rasse: {{ character.rasse || 'x' }},
Heimat: {{ character.heimat || 'x' }}Alba,
Stand: {{ character.heimat || 'x' }}Mittelschicht.
Heimat: {{ character.origin || '-' }},
Stand: {{ character.social_class || '-' }}.
</p>
<p v-if="character.rasse==='Zwerg'">
Hort für Grad {{ character.grad || 'x' }}: 125 GS, für nächsten Grad: 250 GS.
@@ -44,7 +44,7 @@
und {{ character.merkmale?.breite || '-'}},
Augen: {{ character.merkmale?.augenfarbe || '-' }},
Haare: {{ character.merkmale?.haarfarbe || '-' }},
Glaube: {{ character.glaube }}.
Glaube: {{ character.glaube || '-' }}
</p>
<p>
<strong>Merkmale:</strong> {{ character.merkmale?.sonstige || '-' }}