mapping the character data to a viewmodel

that  can be rendered easily into the html template
This commit is contained in:
2025-12-18 21:26:28 +01:00
parent 6ce2065c05
commit b99adede94
5 changed files with 911 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
package pdfrender
import (
"bamort/models"
)
// MapCharacterToViewModel converts a domain Character to a CharacterSheetViewModel
func MapCharacterToViewModel(char *models.Char) (*CharacterSheetViewModel, error) {
vm := &CharacterSheetViewModel{
Skills: make([]SkillViewModel, 0),
Weapons: make([]WeaponViewModel, 0),
Spells: make([]SpellViewModel, 0),
MagicItems: make([]MagicItemViewModel, 0),
Equipment: make([]EquipmentViewModel, 0),
GameResults: make([]GameResultViewModel, 0),
}
// 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,
Homeland: char.Herkunft,
Religion: char.Glaube,
Stand: char.SocialClass,
IconBase64: "", // Will be set later if image exists
}
// Map attributes
vm.Attributes = mapAttributes(char)
// Map derived values
vm.DerivedValues = mapDerivedValues(char)
// Map skills
vm.Skills = mapSkills(char)
// Map weapons
vm.Weapons = mapWeapons(char)
// Map spells
vm.Spells = mapSpells(char)
// Map equipment
vm.Equipment = mapEquipment(char)
return vm, nil
}
// mapAttributes extracts attribute values from Eigenschaften slice
func mapAttributes(char *models.Char) AttributeValues {
attrs := AttributeValues{}
for _, e := range char.Eigenschaften {
switch e.Name {
case "St":
attrs.St = e.Value
case "Gs":
attrs.Gs = e.Value
case "Gw":
attrs.Gw = e.Value
case "Ko":
attrs.Ko = e.Value
case "In":
attrs.In = e.Value
case "Zt":
attrs.Zt = e.Value
case "Au":
attrs.Au = e.Value
case "pA":
attrs.PA = e.Value
case "Wk":
attrs.Wk = e.Value
}
}
attrs.B = char.B.Value
return attrs
}
// mapDerivedValues extracts derived values like LP, AP, etc.
func mapDerivedValues(char *models.Char) DerivedValueSet {
return DerivedValueSet{
LPMax: char.Lp.Max,
LPAktuell: char.Lp.Value,
APMax: char.Ap.Max,
APAktuell: char.Ap.Value,
}
}
// mapSkills converts character skills to SkillViewModel
func mapSkills(char *models.Char) []SkillViewModel {
skills := make([]SkillViewModel, 0, len(char.Fertigkeiten))
for _, skill := range char.Fertigkeiten {
skills = append(skills, SkillViewModel{
Name: skill.Name,
Category: skill.Category,
Value: skill.Fertigkeitswert,
Bonus: skill.Bonus,
PracticePoints: skill.Pp,
IsLearned: skill.Fertigkeitswert > 0,
})
}
return skills
}
// mapWeapons converts character weapon skills to WeaponViewModel
func mapWeapons(char *models.Char) []WeaponViewModel {
weapons := make([]WeaponViewModel, 0, len(char.Waffenfertigkeiten))
for _, weapon := range char.Waffenfertigkeiten {
weapons = append(weapons, WeaponViewModel{
Name: weapon.Name,
Value: weapon.Fertigkeitswert,
})
}
return weapons
}
// mapSpells converts character spells to SpellViewModel
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,
})
}
return spells
}
// mapEquipment converts character equipment to EquipmentViewModel
func mapEquipment(char *models.Char) []EquipmentViewModel {
equipment := make([]EquipmentViewModel, 0, len(char.Ausruestung)+len(char.Behaeltnisse))
// Add regular equipment
for _, item := range char.Ausruestung {
equipment = append(equipment, EquipmentViewModel{
Name: item.Name,
Quantity: item.Anzahl,
Weight: item.Gewicht,
TotalWeight: item.Gewicht * float64(item.Anzahl),
Value: int(item.Wert),
Location: item.BeinhaltetIn,
Container: item.BeinhaltetIn,
IsWorn: item.BeinhaltetIn == "Am Körper",
IsContainer: false,
})
}
// Add containers
for _, container := range char.Behaeltnisse {
equipment = append(equipment, EquipmentViewModel{
Name: container.Name,
Quantity: 1,
Weight: container.Gewicht,
TotalWeight: container.Gewicht,
Value: int(container.Wert),
IsContainer: true,
})
}
return equipment
}
+370
View File
@@ -0,0 +1,370 @@
package pdfrender
import (
"bamort/models"
"testing"
)
func TestMapCharacterToViewModel_BasicInfo(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Bjarnfinnur Haberdson",
},
Rasse: "Mensch",
Typ: "Krieger",
Grad: 5,
Alter: 30,
Gender: "männlich",
Groesse: 180,
Gewicht: 85,
Herkunft: "Clanngadarn",
Glaube: "Druide",
SocialClass: "Mittelschicht",
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if vm.Character.Name != "Bjarnfinnur Haberdson" {
t.Errorf("Expected name 'Bjarnfinnur Haberdson', got '%s'", vm.Character.Name)
}
if vm.Character.Type != "Krieger" {
t.Errorf("Expected type 'Krieger', got '%s'", vm.Character.Type)
}
if vm.Character.Grade != 5 {
t.Errorf("Expected grade 5, got %d", vm.Character.Grade)
}
}
func TestMapCharacterToViewModel_Attributes(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
Eigenschaften: []models.Eigenschaft{
{Name: "St", Value: 79},
{Name: "Gs", Value: 65},
{Name: "Gw", Value: 70},
},
B: models.B{Value: 10},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if vm.Attributes.St != 79 {
t.Errorf("Expected St=79, got %d", vm.Attributes.St)
}
if vm.Attributes.Gs != 65 {
t.Errorf("Expected Gs=65, got %d", vm.Attributes.Gs)
}
if vm.Attributes.B != 10 {
t.Errorf("Expected B=10, got %d", vm.Attributes.B)
}
}
func TestMapCharacterToViewModel_LPandAP(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
Lp: models.Lp{Max: 20, Value: 18},
Ap: models.Ap{Max: 30, Value: 25},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if vm.DerivedValues.LPMax != 20 {
t.Errorf("Expected LP Max=20, got %d", vm.DerivedValues.LPMax)
}
if vm.DerivedValues.LPAktuell != 18 {
t.Errorf("Expected LP Aktuell=18, got %d", vm.DerivedValues.LPAktuell)
}
if vm.DerivedValues.APMax != 30 {
t.Errorf("Expected AP Max=30, got %d", vm.DerivedValues.APMax)
}
if vm.DerivedValues.APAktuell != 25 {
t.Errorf("Expected AP Aktuell=25, got %d", vm.DerivedValues.APAktuell)
}
}
func TestMapCharacterToViewModel_Skills(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
Fertigkeiten: []models.SkFertigkeit{
{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Schwimmen",
},
},
Fertigkeitswert: 10,
Bonus: 2,
Pp: 5,
Category: "Körper",
},
{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Klettern",
},
},
Fertigkeitswert: 8,
Bonus: 1,
Pp: 3,
Category: "Körper",
},
},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(vm.Skills) != 2 {
t.Fatalf("Expected 2 skills, got %d", len(vm.Skills))
}
skill := vm.Skills[0]
if skill.Name != "Schwimmen" {
t.Errorf("Expected skill name 'Schwimmen', got '%s'", skill.Name)
}
if skill.Value != 10 {
t.Errorf("Expected skill value 10, got %d", skill.Value)
}
if skill.Bonus != 2 {
t.Errorf("Expected skill bonus 2, got %d", skill.Bonus)
}
if skill.PracticePoints != 5 {
t.Errorf("Expected practice points 5, got %d", skill.PracticePoints)
}
if skill.Category != "Körper" {
t.Errorf("Expected category 'Körper', got '%s'", skill.Category)
}
}
func TestMapCharacterToViewModel_Weapons(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
Waffenfertigkeiten: []models.SkWaffenfertigkeit{
{
SkFertigkeit: models.SkFertigkeit{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Langschwert",
},
},
Fertigkeitswert: 12,
Bonus: 3,
Category: "Kampf",
},
},
},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(vm.Weapons) != 1 {
t.Fatalf("Expected 1 weapon, got %d", len(vm.Weapons))
}
weapon := vm.Weapons[0]
if weapon.Name != "Langschwert" {
t.Errorf("Expected weapon name 'Langschwert', got '%s'", weapon.Name)
}
if weapon.Value != 12 {
t.Errorf("Expected weapon value 12, got %d", weapon.Value)
}
}
func TestMapCharacterToViewModel_Spells(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
Zauber: []models.SkZauber{
{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Macht über die belebte Natur",
},
},
Bonus: 2,
Quelle: "Arkanum",
},
},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(vm.Spells) != 1 {
t.Fatalf("Expected 1 spell, got %d", len(vm.Spells))
}
spell := vm.Spells[0]
if spell.Name != "Macht über die belebte Natur" {
t.Errorf("Expected spell name 'Macht über die belebte Natur', got '%s'", spell.Name)
}
}
func TestMapCharacterToViewModel_Equipment(t *testing.T) {
// Arrange
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
Ausruestung: []models.EqAusruestung{
{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Rucksack",
},
},
Anzahl: 1,
Gewicht: 2.5,
Wert: 10,
BeinhaltetIn: "Am Körper",
},
{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Seil (20m)",
},
},
Anzahl: 1,
Gewicht: 5.0,
Wert: 15,
BeinhaltetIn: "Rucksack",
},
},
Behaeltnisse: []models.EqContainer{
{
BamortCharTrait: models.BamortCharTrait{
BamortBase: models.BamortBase{
Name: "Lederbeutel",
},
},
Gewicht: 0.5,
Wert: 5,
Tragkraft: 10,
},
},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if len(vm.Equipment) != 3 {
t.Fatalf("Expected 3 equipment items, got %d", len(vm.Equipment))
}
// Check first item
item := vm.Equipment[0]
if item.Name != "Rucksack" {
t.Errorf("Expected equipment name 'Rucksack', got '%s'", item.Name)
}
if item.Quantity != 1 {
t.Errorf("Expected quantity 1, got %d", item.Quantity)
}
if item.Weight != 2.5 {
t.Errorf("Expected weight 2.5, got %f", item.Weight)
}
if item.Location != "Am Körper" {
t.Errorf("Expected location 'Am Körper', got '%s'", item.Location)
}
// Check container
container := vm.Equipment[2]
if container.Name != "Lederbeutel" {
t.Errorf("Expected container name 'Lederbeutel', got '%s'", container.Name)
}
if !container.IsContainer {
t.Error("Expected IsContainer to be true")
}
}
func TestMapCharacterToViewModel_GameResults(t *testing.T) {
// Arrange - no game results in domain model yet
char := &models.Char{
BamortBase: models.BamortBase{
ID: 1,
Name: "Test Character",
},
}
// Act
vm, err := MapCharacterToViewModel(char)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
// Should be empty slice, not nil
if vm.GameResults == nil {
t.Error("Expected GameResults to be empty slice, got nil")
}
if len(vm.GameResults) != 0 {
t.Errorf("Expected 0 game results, got %d", len(vm.GameResults))
}
}
+18
View File
@@ -0,0 +1,18 @@
package pdfrender
// SliceList slices a list based on start index and max items
// Returns the sliced list and whether there are more items
func SliceList[T any](fullList []T, startIndex, maxItems int) ([]T, bool) {
totalCount := len(fullList)
endIndex := startIndex + maxItems
if startIndex >= totalCount {
return []T{}, false
}
if endIndex > totalCount {
endIndex = totalCount
}
return fullList[startIndex:endIndex], endIndex < totalCount
}
+166
View File
@@ -0,0 +1,166 @@
package pdfrender
// TemplateMetadata contains information about a template's capacity and requirements
type TemplateMetadata struct {
Name string // Template name (e.g., "page1_stats.html")
PageType string // "stats", "play", "spell", "equip"
Description string
Blocks []BlockMetadata // List blocks and their capacities
}
// BlockMetadata defines a list block within a template and how many items it can hold
type BlockMetadata struct {
Name string // Logical name (e.g., "skills_column1", "weapons_main")
ListType string // "skills", "weapons", "spells", "equipment", "magicItems", "gameResults"
MaxItems int // Maximum number of items this block can display
Filter string // Optional filter criteria (e.g., "learned", "unlearned", "languages")
Column int // Column number for multi-column layouts (0 if single column)
}
// TemplateSet contains all templates and their metadata for a format
type TemplateSet struct {
Name string // e.g., "Default_A4_Quer"
Description string
Templates []TemplateWithMeta
}
// TemplateWithMeta combines a template with its metadata
type TemplateWithMeta struct {
Metadata TemplateMetadata
Path string // File path to the template
}
// DefaultA4QuerTemplateSet returns the template set for A4 Querformat
func DefaultA4QuerTemplateSet() TemplateSet {
return TemplateSet{
Name: "Default_A4_Quer",
Description: "Standard A4 Querformat Charakterbogen",
Templates: []TemplateWithMeta{
{
Metadata: TemplateMetadata{
Name: "page1_stats.html",
PageType: "stats",
Description: "Statistikseite mit Grundwerten",
Blocks: []BlockMetadata{
{
Name: "skills_column1",
ListType: "skills",
MaxItems: 32,
Column: 1,
},
{
Name: "skills_column2",
ListType: "skills",
MaxItems: 32,
Column: 2,
},
},
},
Path: "templates/Default_A4_Quer/page1_stats.html",
},
{
Metadata: TemplateMetadata{
Name: "page2_play.html",
PageType: "play",
Description: "Spielbogen mit gelernten Fertigkeiten und Waffen",
Blocks: []BlockMetadata{
{
Name: "skills_learned",
ListType: "skills",
MaxItems: 24,
Filter: "learned",
},
{
Name: "skills_unlearned",
ListType: "skills",
MaxItems: 15,
Filter: "unlearned",
},
{
Name: "skills_languages",
ListType: "skills",
MaxItems: 11,
Filter: "languages",
},
{
Name: "weapons_main",
ListType: "weapons",
MaxItems: 30,
},
},
},
Path: "templates/Default_A4_Quer/page2_play.html",
},
{
Metadata: TemplateMetadata{
Name: "page3_spell.html",
PageType: "spell",
Description: "Zauberseite mit Zauberliste",
Blocks: []BlockMetadata{
{
Name: "spells_column1",
ListType: "spells",
MaxItems: 12,
Column: 1,
},
{
Name: "spells_column2",
ListType: "spells",
MaxItems: 12,
Column: 2,
},
{
Name: "magic_items",
ListType: "magicItems",
MaxItems: 5,
},
},
},
Path: "templates/Default_A4_Quer/page3_spell.html",
},
{
Metadata: TemplateMetadata{
Name: "page4_equip.html",
PageType: "equip",
Description: "Ausrüstungsseite",
Blocks: []BlockMetadata{
{
Name: "equipment_sections",
ListType: "equipment",
MaxItems: 20,
},
{
Name: "game_results",
ListType: "gameResults",
MaxItems: 10,
},
},
},
Path: "templates/Default_A4_Quer/page4_equip.html",
},
},
}
}
// GetBlockMetadata returns the metadata for a specific block in a template
func (tm *TemplateMetadata) GetBlockMetadata(blockName string) *BlockMetadata {
for i := range tm.Blocks {
if tm.Blocks[i].Name == blockName {
return &tm.Blocks[i]
}
}
return nil
}
// GetMaxItems returns the maximum items for a specific list type in this template
func (tm *TemplateMetadata) GetMaxItems(listType string, filter string) int {
total := 0
for _, block := range tm.Blocks {
if block.ListType == listType {
if filter == "" || block.Filter == filter {
total += block.MaxItems
}
}
}
return total
}
+184
View File
@@ -0,0 +1,184 @@
package pdfrender
import "time"
// CharacterSheetViewModel represents all data needed to render a character sheet PDF
type CharacterSheetViewModel struct {
Character CharacterInfo
Attributes AttributeValues
DerivedValues DerivedValueSet
Skills []SkillViewModel
Weapons []WeaponViewModel
Spells []SpellViewModel
MagicItems []MagicItemViewModel
Equipment []EquipmentViewModel
GameResults []GameResultViewModel
Meta PageMeta
}
// 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
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
}
// AttributeValues contains all character attributes
type AttributeValues struct {
St int // Stärke
Gs int // Geschicklichkeit
Gw int // Gewandtheit
Ko int // Konstitution
In int // Intelligenz
Zt int // Zaubertaltent
Au int // Aussehen
PA int // Persönliche Ausstrahlung
Wk int // Willenskraft
B int // Bewegungsweite
}
// DerivedValueSet contains all derived character values
type DerivedValueSet struct {
// Lebenspunkte & Ausdauer
LP int
AP int
LPMax int
APMax int
LPAktuell int
APAktuell int
AusdauerBonus int
// Geschwindigkeit
GG int // Grundgeschwindigkeit
SG int // Schrittgeschwindigkeit
// Kampfwerte
Abwehr int // z.B. "Abwehr+12"
SchadenBonus int
AngriffBonus int
// Resistenzen
ResistenzGift int
ResistenzKorper int
ResistenzGeist int
// Zauberwerte
Zaubern int // z.B. "+10/+9"
ZaubernBonus int // Erster Zauberbonus
// Sonstige
Sehen int // Sehen-Wert
Horen int // Hören-Wert
Riechen int // Riechen-Wert
Schmecken int // Schmecken-Wert
Sechster int // Sechster Sinn
}
// SkillViewModel represents a skill for display
type SkillViewModel struct {
Name string
Category string // z.B. "Kampf", "Körper", "Social"
SkillType string // z.B. "Fert", "Waff", "Ungelernte Fertigkeit"
Value int // Erfolgswert (EW)
BaseValue int // Grundwert (für Statistikseite)
Bonus int // Bonus/Malus
PracticePoints int // Praxispunkte (PP)
Attribute1 string // Leiteigenschaft Attribut für Bonus (z.B. "St")
IsLearned bool // Ob die Fertigkeit gelernt wurde
}
// WeaponViewModel represents a weapon for display
type WeaponViewModel struct {
Name string
Value int // Erfolgswert (EW)
ParryValue int // Abwehrwert (falls vorhanden)
Damage string // Schaden (z.B. "1W6+2")
Range string // Reichweite (für Fernkampfwaffen)
Notes string // Besondere Eigenschaften
IsRanged bool // Fernkampfwaffe ja/nein
IsMagical bool // Magische Waffe ja/nein
}
// 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
}
// MagicItemViewModel represents a magical item
type MagicItemViewModel struct {
Name string
Description string
Properties string // Magische Eigenschaften
Charges int // Ladungen (falls zutreffend)
Notes string
}
// EquipmentViewModel represents an equipment item
type EquipmentViewModel struct {
Name string
Quantity int
Weight float64 // Gewicht pro Stück in kg
TotalWeight float64 // Gesamtgewicht
Location string // z.B. "Am Körper", "Container 1"
Container string // Container-Name (z.B. "Becher, Holz")
Value int // Wert in Währungseinheiten
Notes string
IsWorn bool // Am Körper getragen
IsContainer bool // Ist selbst ein Container
}
// GameResultViewModel represents a game session result
type GameResultViewModel struct {
Date time.Time
EP int // Erfahrungspunkte
Gold int // Gold erhalten
Description string // Beschreibung der Sitzung
Location string // Ort der Handlung
Notes string
}
// PageMeta contains metadata about the current page
type PageMeta struct {
Date string
PageNumber int
TotalPages int
IsContinuation bool // Ist eine Fortsetzungsseite
PageType string // "stats", "play", "spell", "equip"
}
// PageData represents data for a single page (after pagination)
type PageData struct {
Character CharacterInfo
Attributes AttributeValues
DerivedValues DerivedValueSet
// Lists sliced according to template block metadata
Skills []SkillViewModel
Weapons []WeaponViewModel
Spells []SpellViewModel
MagicItems []MagicItemViewModel
Equipment []EquipmentViewModel
GameResults []GameResultViewModel
Meta PageMeta
}