remove implementation documentation files

This commit is contained in:
2025-12-21 09:25:20 +01:00
parent cd0f98042d
commit 62abf724b3
27 changed files with 0 additions and 5166 deletions
-169
View File
@@ -1,169 +0,0 @@
# GetAllSpellsWithLE Funktion
## Überblick
Die `GetAllSpellsWithLE` Funktion wurde implementiert, um alle verfügbaren Zauber mit ihren Lerneinheiten (LE) und Erfahrungspunkten (EP) gruppiert nach Kategorien aus der Datenbank abzurufen.
## Funktionssignatur
```go
func GetAllSpellsWithLE(characterClass string, maxLevel int) (map[string][]gin.H, error)
```
### Parameter
- `characterClass`: Die Charakterklasse (z.B. "Magier", "Hexer", "Druide")
- `maxLevel`: Maximale Zauberstufe, die abgerufen werden soll
### Rückgabe
- `map[string][]gin.H`: Eine Map mit Zauberschulen als Schlüssel und Arrays von Zaubern als Werte
- `error`: Fehlermeldung falls etwas schiefgeht
## Features
### 1. Charakterklassen-Zauberschulen-Mapping
Die Funktion verwendet ein vordefiniertes Mapping, das festlegt, welche Zauberschulen eine Charakterklasse erlernen kann:
```go
"Magier": {
"Beherrschen": true,
"Bewegen": true,
"Dweomer": true,
"Erkennen": true,
"Erschaffen": true,
"Verändern": true,
"Zerstören": true,
},
"Hexer": {
"Beherrschen": true,
"Dweomer": true,
"Erkennen": true,
"Verändern": true,
},
```
### 2. Lerneinheiten-Berechnung
Die LE-Kosten werden aus der Datenbank-Tabelle `learning_spell_level_le_costs` abgerufen, die die Zuordnung von Zauberstufen zu LE-Kosten enthält.
**Datenbankstruktur für SpellLevelLECost:**
```go
type SpellLevelLECost struct {
ID uint `gorm:"primaryKey" json:"id"`
Level int `gorm:"uniqueIndex;not null" json:"level"`
LERequired int `gorm:"not null" json:"le_required"`
GameSystem string `gorm:"index;default:midgard" json:"game_system"`
}
```
**Fallback bei fehlenden Datenbankeinträgen:**
Falls keine Einträge in der Datenbank gefunden werden, verwendet die Funktion Standard Midgard-Werte:
```go
Stufe 1: 1 LE Stufe 7: 8 LE
Stufe 2: 2 LE Stufe 8: 10 LE
Stufe 3: 3 LE Stufe 9: 12 LE
Stufe 4: 4 LE Stufe 10: 15 LE
Stufe 5: 5 LE Stufe 11: 18 LE
Stufe 6: 6 LE Stufe 12: 21 LE
```
### 3. EP-Kostenberechnung
EP-Kosten werden basierend auf Charakterklasse und Zauberschule berechnet:
- **Magier**: 10 EP pro LE für alle Schulen
- **Hexer**: 30 EP pro LE für erlaubte Schulen
- **Druide**: 20 EP pro LE für erlaubte Schulen
- **Schamane**: 40 EP pro LE für erlaubte Schulen
- **Priester**: 30 EP pro LE für erlaubte Schulen
- **Barde**: 60 EP pro LE für erlaubte Schulen
- **Ordenskrieger**: 90 EP pro LE für erlaubte Schulen
## Beispiel-Nutzung
```go
// Alle Zauber bis Stufe 3 für einen Hexer
spells, err := GetAllSpellsWithLE("Hexer", 3)
if err != nil {
log.Fatal(err)
}
// Ausgabe: Zauber gruppiert nach Schulen (Beherrschen, Verändern, etc.)
```
## Beispiel-Ausgabe für Hexer (maxLevel: 3)
```json
{
"Beherrschen": [
{
"description": "Hebt andere Zauber auf",
"ep_cost": 30,
"id": 1,
"le_cost": 1,
"learning_category": "Beherrschen",
"level": 1,
"name": "Bannen von Zauberwerk",
"school": "Beherrschen"
},
{
"description": "Beruhigt den Geist",
"ep_cost": 60,
"id": 2,
"le_cost": 2,
"learning_category": "Beherrschen",
"level": 2,
"name": "Geistesruhe",
"school": "Beherrschen"
}
],
"Verändern": [
{
"description": "Heilt körperliche Verletzungen",
"ep_cost": 30,
"id": 6,
"le_cost": 1,
"learning_category": "Verändern",
"level": 1,
"name": "Heilen von Wunden",
"school": "Verändern"
}
]
}
```
## Datenbankstruktur
Die Funktion erwartet folgende Felder in der `gsm_spells` Tabelle:
- `id`: Eindeutige ID des Zaubers
- `name`: Name des Zaubers
- `beschreibung`: Beschreibung des Zaubers
- `stufe`: Zauberstufe (1-12)
- `category`: Anzeige-Kategorie des Zaubers
- `learning_category`: Interne Lernkategorie für EP-Berechnung
- Zusätzliche Felder: `ap`, `art`, `zauberdauer`, `reichweite`, `wirkungsziel`, `wirkungsbereich`, `wirkungsdauer`
## Beispiel-Testdaten
Siehe `example_spell_data.sql` für vollständige Beispieldaten verschiedener Zauberschulen und Stufen.
## Sortierung
Zauber werden innerhalb jeder Kategorie zuerst nach Stufe, dann alphabetisch nach Name sortiert.
## Unterstützte Charakterklassen
- Magier (Ma) - Vollzugriff auf alle Schulen
- Hexer (Hx) - Beherrschen, Dweomer, Erkennen, Verändern
- Druide (Dr) - Bewegen, Erkennen, Erschaffen, Verändern
- Schamane (Sc) - Beherrschen, Erkennen, Verändern
- Priester Beschützer (PB) - Dweomer, Erkennen, Verändern
- Priester Streiter (PS) - Dweomer, Erkennen, Verändern
- Barde (Ba) - Beherrschen, Dweomer, Erkennen
- Ordenskrieger (Or) - Dweomer, Erkennen
Charakterklassen, die nicht in der Mapping-Tabelle stehen, erhalten eine leere Antwort (können keine Zauber lernen).
-190
View File
@@ -1,190 +0,0 @@
# Bamort Logging System
## Überblick
Das Bamort Logging System bietet konfigurierbare Debug- und Standard-Logs, die über Umgebungsvariablen oder .env-Dateien gesteuert werden können.
## Konfiguration
### .env-Dateien
Das System lädt automatisch `.env` und `.env.local` Dateien aus dem Projektverzeichnis, falls vorhanden.
**Priorität der Konfigurationsquellen:**
1. Bereits gesetzte Umgebungsvariablen (höchste Priorität)
2. .env.local Datei
3. .env Datei
4. Standard-Werte (niedrigste Priorität)
**Beispiel .env-Datei:**
```bash
# Bamort Server Konfiguration
ENVIRONMENT=development
DEBUG=true
LOG_LEVEL=DEBUG
PORT=8180
DATABASE_URL=postgresql://user:pass@localhost:5432/bamort
```
### Umgebungsvariablen
### Debug-Modus
```bash
# Debug-Modus aktivieren
DEBUG=true
# oder
DEBUG=1
```
### Log-Level
```bash
# Verfügbare Log-Level: DEBUG, INFO, WARN, ERROR
LOG_LEVEL=DEBUG
LOG_LEVEL=INFO
LOG_LEVEL=WARN
LOG_LEVEL=ERROR
```
### Environment
```bash
# Umgebung definieren
ENVIRONMENT=development # Aktiviert automatisch Debug-Modus
ENVIRONMENT=production # Deaktiviert Debug-Modus standardmäßig
# oder
GO_ENV=development
GO_ENV=production
```
### Server-Port
```bash
# Server-Port konfigurieren
PORT=8180
# oder
SERVER_PORT=8180
```
## Verwendung im Code
### Import
```go
import "bamort/logger"
```
### Logging-Funktionen
```go
// Debug-Messages (nur sichtbar wenn Debug-Modus aktiviert)
logger.Debug("Debug-Information: %s", variable)
logger.Debugf("Debug mit Printf-Syntax: %d", number)
// Info-Messages
logger.Info("Server gestartet auf Port %s", port)
logger.Infof("Benutzer %s angemeldet", username)
// Warning-Messages
logger.Warn("Warnung: %s", warningMessage)
logger.Warnf("Performance-Warnung: %dms", duration)
// Error-Messages
logger.Error("Fehler beim Laden der Datenbank: %s", err.Error())
logger.Errorf("HTTP-Fehler %d: %s", statusCode, message)
```
### Programmgesteuerte Konfiguration
```go
import (
"bamort/logger"
"bamort/config"
)
// Konfiguration laden
cfg := config.LoadConfig()
// Debug-Modus setzen
logger.SetDebugMode(true)
// Minimales Log-Level setzen
logger.SetMinLogLevel(logger.DEBUG)
// Debug-Status prüfen
if logger.IsDebugEnabled() {
// Nur ausführen wenn Debug aktiv
}
```
## Beispiel-Konfigurationen
### Development
```bash
export ENVIRONMENT=development
export DEBUG=true
export LOG_LEVEL=DEBUG
export PORT=8180
```
### Production
```bash
export ENVIRONMENT=production
export DEBUG=false
export LOG_LEVEL=INFO
export PORT=8180
```
### Testing
```bash
export ENVIRONMENT=test
export DEBUG=true
export LOG_LEVEL=WARN
export PORT=8181
```
## Log-Format
```
[2025-08-10 15:04:05] DEBUG: Debug-Nachricht hier
[2025-08-10 15:04:05] INFO: Info-Nachricht hier
[2025-08-10 15:04:05] WARN: Warning-Nachricht hier
[2025-08-10 15:04:05] ERROR: Error-Nachricht hier
```
## Docker-Konfiguration
### Dockerfile
```dockerfile
# Für Development
ENV ENVIRONMENT=development
ENV DEBUG=true
ENV LOG_LEVEL=DEBUG
# Für Production
ENV ENVIRONMENT=production
ENV DEBUG=false
ENV LOG_LEVEL=INFO
```
### docker-compose.yml
```yaml
version: '3.8'
services:
bamort-api:
environment:
- ENVIRONMENT=development
- DEBUG=true
- LOG_LEVEL=DEBUG
- PORT=8180
```
## Best Practices
1. **Debug-Messages**: Verwenden Sie Debug-Messages für detaillierte Informationen, die nur während der Entwicklung relevant sind.
2. **Info-Messages**: Verwenden Sie Info-Messages für wichtige Ereignisse wie Server-Start, Benutzeraktionen, etc.
3. **Warning-Messages**: Verwenden Sie Warn-Messages für potentielle Probleme, die nicht zum Absturz führen.
4. **Error-Messages**: Verwenden Sie Error-Messages für tatsächliche Fehler und Exceptions.
5. **Performance**: Debug-Messages werden automatisch gefiltert, wenn der Debug-Modus deaktiviert ist, daher keine Performance-Einbußen in der Produktion.
6. **Sensitive Daten**: Niemals Passwörter, Token oder andere sensitive Daten loggen, besonders nicht in höheren Log-Levels.
-152
View File
@@ -1,152 +0,0 @@
# Test Environment Configuration
## Überblick
Alle Tests in diesem Projekt sind so konfiguriert, dass sie `ENVIRONMENT=test` verwenden, um sicherzustellen, dass:
1. Die richtige Datenbank-Konfiguration verwendet wird (Test-DB statt Production-DB)
2. Logger-Ausgaben reduziert sind
3. Tests isoliert von der Produktionsumgebung laufen
## Test-Umgebung einrichten
### Option 1: Lokale setupTestEnvironment Funktion (empfohlen)
Jeder Test sollte eine lokale `setupTestEnvironment` Funktion verwenden:
```go
package mypackage
import (
"os"
"testing"
)
// setupTestEnvironment setzt ENVIRONMENT=test für Tests
func setupTestEnvironment(t *testing.T) {
original := os.Getenv("ENVIRONMENT")
os.Setenv("ENVIRONMENT", "test")
t.Cleanup(func() {
if original != "" {
os.Setenv("ENVIRONMENT", original)
} else {
os.Unsetenv("ENVIRONMENT")
}
})
}
func TestMyFunction(t *testing.T) {
setupTestEnvironment(t)
// Ihr Test-Code hier...
}
```
### Option 2: testutils Package (für komplexere Setups)
Für komplexere Test-Setups verwenden Sie das `testutils` Package:
```go
package mypackage
import (
"bamort/testutils"
"testing"
)
func TestMyFunction(t *testing.T) {
testutils.SetupTestEnvironment(t)
// Oder für spezifische Konfiguration:
testutils.SetupTestEnvironmentWithConfig(t, map[string]string{
"DEBUG": "false",
"LOG_LEVEL": "ERROR",
})
// Ihr Test-Code hier...
}
```
## Aktualisierte Test-Dateien
Folgende Test-Dateien wurden bereits aktualisiert:
-`config/config_test.go` - Alle Tests setzen ENVIRONMENT=test
-`logger/logger_test.go` - Alle Tests setzen ENVIRONMENT=test
-`character/character_test.go` - setupTestEnvironment hinzugefügt
-`character/handlers_test.go` - Test-Umgebung konfiguriert
## Database-Tests
Die `database.ConnectDatabase()` Funktion erkennt automatisch `ENVIRONMENT=test` und verwendet die Test-Datenbank:
```go
func ConnectDatabase() *gorm.DB {
cfg := config.LoadConfig()
if cfg.Environment == "test" {
logger.Debug("Test-Umgebung erkannt, verwende Test-Datenbank")
SetupTestDB()
} else {
logger.Debug("Verwende konfigurierte Datenbank (%s)", cfg.DatabaseType)
return ConnectDatabaseOrig()
}
return DB
}
```
## Best Practices
1. **Immer setupTestEnvironment() aufrufen**: Jeder Test sollte die Umgebung konfigurieren
2. **Cleanup automatisch**: Verwenden Sie `t.Cleanup()` um ursprüngliche Werte wiederherzustellen
3. **Test-Isolation**: Tests sollten sich nicht gegenseitig beeinflussen
4. **Keine Produktionsdaten**: Tests verwenden immer Test-Datenbank
## Verifikation
Um zu prüfen, ob alle Tests korrekt konfiguriert sind:
```bash
# Script ausführen
./verify_test_environment.sh
# Einzelne Packages testen
go test ./config -v
go test ./logger -v
go test ./character -v
```
## Migration bestehender Tests
Für bestehende Tests, die noch nicht aktualisiert wurden:
1. Import für `os` hinzufügen
2. `setupTestEnvironment` Funktion hinzufügen
3. `setupTestEnvironment(t)` am Anfang jeder Test-Funktion aufrufen
Beispiel:
```go
// Vor der Migration
func TestMyFunction(t *testing.T) {
// Test-Code...
}
// Nach der Migration
func TestMyFunction(t *testing.T) {
setupTestEnvironment(t)
// Test-Code...
}
```
## Troubleshooting
**Problem**: Test verwendet Produktions-Datenbank
**Lösung**: `setupTestEnvironment(t)` am Anfang des Tests aufrufen
**Problem**: Umgebungsvariablen beeinflussen sich zwischen Tests
**Lösung**: `t.Cleanup()` verwenden für automatisches Aufräumen
**Problem**: Import-Zyklen bei testutils
**Lösung**: Lokale `setupTestEnvironment` Funktion in jedem Package verwenden
-189
View File
@@ -1,189 +0,0 @@
# Derived Values API Dokumentation
Das neue System für abgeleitete Werte teilt die Berechnungen in zwei Kategorien auf:
## 1. Statische Felder (ohne Würfelwürfe)
**Endpunkt:** `POST /api/characters/calculate-static-fields`
Berechnet alle Felder, die keine Würfelwürfe benötigen.
### Request:
```json
{
"st": 70,
"gs": 60,
"gw": 65,
"ko": 75,
"in": 50,
"zt": 30,
"au": 55,
"rasse": "Menschen",
"typ": "Krieger"
}
```
### Response:
```json
{
"ausdauer_bonus": 10,
"schadens_bonus": 2,
"angriffs_bonus": 0,
"abwehr_bonus": 0,
"zauber_bonus": -1,
"resistenz_bonus_koerper": 1,
"resistenz_bonus_geist": 0,
"resistenz_koerper": 12,
"resistenz_geist": 11,
"abwehr": 11,
"zaubern": 10,
"raufen": 6
}
```
## 2. Würfelfelder (mit Würfelwürfen)
**Endpunkt:** `POST /api/characters/calculate-rolled-field`
Berechnet einzelne Felder mit Würfelwürfen, die vom Frontend bereitgestellt werden.
### PA (Persönliche Ausstrahlung):
```json
{
"st": 70, "gs": 60, "gw": 65, "ko": 75, "in": 50, "zt": 30, "au": 55,
"rasse": "Menschen",
"typ": "Krieger",
"field": "pa",
"roll": 55
}
```
Response:
```json
{
"field": "pa",
"value": 55,
"formula": "1d100 + 4×In/10 - 20",
"details": {
"roll": 55,
"in_bonus": 20,
"base_modifier": -20,
"modifier": 0
}
}
```
### WK (Willenskraft):
```json
{
"st": 70, "gs": 60, "gw": 65, "ko": 75, "in": 50, "zt": 30, "au": 55,
"rasse": "Menschen",
"typ": "Krieger",
"field": "wk",
"roll": 45
}
```
### LP Max (Lebenspunkte Maximum):
```json
{
"st": 70, "gs": 60, "gw": 65, "ko": 75, "in": 50, "zt": 30, "au": 55,
"rasse": "Menschen",
"typ": "Krieger",
"field": "lp_max",
"roll": 2
}
```
### AP Max (Abenteuerpunkte Maximum):
```json
{
"st": 70, "gs": 60, "gw": 65, "ko": 75, "in": 50, "zt": 30, "au": 55,
"rasse": "Menschen",
"typ": "Krieger",
"field": "ap_max",
"roll": 3
}
```
### B Max (Bewegungsweite):
```json
{
"st": 70, "gs": 60, "gw": 65, "ko": 75, "in": 50, "zt": 30, "au": 55,
"rasse": "Menschen",
"typ": "Krieger",
"field": "b_max",
"roll": [2, 1, 3, 2]
}
```
## Verwendung im Frontend
### 1. Alle statischen Felder auf einmal berechnen:
```javascript
const calculateStaticFields = async (attributes, race, type) => {
const response = await fetch('/api/characters/calculate-static-fields', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
st: attributes.st,
gs: attributes.gs,
gw: attributes.gw,
ko: attributes.ko,
in: attributes.in,
zt: attributes.zt,
au: attributes.au,
rasse: race,
typ: type
})
});
return await response.json();
};
```
### 2. Einzelne Würfelfelder berechnen:
```javascript
const calculateRolledField = async (attributes, race, type, field, diceRoll) => {
const response = await fetch('/api/characters/calculate-rolled-field', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...attributes,
rasse: race,
typ: type,
field: field,
roll: diceRoll
})
});
return await response.json();
};
// Beispiele:
// PA: roll = einzelner Würfelwert (1-100)
// WK: roll = einzelner Würfelwert (1-100)
// LP Max: roll = einzelner Würfelwert (1-3)
// AP Max: roll = einzelner Würfelwert (1-3)
// B Max: roll = Array von Würfelwerten (je nach Rasse 2-4 Werte von 1-3)
```
## Würfelwürfe im Frontend generieren
```javascript
const rollD100 = () => Math.floor(Math.random() * 100) + 1;
const rollD3 = () => Math.floor(Math.random() * 3) + 1;
const getMovementDiceCount = (race) => {
switch(race) {
case 'Gnome':
case 'Halblinge':
return 2;
case 'Zwerge':
return 3;
default:
return 4;
}
};
const rollMovement = (race) => {
const diceCount = getMovementDiceCount(race);
return Array.from({length: diceCount}, () => rollD3());
};
```
-55
View File
@@ -1,55 +0,0 @@
# API Examples
## Create Ausruestung
POST /ausruestung
{
"character_id": 1,
"name": "Magic Sword",
"anzahl": 1,
"gewicht": 2.5,
"wert": 150.0,
"beinhaltet_in": null,
"beschreibung": "A glowing sword imbued with magical energy.",
"bonus": 5,
"ist_magisch": true,
"abw": 10,
"ausgebrannt": false
}
## Get All Ausruestung for a Character
GET /ausruestung/1 Response:
[
{
"ausruestung_id": 1,
"character_id": 1,
"name": "Magic Sword",
"anzahl": 1,
"gewicht": 2.5,
"wert": 150.0,
"beinhaltet_in": null,
"beschreibung": "A glowing sword imbued with magical energy.",
"bonus": 5,
"ist_magisch": true,
"abw": 10,
"ausgebrannt": false
}
]
## Update Ausruestung
PUT /ausruestung/1
{
"anzahl": 2,
"gewicht": 3.0
}
## Delete Ausruestung
DELETE /ausruestung/1
## Summary
These endpoints provide complete CRUD functionality for managing Ausruestung items. They integrate with the database and offer flexibility to interact with equipment data.
-212
View File
@@ -1,212 +0,0 @@
# GetLernCostNewSystem - Neue Datenbank-basierte Lernkosten
## Übersicht
Die Funktion `GetLernCostNewSystem` ist eine neue Implementierung des Lernkosten-Systems, die anstelle der hardkodierten `learningCostsData` eine vollständige Datenbank-Lösung verwendet.
## Funktionen
### Alte vs Neue Implementation
| Aspect | GetLernCost (Alt) | GetLernCostNewSystem (Neu) |
|--------|------------------|---------------------------|
| Datenquelle | `learningCostsData` (hardkodiert) | Datenbank-Tabellen (`learning_*`) |
| Flexibilität | Statisch | Dynamisch konfigurierbar |
| Verwaltung | Code-Änderungen nötig | Admin-Interface möglich |
| Performance | Schnell (im Speicher) | Leicht langsamer (DB-Queries) |
| Skalierbarkeit | Begrenzt | Unbegrenzt |
### API-Kompatibilität
Beide Funktionen verwenden die gleiche Request/Response-Struktur:
```go
// Request (identisch)
type LernCostRequest struct {
CharId uint `json:"char_id"`
Name string `json:"name"`
CurrentLevel int `json:"current_level"`
Type string `json:"type"`
Action string `json:"action"`
UsePP int `json:"use_pp"`
UseGold int `json:"use_gold"`
Reward *string `json:"reward"`
}
// Response (identisch)
type SkillCostResultNew struct {
CharacterID string `json:"character_id"`
CharacterClass string `json:"character_class"`
SkillName string `json:"skill_name"`
Category string `json:"category"`
Difficulty string `json:"difficulty"`
TargetLevel int `json:"target_level"`
EP int `json:"ep"`
GoldCost int `json:"gold_cost"`
// ... weitere Felder
}
```
## Endpoints
```
POST /api/characters/lerncost # Altes System (learningCostsData)
POST /api/characters/lerncost-new # Neues System (Datenbank)
```
## Setup und Initialisierung
### 1. Lernkosten-System initialisieren
```bash
# CLI-Tool verwenden
cd backend
go run cmd/learning_costs_cli/main.go -init-learning
# Oder HTTP-Endpoint
curl -X POST http://localhost:8080/api/maintenance/initialize-learning-costs
```
### 2. Validierung
```bash
# CLI-Tool
go run cmd/learning_costs_cli/main.go -validate-learning
# Zusammenfassung anzeigen
go run cmd/learning_costs_cli/main.go -summary-learning
```
## Verwendung
### Beispiel-Request
```bash
curl -X POST http://localhost:8080/api/characters/lerncost-new \
-H "Content-Type: application/json" \
-d '{
"char_id": 20,
"name": "Athletik",
"current_level": 9,
"type": "skill",
"action": "improve",
"use_pp": 0,
"use_gold": 0,
"reward": "default"
}'
```
### Beispiel-Response
```json
[
{
"character_id": "20",
"character_class": "Kr",
"skill_name": "Athletik",
"category": "Körper",
"difficulty": "normal",
"target_level": 10,
"ep": 40,
"gold_cost": 40,
"le": 0,
"pp_used": 0,
"gold_used": 0
}
// ... weitere Level bis 18
]
```
## Vorteile des neuen Systems
### 1. **Flexibilität**
- Lernkosten können zur Laufzeit geändert werden
- Neue Charakterklassen, Fertigkeiten und Schwierigkeitsgrade einfach hinzufügbar
- Verschiedene Regelbücher/Quellen können aktiviert/deaktiviert werden
### 2. **Verwaltbarkeit**
- Admin-Interface möglich
- Datenexport und -import
- Versioning und Änderungshistorie
### 3. **Skalierbarkeit**
- Unterstützt beliebig viele Fertigkeiten und Klassen
- Optimierte Datenbankabfragen
- Caching-Möglichkeiten
### 4. **Datenintegrität**
- Foreign Key Constraints
- Validierung auf Datenbankebene
- Transaktionale Konsistenz
## Migration vom alten System
### Schritt 1: Parallel-Betrieb
Beide Systeme können parallel laufen. Clients können zwischen den Endpoints wählen:
- `/lerncost` für das alte System
- `/lerncost-new` für das neue System
### Schritt 2: Testing und Validierung
```go
// Test beide Systeme und vergleiche Ergebnisse
func compareSystems(request LernCostRequest) {
oldResponse := callOldSystem(request)
newResponse := callNewSystem(request)
// Vergleiche Ergebnisse und identifiziere Unterschiede
compareResults(oldResponse, newResponse)
}
```
### Schritt 3: Umstellung
Nach erfolgreicher Validierung kann der alte Endpoint durch den neuen ersetzt werden.
## Fehlerbehebung
### Häufige Fehler
1. **"Fertigkeit nicht gefunden"**
- Lernkosten-System nicht initialisiert
- Fertigkeit existiert nicht in der Datenbank
- Lösung: `InitializeLearningCostsSystem()` ausführen
2. **"Charakterklasse nicht verfügbar"**
- Klasse nicht in `learning_character_classes` Tabelle
- Lösung: Klasse zur Datenbank hinzufügen
3. **"EP-Kosten nicht gefunden"**
- Fehlende Einträge in `learning_class_category_ep_costs`
- Lösung: Daten für die spezifische Klasse/Kategorie-Kombination hinzufügen
### Debug-Informationen
```bash
# Prüfe Datenbank-Status
go run cmd/learning_costs_cli/main.go -summary-learning
# Validiere Daten
go run cmd/learning_costs_cli/main.go -validate-learning
```
## Technische Details
### Datenbank-Schema
Das neue System verwendet folgende Tabellen:
- `learning_sources` - Regelbücher/Quellen
- `learning_character_classes` - Charakterklassen
- `learning_skill_categories` - Fertigkeitskategorien
- `learning_skill_difficulties` - Schwierigkeitsgrade
- `learning_class_category_ep_costs` - EP-Kosten pro Klasse/Kategorie
- `learning_skill_category_difficulties` - Fertigkeiten-Zuordnungen
- `learning_skill_improvement_costs` - Verbesserungskosten
- `learning_spell_schools` - Zauberschulen
- `learning_class_spell_school_ep_costs` - Zauber-EP-Kosten
- `learning_spell_level_le_costs` - LE-Kosten pro Zaubergrad
### Performance-Optimierungen
- Indizierte Spalten für häufige Abfragen
- JOIN-optimierte Queries
- Möglichkeit für Query-Caching
- Prepared Statements
-106
View File
@@ -1,106 +0,0 @@
# Konsolidierung der Character Skill Cost API
## ✅ **Was wurde konsolidiert:**
### **Entfernte Redundanzen:**
-`learning_cost_system.go` - redundante Implementierung
-`learning_cost_system_test.go` - redundante Tests
- ❌ Doppelte Routen für Kostenberechnung
-`CalculateSkillCostHandler` - ersetzt durch bestehenden Handler
-`CalculateSkillCostForCharacterHandler` - nicht benötigt
### **Beibehaltene Lösung:**
-**`skill_costs_handler.go`** - Vollständige, ausgereifte Implementierung
-**GSMaster-Integration** - Nutzt bestehende Datenbank-Strukturen
-**Praxispunkte-Integration** - Automatische PP-Berücksichtigung
-**Multi-Level-Berechnung** - Kosten für mehrere Stufen
## 🎯 **Finale Routen-Struktur:**
```go
// Hauptendpunkt (konsolidiert)
POST /api/characters/:id/skill-cost
// Legacy-Endpunkte (beibehalten für Kompatibilität)
GET /api/characters/:id/improve
GET /api/characters/:id/improve/skill
GET /api/characters/:id/learn
// System-Information
GET /api/characters/character-classes
GET /api/characters/skill-categories
```
## 🚀 **Hauptendpunkt Funktionalitäten:**
### **Ein Endpunkt für alles:**
- ✅ Fertigkeiten lernen
- ✅ Fertigkeiten verbessern (1 Stufe)
- ✅ Multi-Level-Verbesserung
- ✅ Zauber lernen
- ✅ Automatische Praxispunkte-Integration
- ✅ Automatische Skill-Erkennung aus GSMaster-DB
### **Request-Beispiele:**
```json
// Fertigkeit lernen
{"name": "Stehlen", "type": "skill", "action": "learn"}
// Verbessern mit PP
{"name": "Menschenkenntnis", "type": "skill", "action": "improve", "use_pp": 2}
// Multi-Level
{"name": "Dolch", "type": "weapon", "action": "improve", "current_level": 10, "target_level": 12}
```
## 📊 **Vorteile der Konsolidierung:**
### **Für Entwickler:**
- 🎯 **Eine API** statt mehrere verschiedene
- 🔧 **Konsistente Struktur** für alle Anfragen
- 🗃️ **Weniger Code** zu maintainen
- 🧪 **Einheitliche Tests**
### **Für Nutzer:**
- 📝 **Einfachere Integration** - nur ein Endpunkt lernen
- 🔄 **Vollständige Funktionalität** in einer Anfrage
-**Bessere Performance** durch weniger API-Calls
- 🔍 **Automatische Erkennung** von Skill-Eigenschaften
## 🔧 **Technische Verbesserungen:**
### **Automatisierung:**
- 🤖 Charakterklasse aus Charakter-Daten
- 🤖 Aktueller Skill-Level aus Charakter
- 🤖 Skill-Kategorie und Schwierigkeit aus GSMaster-DB
- 🤖 Praxispunkte-Verfügbarkeit und -Anwendung
### **Validierung:**
- ✅ Charakterklassen-Beschränkungen
- ✅ Skill-Verfügbarkeit prüfen
- ✅ Level-Grenzen validieren
- ✅ PP-Verfügbarkeit prüfen
## 📈 **Migration Path:**
### **Sofort nutzbar:**
Alle bestehenden Legacy-Endpunkte funktionieren weiterhin.
### **Empfohlener Übergang:**
1. **Neue Implementierungen** → Nutze `POST /:id/skill-cost`
2. **Bestehender Code** → Kann schrittweise migriert werden
3. **Legacy-Endpunkte** → Bleiben für Rückwärtskompatibilität
## 🎉 **Ergebnis:**
**Von 8 verschiedenen Endpunkten zu 1 Hauptendpunkt**
- Reduzierte Komplexität
- Verbesserte Funktionalität
- Bessere Maintainability
- Konsistente API-Erfahrung
Die konsolidierte Lösung nutzt das Beste aus beiden Welten:
- Die ausgereifte GSMaster-Integration
- Die vollständige Praxispunkte-Unterstützung
- Die Multi-Level-Berechnung
- Plus einfache System-Info-Endpunkte
@@ -1,82 +0,0 @@
# Experience and Wealth Update API
Diese API-Endpunkte ermöglichen es, Erfahrungspunkte und Vermögen von Charakteren zu aktualisieren.
## Erfahrungspunkte aktualisieren
### PUT /api/characters/{id}/experience
Aktualisiert die Erfahrungspunkte eines Charakters.
**Request Body:**
```json
{
"experience_points": 150
}
```
**Response (Success):**
```json
{
"message": "Experience updated successfully",
"experience_points": 150
}
```
**Validierung:**
- `experience_points` muss >= 0 sein
## Vermögen aktualisieren
### PUT /api/characters/{id}/wealth
Aktualisiert das Vermögen eines Charakters. Es können einzelne Währungen oder alle zusammen aktualisiert werden.
**Request Body (einzelne Währung):**
```json
{
"goldstücke": 250
}
```
**Request Body (mehrere Währungen):**
```json
{
"goldstücke": 100,
"silberstücke": 50,
"kupferstücke": 200
}
```
**Response (Success):**
```json
{
"message": "Wealth updated successfully",
"wealth": {
"goldstücke": 100,
"silberstücke": 50,
"kupferstücke": 200
}
}
```
## Frontend Integration
Die ExperianceView.vue Komponente bietet jetzt:
1. **Bearbeitbare Eingabefelder** für EP und Goldstücke
2. **Automatisches Speichern** beim Verlassen der Felder oder Enter-Taste
3. **Visueller Feedback** während der Bearbeitung
4. **Fehlerbehandlung** mit Reset bei Fehlern
5. **Event Emission** (`character-updated`) für Parent-Komponenten
### Verwendung:
```vue
<ExperianceView
:character="selectedCharacter"
@character-updated="refreshCharacter"
/>
```
Das `character-updated` Event sollte in der Parent-Komponente behandelt werden, um die Charakter-Daten neu zu laden.
-244
View File
@@ -1,244 +0,0 @@
# Improved Skill Cost API Documentation
## Overview
The improved `/api/characters/:id/skill-cost` endpoint provides enhanced functionality for calculating learning and improvement costs for skills, weapons, and spells.
## Endpoint
```
POST /api/characters/:id/skill-cost
```
## Request Body
### Basic Request Structure
```json
{
"name": "string (required)",
"type": "string (required): skill|spell|weapon",
"action": "string (required): learn|improve",
"current_level": "number (optional)",
"target_level": "number (optional)"
}
```
### Validation Rules
- `name`: Required, skill/spell/weapon name
- `type`: Required, must be one of: `skill`, `spell`, `weapon`
- `action`: Required, must be one of: `learn`, `improve`
- `current_level`: Optional for `learn`, required for `improve` (unless character already has the skill)
- `target_level`: Optional, enables multi-level cost calculation
## Response Formats
### Single Cost Response
```json
{
"stufe": 11,
"le": 0,
"ep": 200,
"money": 200,
"skill_name": "Menschenkenntnis",
"skill_type": "skill",
"action": "improve",
"character_id": 1,
"current_level": 10,
"target_level": 0,
"category": "Sozial",
"difficulty": "schwer",
"can_afford": true,
"notes": "Verbesserung von 10 auf 11. Kosten für Hexer"
}
```
### Multi-Level Cost Response
```json
{
"skill_name": "Menschenkenntnis",
"skill_type": "skill",
"character_id": 1,
"current_level": 10,
"target_level": 13,
"level_costs": [
{
"stufe": 11,
"le": 0,
"ep": 200,
"money": 200,
"skill_name": "Menschenkenntnis",
"skill_type": "skill",
"action": "improve",
"character_id": 1,
"current_level": 10,
"target_level": 11,
"category": "Sozial",
"difficulty": "schwer",
"can_afford": true
},
{
"stufe": 12,
"le": 0,
"ep": 200,
"money": 200,
"skill_name": "Menschenkenntnis",
"skill_type": "skill",
"action": "improve",
"character_id": 1,
"current_level": 11,
"target_level": 12,
"category": "Sozial",
"difficulty": "schwer",
"can_afford": true
},
{
"stufe": 13,
"le": 0,
"ep": 400,
"money": 400,
"skill_name": "Menschenkenntnis",
"skill_type": "skill",
"action": "improve",
"character_id": 1,
"current_level": 12,
"target_level": 13,
"category": "Sozial",
"difficulty": "schwer",
"can_afford": true
}
],
"total_cost": {
"stufe": 13,
"le": 0,
"ep": 800,
"money": 800
},
"can_afford_total": true
}
```
## API Improvements
### 1. Enhanced Validation
- **Binding validation**: Uses Gin's binding tags for automatic validation
- **Input sanitization**: Trims whitespace from skill names
- **Type safety**: Validates skill types and actions against allowed values
### 2. Automatic Level Detection
- **Smart level detection**: Automatically detects current skill level from character data
- **Fallback handling**: Gracefully handles missing skill data
### 3. Multi-Level Cost Calculation
- **Range calculations**: Calculate costs for improving from current level to target level
- **Detailed breakdown**: Shows individual costs for each level improvement
- **Total cost summary**: Provides cumulative cost for the entire improvement path
### 4. Enhanced Response Data
- **Skill metadata**: Includes category and difficulty information
- **Affordability check**: Indicates whether character can afford the costs
- **Contextual notes**: Provides helpful information about the cost calculation
### 5. Better Error Handling
- **Detailed error messages**: More specific error descriptions
- **Graceful degradation**: Handles missing data gracefully
- **Input validation**: Comprehensive validation with clear error messages
## Example Usage
### Learn a New Skill
```bash
curl -X POST "http://localhost:8180/api/characters/1/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Menschenkenntnis",
"type": "skill",
"action": "learn"
}'
```
### Improve an Existing Skill
```bash
curl -X POST "http://localhost:8180/api/characters/1/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Menschenkenntnis",
"type": "skill",
"action": "improve",
"current_level": 10
}'
```
### Multi-Level Improvement
```bash
curl -X POST "http://localhost:8180/api/characters/1/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Menschenkenntnis",
"type": "skill",
"action": "improve",
"current_level": 10,
"target_level": 15
}'
```
### Learn a Spell
```bash
curl -X POST "http://localhost:8180/api/characters/1/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Feuerball",
"type": "spell",
"action": "learn"
}'
```
### Improve a Weapon Skill
```bash
curl -X POST "http://localhost:8180/api/characters/1/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Beidhändiger Kampf",
"type": "weapon",
"action": "improve",
"current_level": 5
}'
```
## Error Responses
### Validation Errors
```json
{
"error": "Ungültige Anfrageparameter: Key: 'SkillCostRequest.Type' Error:Field validation for 'Type' failed on the 'oneof' tag"
}
```
### Character Not Found
```json
{
"error": "Charakter nicht gefunden"
}
```
### Skill Not Found
```json
{
"error": "Fehler bei der Kostenberechnung: unbekannte Fertigkeit"
}
```
### Invalid Level Range
```json
{
"error": "Fehler bei der Multi-Level-Kostenberechnung"
}
```
## Benefits of the Improvements
1. **Better User Experience**: More detailed and helpful responses
2. **Reduced API Calls**: Multi-level calculation reduces need for multiple requests
3. **Better Error Handling**: Clear, actionable error messages
4. **Automatic Detection**: Reduces manual input requirements
5. **Comprehensive Data**: Includes all relevant information for frontend display
6. **Type Safety**: Prevents invalid requests through validation
7. **Extensibility**: Easy to add new skill types or actions
8. **Performance**: Efficient calculation and response structure
-191
View File
@@ -1,191 +0,0 @@
# Character Skill Cost API
Konsolidierte API für die Berechnung von Lern- und Verbesserungskosten für Fertigkeiten und Zauber.
## Hauptendpunkt
### Kostenberechnung für Charakter
```
POST /api/characters/:id/skill-cost
```
Dies ist der **Hauptendpunkt** für alle Kostenberechnungen. Er nutzt das bestehende GSMaster-System und die Praxispunkte-Integration.
**Request Body:**
```json
{
"name": "Stehlen",
"type": "skill",
"action": "learn",
"current_level": 14,
"target_level": 16,
"use_pp": 2
}
```
**Response:**
```json
{
"skill_name": "Stehlen",
"skill_type": "skill",
"action": "improve",
"character_id": 123,
"current_level": 14,
"target_level": 16,
"category": "Halbwelt",
"difficulty": "schwer",
"stufe": 16,
"le": 0,
"ep": 180,
"money": 180,
"original_cost": 200,
"final_cost": 180,
"pp_used": 1,
"pp_available": 2,
"description": "Verbesserung von Stehlen von +14 auf +16 (mit 1 PP)"
}
```
### Parameter
#### Pflichtfelder
- `name`: Name der Fertigkeit oder des Zaubers
- `type`: "skill", "spell" oder "weapon"
- `action`: "learn" oder "improve"
#### Optionale Felder
- `current_level`: Aktueller Wert (automatisch ermittelt falls nicht angegeben)
- `target_level`: Zielwert (für Multi-Level-Berechnung)
- `use_pp`: Anzahl der zu verwendenden Praxispunkte
## Legacy-Endpunkte
### Nächste Stufe
```
GET /api/characters/:id/improve
```
Zeigt Kosten für die nächste Verbesserungsstufe aller Fertigkeiten.
### Alle Stufen
```
GET /api/characters/:id/improve/skill
```
Zeigt Kosten für alle möglichen Verbesserungsstufen.
### Einfache Lernkosten
```
GET /api/characters/:id/learn
```
Zeigt grundlegende Lernkosten für neue Fertigkeiten.
## System-Information
### Charakterklassen
```
GET /api/characters/character-classes
```
**Response:**
```json
{
"character_classes": {
"Sp": {
"code": "Sp",
"name": "Spitzbube",
"skill_ep_costs": {
"Halbwelt": 10,
"Sozial": 10,
"Waffen": 20
},
"spell_ep_costs": {}
}
}
}
```
### Fertigkeitskategorien
```
GET /api/characters/skill-categories
```
**Response:**
```json
{
"skill_categories": {
"Halbwelt": {
"name": "Halbwelt",
"learn_costs": {
"leicht": 1,
"schwer": 2
},
"improve_costs": {
"schwer": {
"15": 20,
"16": 50
}
}
}
}
}
```
## Praxispunkte-Integration
Das System unterstützt automatisch Praxispunkte:
1. **Automatische Ermittlung**: Verfügbare PP werden automatisch geladen
2. **Kostenreduzierung**: 1 PP = 1 LE weniger Kosten
3. **Validierung**: Prüfung ob genügend PP vorhanden sind
### Beispiel mit Praxispunkten
```json
{
"name": "Menschenkenntnis",
"type": "skill",
"action": "improve",
"current_level": 14,
"use_pp": 2
}
```
## Features
### ✅ Automatische Erkennung
- Charakterklasse aus Charakter-Daten
- Aktueller Fertigkeitswert aus Charakter
- Fertigkeitskategorie und Schwierigkeit aus GSMaster-DB
### ✅ Multi-Level-Berechnung
- Kosten für mehrere Stufen auf einmal
- Berücksichtigung gestaffelter Kosten
### ✅ Praxispunkte-Integration
- Automatische PP-Verwaltung
- Kostenreduzierung durch PP
- PP-Verfügbarkeitsprüfung
### ✅ Vollständige Validierung
- Charakterklassen-Beschränkungen
- Fertigkeits-Verfügbarkeit
- Level-Grenzen
## Fehlerbehandlung
Das System gibt detaillierte Fehlermeldungen zurück:
```json
{
"error": "Fertigkeit nicht bei diesem Charakter vorhanden oder current_level erforderlich"
}
```
## Migration von alten Endpunkten
**Alt:** Multiple verschiedene Endpunkte
**Neu:** Ein einheitlicher Endpunkt `POST /api/characters/:id/skill-cost`
Der neue Endpunkt kombiniert alle Funktionalitäten und bietet:
- Konsistente Request/Response-Struktur
- Vollständige GSMaster-Integration
- Automatische Praxispunkte-Berücksichtigung
- Multi-Level-Kostenberechnung
@@ -1,168 +0,0 @@
# Normalisierte Datenbankstruktur für Lernkosten
## Übersicht
Die `LearningCostsTable2` Strukt### 5. **Erweiterte Migration**
- Erstellt Standardquellen automatisch (KOD, ARK, MYS, UNB)
- Verknüpft Charakterklassen mit KOD
- Verknüpft Fertigkeiten mit KOD (Standardquelle)
- Verknüpft Zauber mit ARK (Standardquelle)
- Verknüpft Basis-Zauberschulen mit KOD, erweiterte mit ARK
- "Unbekannt"-Kategorie wird mit UNB-Quelle verknüpfturde in eine normalisierte Datenbankstruktur transformiert, die es ermöglicht, die Lernkosten-Daten effizient in der Datenbank zu verwalten.
## Neue Tabellen
### 1. Basistabellen
#### `learning_character_classes`
Speichert alle Charakterklassen mit Code und vollständigem Namen.
- `id` (PK)
- `code` (String, 3 Zeichen, z.B. "Hx", "Ma", "Kr")
- `name` (String, z.B. "Hexer", "Magier", "Krieger")
- `description` (Optional)
- `game_system` (Standard: "midgard")
#### `learning_skill_categories`
Speichert alle Fertigkeitskategorien.
- `id` (PK)
- `name` (String, z.B. "Alltag", "Kampf", "Waffen")
- `description` (Optional)
- `game_system` (Standard: "midgard")
#### `learning_skill_difficulties`
Speichert alle Schwierigkeitsgrade.
- `id` (PK)
- `name` (String, z.B. "leicht", "normal", "schwer", "sehr schwer")
- `description` (Optional)
- `game_system` (Standard: "midgard")
#### `learning_spell_schools`
Speichert alle Zauberschulen.
- `id` (PK)
- `name` (String, z.B. "Beherrschen", "Bewegen", "Erkennen")
- `description` (Optional)
- `game_system` (Standard: "midgard")
### 2. Kostentabellen
#### `learning_class_category_ep_costs`
EP-Kosten für 1 Trainingseinheit (TE) pro Charakterklasse und Fertigkeitskategorie.
- `id` (PK)
- `character_class_id` (FK zu learning_character_classes)
- `skill_category_id` (FK zu learning_skill_categories)
- `ep_per_te` (Integer, EP-Kosten pro TE)
#### `learning_class_spell_school_ep_costs`
EP-Kosten für 1 Lerneinheit (LE) für Zauber pro Charakterklasse und Zauberschule.
- `id` (PK)
- `character_class_id` (FK zu learning_character_classes)
- `spell_school_id` (FK zu learning_spell_schools)
- `ep_per_le` (Integer, EP-Kosten pro LE)
#### `learning_spell_level_le_costs`
LE-Kosten pro Zauber-Stufe.
- `id` (PK)
- `level` (Integer, Zauber-Stufe 1-12)
- `le_required` (Integer, benötigte Lerneinheiten)
- `game_system` (Standard: "midgard")
### 3. Verknüpfungstabellen
#### `learning_skill_category_difficulties`
Definiert die Schwierigkeit einer Fertigkeit in einer bestimmten Kategorie.
Eine Fertigkeit kann in mehreren Kategorien mit unterschiedlichen Schwierigkeiten existieren.
- `id` (PK)
- `skill_id` (FK zu lookup_lists/skills)
- `skill_category_id` (FK zu learning_skill_categories)
- `skill_difficulty_id` (FK zu learning_skill_difficulties)
- `learn_cost` (Integer, LE-Kosten für das Erlernen)
#### `learning_skill_improvement_costs`
TE-Kosten für Verbesserungen basierend auf Kategorie, Schwierigkeit und aktuellem Wert.
- `id` (PK)
- `skill_category_difficulty_id` (FK zu learning_skill_category_difficulties)
- `current_level` (Integer, aktueller Fertigkeitswert)
- `te_required` (Integer, benötigte Trainingseinheiten)
## Vorteile der neuen Struktur
### 1. Normalisierung
- Eliminierung von Redundanzen
- Konsistente Datenstruktur
- Einfache Wartung und Updates
### 2. Flexibilität
- Fertigkeiten können in mehreren Kategorien mit unterschiedlichen Schwierigkeiten existieren
- Neue Charakterklassen, Kategorien oder Schwierigkeiten können einfach hinzugefügt werden
- Lernkosten können individuell angepasst werden
### 3. Performance
- Effiziente Abfragen durch indexierte Fremdschlüssel
- Optimierte Joins für komplexe Kostenberechnungen
### 4. Erweiterbarkeit
- Einfache Integration neuer Spielsysteme über `game_system`
- Möglichkeit zur Versionierung von Lernkosten
- Unterstützung für charakterspezifische Modifikatoren
## Migration und Integration
### Migrationsfunktionen
1. **`CreateLearningCostsTables()`**
- Erstellt alle neuen Tabellen
- Definiert Indizes und Fremdschlüssel
2. **`MigrateLearningCostsToDatabase()`**
- Überträgt alle Daten aus `learningCostsData` in die Datenbank
- Erstellt Basisdaten für alle Charakterklassen, Kategorien, etc.
3. **`EnhanceLearningDataWithExistingTables()`**
- Verknüpft vorhandene Fertigkeiten und Zauber mit den neuen Tabellen
- Erstellt fehlende Verknüpfungen mit Standardwerten
4. **`InitializeLearningCostsSystem()`**
- Hauptfunktion für die komplette Migration
- Führt alle Schritte in der richtigen Reihenfolge aus
### Query-Funktionen
1. **`GetEPPerTEForClassAndCategory()`**
- Holt EP-Kosten für eine Klasse und Kategorie
2. **`GetSkillCategoryAndDifficulty()`**
- Findet die beste Kategorie für eine Fertigkeit (niedrigste EP-Kosten)
3. **`GetSpellLearningInfo()`**
- Holt alle Informationen für das Erlernen eines Zaubers
4. **`GetImprovementCost()`**
- Holt Verbesserungskosten für eine spezifische Situation
## Verwendung
```go
// System initialisieren (einmalig)
if err := InitializeLearningCostsSystem(); err != nil {
log.Fatal("Failed to initialize learning costs system:", err)
}
// Kostenberechnung für Fertigkeit
skillInfo, err := GetSkillCategoryAndDifficulty("Klettern", "Kr")
if err != nil {
log.Error("Failed to get skill info:", err)
}
// Zauber-Lernkosten ermitteln
spellInfo, err := GetSpellLearningInfo("Feuerball", "Ma")
if err != nil {
log.Error("Failed to get spell info:", err)
}
```
## Kompatibilität
Die neue Struktur ist vollständig kompatibel mit dem bestehenden System:
- Alle bestehenden Funktionen wie `CalcSkillLernCost()` können weiterhin verwendet werden
- Die neuen Query-Funktionen ersetzen schrittweise die statischen Maps
- Keine Änderungen an bestehenden APIs erforderlich
-125
View File
@@ -1,125 +0,0 @@
# Vollständige Midgard Fertigkeiten-Konsolidierung
## Charakterklassen (vollständig)
```go
"As": "Assassine",
"Bb": "Barbar",
"Gl": "Glücksritter",
"Hä": "Händler",
"Kr": "Krieger",
"Sp": "Spitzbube",
"Wa": "Waldläufer",
"Ba": "Barde",
"Or": "Ordensritter",
"Dr": "Druide",
"Hx": "Hexer",
"Ma": "Magier",
"PB": "Priester Beschützer",
"PS": "Priester Streiter",
"Sc": "Schamane"
```
## Kategorien mit allen Fertigkeiten und Schwierigkeiten
### 1. Alltag
**Lernkosten (LE):**
- leicht (1 LE): Bootfahren+12, Glücksspiel+12, Klettern+12, Musizieren+12, Reiten+12, Schwimmen+12, Seilkunst+12, Wagenlenken+12
- normal (1 LE): Schreiben+8, Sprache+8
- schwer (2 LE): Erste Hilfe+8, Etikette+8
- sehr schwer (10 LE): Gerätekunde+8, Geschäftssinn+8
### 2. Freiland
**Lernkosten (LE):**
- leicht (1 LE): Überleben+8
- normal (2 LE): Naturkunde+8, Pflanzenkunde+8, Tierkunde+8
- schwer (4 LE): Schleichen+8, Spurensuche+8, Tarnen+8
### 3. Halbwelt
**Lernkosten (LE):**
- leicht (1 LE): Balancieren+12, Fälschen+12, Gaukeln+12, Glücksspiel+12, Klettern+12
- normal (2 LE): Akrobatik+8
- schwer (2 LE): Gassenwissen+8, Stehlen+8
- sehr schwer (10 LE): Betäuben+8
### 4. Kampf
**Lernkosten (LE):**
- leicht (1 LE): Geländelauf+12, Reiten+12, Reiterkampf+12
- normal (2 LE): Anführen+8, Athletik+8
- schwer (10 LE): Betäuben+8
- sehr schwer (10 LE): Beidhändiger Kampf+5, Kampf in Vollrüstung+5, Fechten+5, Scharfschießen+5
### 5. Körper
**Lernkosten (LE):**
- leicht (1 LE): Balancieren+12, Geländelauf+12, Klettern+12, Schwimmen+12
- normal (1 LE): Tauchen+8
- schwer (2 LE): Akrobatik+8, Athletik+8, Laufen+8, Meditieren+8
### 6. Sozial
**Lernkosten (LE):**
- leicht (2 LE): Anführen+8, Etikette+8, Verführen+8, Verstellen+8
- normal (2 LE): Beredsamkeit+8, Gassenwissen+8, Verhören+8
- schwer (4 LE): Menschenkenntnis+8
### 7. Unterwelt
**Lernkosten (LE):**
- leicht (2 LE): Fälschen+12, Stehlen+8
- normal (2 LE): Gassenwissen+8, Verhören+8
- schwer (4 LE): Einschüchtern+8, Menschenkenntnis+8
### 8. Waffen
**Lernkosten (LE):**
- leicht (1 LE): Armbrust+8, Bogen+8, Dolch+8, Hiebwaffen+8, Kettenwaffen+8, Wurfwaffen+8
- normal (2 LE): Fechten+5, Schilde+8, Stangenwaffen+8, Zweihandhiebwaffen+8, Zweihandflegel+8
- schwer (10 LE): Beidhändiger Kampf+5, Kampf in Vollrüstung+5, Scharfschießen+5
### 9. Wissen
**Lernkosten (LE):**
- normal (2 LE): Alchemie+8, Götter+8, Geschichte+8, Heraldik+8, Kriegsführung+8, Rechtskunde+8, Strategie+8
- schwer (4 LE): Gerätekunde+8, Pflanzenkunde+8, Tierkunde+8
## Mapping für Datenbank-Skills
### Skills ohne Category/Difficulty in JSON:
```
Hören -> Alltag/leicht (basierend auf Wahrnehmung)
Nachtsicht -> Alltag/leicht (basierend auf Wahrnehmung)
Riechen -> Alltag/leicht (basierend auf Wahrnehmung)
Sechster Sinn -> Alltag/leicht (basierend auf Wahrnehmung)
Sehen -> Alltag/leicht (basierend auf Wahrnehmung)
Wahrnehmung -> Alltag/leicht (implizit aus Tabellen)
Athletik -> Kampf/normal ODER Körper/schwer (Dopplung in Tabellen!)
Erste Hilfe -> Alltag/schwer
Geländelauf -> Kampf/leicht ODER Körper/leicht (Dopplung!)
Kampf in Vollrüstung -> Kampf/sehr_schwer ODER Waffen/schwer (Dopplung!)
Klettern -> Alltag/leicht ODER Halbwelt/leicht ODER Körper/leicht (Dreifachung!)
Landeskunde -> fehlt in Tabellen (sollte Wissen/normal sein)
Laufen -> Körper/schwer
Robustheit -> fehlt in Tabellen (sollte Körper/normal sein)
Sprache -> Alltag/normal
```
### Weapon Skills:
```
Armbrüste -> Waffen/leicht
Einhandschlagwaffen -> Waffen/leicht (als Hiebwaffen)
Schilde -> Waffen/normal
Spießwaffen -> Waffen/normal (als Stangenwaffen)
Stichwaffen -> Waffen/leicht (als Dolch)
Stielwurfwaffen -> Waffen/leicht (als Wurfwaffen)
Waffenloser Kampf -> fehlt in Tabellen (sollte Waffen/normal sein)
Zweihandschlagwaffen -> Waffen/normal (als Zweihandhiebwaffen)
```
## Probleme identifiziert:
1. **Doppelte/Dreifache Einträge**: Athletik, Geländelauf, Klettern erscheinen in mehreren Kategorien
2. **Fehlende Skills**: Landeskunde, Robustheit, Waffenloser Kampf sind nicht in den Lerntabellen
3. **Inkonsistente Namensgebung**: Hiebwaffen vs. Einhandschlagwaffen
4. **Wahrnehmungsunterkategorien**: Hören, Sehen, etc. sind separate Skills aber nicht in Lerntabellen
## Empfohlene Auflösung:
1. **Eindeutige Zuordnung** nach Kontext/Priorität
2. **Fehlende Skills** mit sinnvollen Defaults ergänzen
3. **Namens-Mapping** für Kompatibilität
4. **Wahrnehmungssubskills** als Alltag/leicht behandeln
@@ -1,60 +0,0 @@
# Migration von GetLearnCost zu GetSkillCost
## Zusammenfassung der Änderungen
### Entfernte Legacy-Funktionen:
1. **`GetLearnCost`** - Legacy-Endpunkt für einfache Lernkosten entfernt
2. **Route `/api/characters/:id/learn`** - Endpunkt entfernt
3. **Test `TestGetLearnCost`** - Durch `TestGetSkillCost` ersetzt
### Grund für die Migration:
**GetLearnCost** war ein Legacy-Endpunkt mit begrenzten Funktionen:
- Nur für einfache Lernkosten von Fertigkeiten und Zaubern
- Keine Verbesserungskosten
- Keine Praxispunkte-Unterstützung
- Keine Belohnungsoptionen
- Verwendete veraltete `LearnRequestStruct`
**GetSkillCost** ist der moderne, vollständige Endpunkt:
- Berechnet Kosten für Lernen und Verbessern
- Unterstützt Multi-Level-Berechnungen
- Berücksichtigt Praxispunkte (PP) zur Kostenreduktion
- Unterstützt Belohnungsoptionen (kostenloses Lernen, Gold-für-EP-Tausch)
- Moderne `SkillCostRequest` Struktur
### Status der verbleibenden Legacy-Endpunkte:
**Noch vorhanden (marked als Legacy):**
- `/api/characters/:id/improve` - `GetSkillNextLevelCosts`
- `/api/characters/:id/improve/skill` - `GetSkillAllLevelCosts`
Diese verwenden noch `LearnRequestStruct` und könnten langfristig auch durch `GetSkillCost` ersetzt werden.
### Nächste Schritte für Audit-Log Integration:
Da es derzeit keine direkten Learn/Improve Endpunkte gibt, die automatisch EP und Gold abziehen, sollten folgende Endpunkte erstellt werden:
1. **POST `/api/characters/:id/learn-skill`** - Fertigkeit lernen (mit Audit-Log)
2. **POST `/api/characters/:id/improve-skill`** - Fertigkeit verbessern (mit Audit-Log)
3. **POST `/api/characters/:id/learn-spell`** - Zauber lernen (mit Audit-Log)
4. **POST `/api/characters/:id/improve-spell`** - Zauber verbessern (mit Audit-Log)
Diese würden:
- Kosten mit `GetSkillCost` berechnen
- EP und Gold automatisch abziehen
- Audit-Log-Einträge mit `ReasonSkillLearning`, `ReasonSkillImprovement`, etc. erstellen
- Charakterdaten aktualisieren
### Audit-Log Gründe für das Lernsystem:
```go
const (
ReasonSkillLearning = "skill_learning" // Fertigkeit lernen
ReasonSkillImprovement = "skill_improvement" // Fertigkeit verbessern
ReasonSpellLearning = "spell_learning" // Zauber lernen
ReasonSpellImprovement = "spell_improvement" // Zauber verbessern
)
```
Das Audit-Log-System ist bereits vollständig implementiert und bereit für die Integration in das Lernsystem.
@@ -1,742 +0,0 @@
## Plan: PDF-Export für Charakterbögen
**Ziel:** Charakterbögen als mehrseitige PDF-Datei exportieren mit HTML-Templates und automatischem Seitenumbruch für Listen variabler Länge.
**Tech-Stack:** Go Backend (Gin), Vue 3 Frontend, GORM Datenbank, chromedp, Go html/template
**Architektur-Entscheidung:** Statt low-level PDF-Generierung nutzen wir HTML-Templates + chromedp für Browser-basiertes Rendering. Dies ermöglicht:
- Moderne CSS-Layouts (Flexbox, Grid, @page media queries)
- Einfachere Template-Wartung (HTML/CSS statt Koordinaten-basiert)
- Perfekte Schrift-/Unicode-Unterstützung
- WYSIWYG-Entwicklung (Templates im Browser testbar)
---
### Step 1: HTML-Templates vorbereiten & integrieren
**Template-Struktur:**
```
backend/
templates/
Default_A4_Quer/
page1_stats.html # Seite 1: Statistikseite
page2_play.html # Seite 2: Spielbogen
page3_spell.html # Seite 3: Zauberseite
page4_equip.html # Seite 4: Ausrüstung
shared/
export_format_a4_quer.css # Gemeinsames Stylesheet
headerimg.png # Statische Dekorationsgrafik
```
**Template-Konvertierung (HTML → Go Templates):**
- Bestehende HTML-Dateien (`char_p1_stats_a4_quer.html` etc.) dienen als Basis
- Beispieldaten werden durch Go template-Syntax ersetzt:
- `Bjarnfinnur Haberdson``{{.Character.Name}}`
- `18` (Grad) → `{{.Character.Grade}}`
- `79` (St) → `{{.Character.Attributes.St}}`
- Tabellen-Loops: `{{range .Character.Skills}}...{{end}}`
**CSS-Handling:**
- Externes Stylesheet `export_format_a4_quer.css` bleibt erhalten
- Templates linken auf Stylesheet via `<link rel="stylesheet" href="shared/export_format_a4_quer.css">`
- Beim Rendering wird CSS-Datei aus `templates/Default_A4_Quer/shared/` geladen
- Chromedp wartet auf vollständiges Laden aller Stylesheets vor PDF-Generierung
**Bild-Handling:**
- **Charakter-Icons:**
- Aus Datenbank geladen (Character.IconData als Blob)
- Als base64-kodiertes Data-URI in Template eingefügt:
- `<img src="data:image/png;base64,{{.Character.IconBase64}}">`
- **Statische Bilder (headerimg.png):**
- Aus Filesystem geladen (`templates/Default_A4_Quer/shared/headerimg.png`)
- Als base64-kodierte Data-URIs in Template eingebettet oder als relativer Pfad
**Print-Media CSS:**
- Bestehende `@page { size: A4 landscape; margin: 1cm; }` bleibt erhalten
- Chromedp nutzt print media emulation für korrektes Seiten-Layout
---
### Step 2: Chromedp-Integration aufbauen
**Package-Struktur:**
```
backend/
pdfrender/
chromedp.go # Chromedp-Wrapper
templates.go # Template-Loader & Renderer
pagination.go # Listen-Pagination-Logik
viewmodel.go # Domain → Template Mapping
```
**Chromedp-Wrapper (`chromedp.go`):**
- Funktion `RenderHTMLToPDF(htmlPages []string) ([]byte, error)`
- Input: Array von gerenderten HTML-Strings (eine pro Seite)
- Output: PDF als byte array
- Chromedp-Konfiguration:
- Headless-Modus
- Disable-GPU (für Server-Umgebung)
- Print-to-PDF mit Optionen:
- `Landscape: true` für A4 quer
- `PrintBackground: true` für CSS-Hintergründe
- `PreferCSSPageSize: true` für `@page`-Regeln
- Timeout-Handling (z.B. 30s max)
**Template-Loader (`templates.go`):**
- Funktion `LoadTemplates() (*template.Template, error)`
- Lädt alle `.html`-Dateien aus `templates/Default_A4_Quer/`
- Parse mit Go's `html/template`
- Validierung: Alle erwarteten Templates vorhanden
- Caching: Templates einmalig beim Start laden
- Funktion `RenderTemplate(name string, data interface{}) (string, error)`
- Rendert einzelnes Template mit Daten
- Fügt base64-kodierte statische Bilder ein
- Error-Handling für fehlende Template-Variablen
**Helper-Funktionen:**
- `ImageToBase64DataURI(imageData []byte, mimeType string) string`
- Konvertiert Bild-Bytes zu Data-URI
- `LoadStaticImage(path string) (string, error)`
- Lädt statisches Bild und konvertiert zu base64
---
### Step 3: Pagination-Logik implementieren
**Problem:** Listen (Skills, Weapons, Spells, Equipment) können Template-Kapazität überschreiten.
**Lösung:** Blockweise Pagination mit Fortsetzungsseiten
**Konzept:**
- Jede Liste hat eine `maxRows` pro Block/Seite
- Beispiel Skills Seite 1:
- Spalte 3: max ~32 Zeilen
- Spalte 4: max ~32 Zeilen
- Total Seite 1: ~64 Skills
- Bei Überschreitung: Fortsetzungsseite mit gleicher Struktur
**Implementation (`pagination.go`):**
```go
type PageData struct {
Character CharacterData
Skills []Skill // nur die Skills für diese Seite
Weapons []Weapon // nur die Weapons für diese Seite
// ... weitere Listen
}
type PaginationConfig struct {
Page1SkillsMax int // 64 (2x32)
Page2SkillsMax int // je nach Layout
Page2WeaponsMax int
Page3SpellsMax int
// ... für alle Listen
}
func PaginateCharacterData(char Character, config PaginationConfig) []PageData
```
**Pagination-Regeln (basierend auf Templates):**
- **Seite 1 (Stats):**
- Skills: 2 Spalten à ~32 Zeilen = 64 Skills max
- Overflow geht zu Seite 2
- **Seite 2 (Play):**
- Skills (gelernte): Linke Spalte, ~24 Zeilen
- Skills (ungelernte): Kleine Tabelle, ~15 Zeilen
- Skills (Sprachen/etc.): Weitere Tabelle, ~11 Zeilen
- Weapons: Große Tabelle rechts, ~30 Zeilen
- Bei Overflow: Seite 2.1 mit gleicher Struktur
- **Seite 3 (Spells):**
- Zauber: 2 Spalten à ~12 Zeilen = 24 Zauber
- Magische Gegenstände: ~5 Zeilen
- Bei Overflow: Seite 3.1
- **Seite 4 (Equipment):**
- Ausrüstungs-Sektionen: Variable Zeilenanzahl pro Sektion
- Bei Overflow: Seite 4.1
**Fortsetzungsseiten:**
- Gleiche Template-Struktur wie Hauptseite
- Header zeigt "Fortsetzung" an (optional)
- Datum bleibt gleich
---
### Step 4: View-Model Mapping
**Domain → Template Transformation (`viewmodel.go`):**
```go
type CharacterSheetViewModel struct {
// Basis-Daten
Character struct {
Name string
Player string
Type string
Grade int
Birthdate string
Age int
Height int
Weight int
IconBase64 string // base64-kodiertes Bild
// ...
}
// Attribute
Attributes struct {
St, Gs, Gw, Ko, In, Zt, Au, PA, Wk, B int
}
// Abgeleitete Werte
DerivedValues struct {
LP, AP, GG, SG int
Abwehr, Resistenz string
Zaubern string
// Boni
AusdauerBonus int
SchadenBonus int
AngriffBonus int
// ...
}
// Listen (bereits paginiert für diese Seite)
Skills []SkillViewModel
Weapons []WeaponViewModel
Spells []SpellViewModel
MagicItems []MagicItemViewModel
Equipment []EquipmentViewModel
GameResults []GameResultViewModel
// Meta
Date string
PageNumber int
IsContinuation bool
}
```
**Mapping-Funktion:**
```go
func MapCharacterToViewModel(
char *models.Character,
pageType string,
pageNumber int,
) (*CharacterSheetViewModel, error)
```
**Spezial-Handling:**
- **Fertigkeiten gruppieren:**
- Seite 1: Alle Skills mit Grundwerten (EW aus Grundfertigkeit)
- Seite 2: Nur gelernte Skills mit Spielwerten (PP, aktuelle EW)
- Ungelernte Skills mit Modifikatoren
- **Zauber formatieren:**
- Mehrzeilige Zellen: AP/Prozess als zwei Zeilen in einer Zelle
- `<hr>` für visuelle Trennung in HTML
- **Ausrüstung nach Container:**
- "Am Körper getragen"
- "Becher, Holz" (Container 1)
- "Wasserschlauch" (Container 2)
- etc.
---
### Step 5: API-Endpoint implementieren
**Endpoint:**
```
GET /api/characters/{id}/pdf?format=a4_quer
```
**Handler-Flow:**
1. **Authentifizierung:** User-ID aus JWT/Session
2. **Autorisierung:** Prüfen ob User Zugriff auf Character hat
3. **Daten laden:**
```go
char := LoadCharacterWithRelations(id) // Preload Skills, Spells, Equipment, etc.
```
4. **Icon vorbereiten:**
```go
if char.IconData != nil {
iconBase64 := base64.StdEncoding.EncodeToString(char.IconData)
char.IconBase64 = "data:image/png;base64," + iconBase64
}
```
5. **Pagination:**
```go
pages := PaginateCharacterData(char, GetPaginationConfig())
```
6. **Template-Rendering:**
```go
htmlPages := make([]string, 0, 4)
for i, pageData := range pages {
var templateName string
switch pageData.Type {
case "stats": templateName = "page1_stats.html"
case "play": templateName = "page2_play.html"
case "spell": templateName = "page3_spell.html"
case "equip": templateName = "page4_equip.html"
}
html := RenderTemplate(templateName, pageData)
htmlPages = append(htmlPages, html)
}
```
7. **PDF-Generierung:**
```go
pdfBytes, err := RenderHTMLToPDF(htmlPages)
```
8. **Response:**
```go
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Disposition",
fmt.Sprintf("attachment; filename=\"%s_charakterbogen.pdf\"",
sanitizeFilename(char.Name)))
w.Write(pdfBytes)
```
**Error-Handling:**
- Character nicht gefunden: 404
- Keine Berechtigung: 403
- Template-Fehler: 500 mit Logging
- Chromedp-Fehler: 500 mit Logging
- Timeout: 504
**Route registrieren:**
- In `backend/character/routes.go`:
```go
charGroup.GET("/:id/pdf", CharacterPDFHandler)
```
---
### Step 6: Frontend-Integration
**Vue Component: Character Detail View**
```vue
<template>
<div class="character-actions">
<button @click="downloadPDF" :disabled="downloading">
<i class="icon-pdf"></i>
{{ downloading ? 'Generiere PDF...' : 'Charakterbogen (PDF)' }}
</button>
</div>
</template>
<script setup>
const downloading = ref(false)
async function downloadPDF() {
downloading.value = true
try {
const response = await fetch(
`/api/characters/${characterId}/pdf?format=a4_quer`,
{ headers: { 'Authorization': `Bearer ${token}` } }
)
if (!response.ok) throw new Error('PDF generation failed')
const blob = await response.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${characterName}_charakterbogen.pdf`
a.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// Error-Toast anzeigen
} finally {
downloading.value = false
}
}
</script>
```
**Format-Auswahl (optional, später):**
```vue
<select v-model="selectedFormat">
<option value="a4_quer">A4 Querformat</option>
<option value="a4_hoch">A4 Hochformat</option>
</select>
```
---
### Step 7: Deployment & Dependencies
**Go Dependencies:**
```bash
go get github.com/chromedp/chromedp
```
**System Requirements:**
- Chrome/Chromium installiert auf Server
- Ubuntu: `apt-get install chromium-browser`
- Alpine (Docker): `apk add chromium`
- Ausreichend Speicher für Chrome-Prozesse (~100-200MB pro Instanz)
**Docker Integration:**
```dockerfile
FROM golang:1.21-alpine
# Chrome installieren
RUN apk add --no-cache chromium
# Fonts für PDF (optional, verbessert Rendering)
RUN apk add --no-cache font-dejavu
# ... Rest des Dockerfiles
```
**Environment Variables:**
```bash
CHROME_BIN=/usr/bin/chromium-browser
PDF_TIMEOUT=30s
```
---
### LEGACY REFERENCE (old approach - for reference only):
The following sections describe the original plan using YAML templates and coordinate-based rendering. This approach has been replaced by the HTML template + chromedp approach described above.
---
### OLD Step 3: API-Endpoint implementieren
**Details:**
- **Endpoint-Design:**
- HTTP-GET:
- `GET /api/characters/{id}/pdf?templateId=a4_quer_char_sheet_v1`
- Antwort:
- `Content-Type: application/pdf`
- `Content-Disposition: attachment; filename="<charname>_charbogen.pdf"`
- **Handler-Ablauf:**
1. Authentifizierung/Autorisierung prüfen.
2. Charakter inkl. aller benötigten Relationen laden:
- Basis-/Statistikwerte,
- Spielwerte,
- Fertigkeiten,
- Waffen,
- Zauber,
- magische Gegenstände,
- Ausrüstung,
- Spielergebnisse.
3. Template wählen:
- `templateId` aus Query (`template_a4_quer.yaml` als Default).
- Template aus YAML in interne Structs laden/validieren.
4. Domain-Daten in ein View-Model überführen, das zu den Template-Bindings passt (z.B. `character.attributes.st.base`, Zauberfelder `ap`, `prozess`, `zd`, `rw`, `wb`, `wd` usw.).
5. Section-Renderer + Pagination-Engine aufrufen:
- `pages := RenderCharacterSheet(template, viewModel)`.
6. PDF-Dokument mit dem `pdf`-Package erzeugen:
- Für jede Seite:
- Seite hinzufügen,
- alle Blöcke mit ihren Koordinaten und Daten rendern.
7. PDF an den Client streamen.
- **Fehlerfälle:**
- Charakter nicht gefunden → 404.
- Template nicht gefunden/ungültig → 500, Logging, ggf. Fallback auf Default-Template.
- PDF-Erzeugung fehlgeschlagen → 500, Logging.
**Route registrieren:**
- In der bestehenden Gin-Router-Konfiguration:
- z.B. in `cmd/backend/main.go`:
- Gruppe `/api/characters` → `GET("/:id/pdf", CharacterPDFHandler)`.
---
### Step 4: Listen-Rendering mit Pagination
**Details:**
- **Datenaufbereitung:**
- Aus dem Domänenmodell werden pro Charakter logisch getrennte Listen erzeugt:
- `statsSkills` (Fertigkeiten-Grundwerte für Seite 1),
- `playSkills` (Fertigkeiten-Spielwerte für Seite 2/2.x),
- `weapons`,
- `spells`,
- `magicItems`,
- `equipmentOnBody`, `equipmentContainer1`, `equipmentContainer2`, …,
- `gameResults`.
- Diese Listen sind unabhängig von Seiten und Blöcken.
### Step 5: Frontend-Integration
**Details:**
- **Download im Charakter-UI (Vue 3):**
- In der Detailansicht eines Charakters:
- Button „Charakterbogen (PDF)“.
- Klick:
- HTTP-Request auf `GET /api/characters/{id}/pdf?templateId=a4_quer_char_sheet_v1`.
- Browser-Auslösung eines Downloads (`Content-Disposition`).
- **Template-Auswahl (optional, später):**
- Backend-Endpunkt `GET /api/pdf-templates`:
- Liefert Liste der verfügbaren Templates (`id`, `name`, Ausrichtung).
- Frontend:
- Dropdown neben Download-Button (z.B. „A4 quer“, „A4 hoch“).
- Ausgewählte `templateId` wird in den PDF-Request aufgenommen.
- **Fehler-Handling im UI:**
- Bei HTTP-Fehlern:
- Kurze Meldung („PDF-Export fehlgeschlagen“).
- Kein Client-seitiges Layout:
- Frontend kennt nur `templateId`, die komplette Renderlogik liegt im Backend.
---
### Step 6: Template-Konfiguration
**Font-Konfiguration:**
- Zentral im `pdf`-Package:
- Registrierung der verwendeten Fonts (liberation sans als Arial Replacement).
- Sicherstellen, dass:
- Umlaute (ä, ö, ü, ß) und Sonderzeichen korrekt dargestellt werden,
- gleiche Fontkonfiguration für alle Template seitendamit die Zeilenhöhe konstant bleibt
---
## Implementierungs-Reihenfolge (New chromedp Approach)
1. **HTML Templates vorbereiten (Step 1):**
- HTML-Templates nach `backend/templates/Default_A4_Quer/` kopieren
- CSS nach `backend/templates/Default_A4_Quer/shared/` kopieren
- Statische Bilder nach `backend/templates/Default_A4_Quer/shared/images/` kopieren
- Beispieldaten durch Go-Template-Syntax ersetzen (z.B. `{{.Character.Name}}`)
- Base64-Konvertierung für Bilder testen
2. **Chromedp Integration (Step 2):**
- `backend/pdfrender/chromedp.go` implementieren:
- `RenderHTMLToPDF()` mit Context-Management
- `PrintToPDF()` mit A4-Landscape-Settings
- `backend/pdfrender/templates.go` implementieren:
- `LoadTemplates()` mit Parse-Fehlerbehandlung
- `RenderTemplate()` mit ViewModel-Binding
- CSS/Bild-Einbettung testen
- Unit-Test: Einfaches HTML → PDF generieren
3. **Pagination-Logik (Step 3):**
- `backend/pdfrender/pagination.go` implementieren:
- `PaginationConfig` struct mit max rows
- `PaginateCharacterData()` mit Liste-Splitting
- Regeln implementieren (basierend auf HTML-Template-Kapazitäten):
- Seite 1: Skills max 64
- Seite 2: Weapons max 30, Skills various
- Seite 3: Spells max 24, Magic Items max 5
- Seite 4: Equipment nach Container
- Unit-Tests mit Overflow-Szenarien
4. **View-Model-Mapping (Step 4):**
- `backend/pdfrender/viewmodel.go` implementieren:
- `CharacterSheetViewModel` struct
- `MapCharacterToViewModel()` mit Domain → Template Transformation
- Spezialbehandlung:
- Skills gruppieren (Grundwerte/Spielwerte/ungelernt)
- Zauber formatieren (AP/Prozess mehrzeilig)
- Ausrüstung nach Container gruppieren
- Unit-Tests mit Mock-Character-Daten
5. **API-Endpoint (Step 5):**
- Handler `CharacterPDFHandler` in `backend/character/` implementieren:
- Auth/Authz
- Character + Relations laden
- Icon als base64 vorbereiten
- Pagination aufrufen
- Templates rendern
- Chromedp PDF-Generierung
- Stream als PDF
- Route registrieren in `backend/character/routes.go`
- Error-Handling (404/403/500/504)
6. **Frontend-Integration (Step 6):**
- Vue-Component anpassen:
- Download-Button mit Loading-State
- Fetch-Call zu `/api/characters/:id/pdf`
- Blob-Download-Handling
- Error-Toast bei Fehlern
- Optional: Format-Auswahl (A4-Quer/-Hoch)
7. **Deployment (Step 7):**
- Dockerfile anpassen:
- Chromium installieren
- Fonts installieren (dejavu)
- Environment-Variables konfigurieren:
- `CHROME_BIN`, `PDF_TIMEOUT`
- Docker-Compose testen
8. **Feintuning & Tests:**
- Layout in verschiedenen PDF-Readern testen
- `maxRows` justieren bei Bedarf
- Performance-Tests (Generierungs-Dauer messen)
- Testing-Checkliste durchgehen
---
## Further Considerations
### Mehrsprachigkeit
- **Aktuell:** Templates fest auf Deutsch
- **Später:** Feldnamen aus i18n-Config laden
- **KISS:** Start mit Deutsch, i18n bei Bedarf nachrüsten
### Template-Customization
- **Option A (komplex):** Benutzer eigene Templates hochladen lassen
- **Option B (KISS):** 23 vordefinierte Templates fest im Code
- **Empfehlung:** Option B für MVP, Option A als Feature später
### Caching
- **On-the-fly (Start):** PDF bei jedem Request neu generieren
- **Mit Cache (später):** PDF cachen, Invalidierung bei Character-Update
- **KISS:** Start ohne Cache, Performance-Optimierung bei Bedarf
### Fehlerbehandlung
- Charakter nicht gefunden → 404
- PDF-Generierung fehlgeschlagen → 500 mit Fehlerlog
- Ungültiges Template → Fallback auf Default-Template
### Logging
- PDF-Export-Anfragen loggen (Character-ID, Template, User)
- Generierungs-Dauer messen für Performance-Monitoring
- Fehler detailliert loggen für Debugging
---
## Testing-Checkliste (Chromedp Approach)
### Unit-Tests
- [ ] **Template Loading:**
- [ ] Alle 4 Templates laden ohne Fehler
- [ ] CSS-Datei korrekt eingebettet
- [ ] Statische Bilder als base64 geladen
- [ ] Fehlerbehandlung bei fehlendem Template
- [ ] **Pagination:**
- [ ] Skills: < 64 → 1 Seite, > 64 → 2 Seiten
- [ ] Weapons: < 30 → 1 Block, > 30 → Continuation Page
- [ ] Spells: < 24 → 1 Seite, > 24 → 2 Seiten
- [ ] Equipment: Multiple Container richtig gruppiert
- [ ] **View-Model Mapping:**
- [ ] Character-Basics korrekt gemappt
- [ ] Attributes alle vorhanden
- [ ] Derived Values berechnet
- [ ] Skills nach Kategorie gruppiert
- [ ] Zauber mit mehrzeiligen Feldern (AP/Prozess)
- [ ] Equipment nach Container sortiert
- [ ] Character-Icon als base64 Data-URI
- [ ] **Chromedp Integration:**
- [ ] Einfaches HTML → PDF erfolgreich
- [ ] A4-Landscape korrekt (297mm x 210mm)
- [ ] Print-CSS-Regeln angewendet (@page)
- [ ] Timeout-Handling funktioniert
### Integration-Tests
- [ ] **End-to-End PDF-Generierung:**
- [ ] Charakter mit minimalen Daten (leere Listen)
- [ ] Charakter mit wenigen Skills (< 64) → 1 Seite
- [ ] Charakter mit vielen Skills (> 64) → Multi-Page mit Continuation
- [ ] Charakter mit allen Listen gefüllt → 4+ Seiten
- [ ] Character mit DB-Icon → Icon korrekt im PDF
- [ ] Character ohne Icon → Platzhalter oder leer
### API-Tests
- [ ] **Endpoint-Funktionalität:**
- [ ] `GET /api/characters/:id/pdf` → 200 + PDF
- [ ] Character nicht gefunden → 404
- [ ] Nicht authentifiziert → 401
- [ ] Keine Berechtigung (fremder Character) → 403
- [ ] Timeout/Chromedp-Fehler → 500 mit Log
- [ ] Content-Type `application/pdf` korrekt
- [ ] Content-Disposition mit Filename korrekt
- [ ] Filename sanitized (keine Sonderzeichen-Probleme)
### Frontend-Tests
- [ ] **Download-Funktion:**
- [ ] Button klickbar
- [ ] Loading-State während Generierung
- [ ] Download startet automatisch
- [ ] Filename korrekt (z.B. `Gandalf_charakterbogen.pdf`)
- [ ] Error-Toast bei 404/500
### PDF-Qualitäts-Tests
- [ ] **Rendering-Qualität:**
- [ ] PDF öffnet in Adobe Reader
- [ ] PDF öffnet in Chrome/Firefox PDF-Viewer
- [ ] PDF öffnet in Evince (Linux)
- [ ] Layout identisch zu HTML-Preview in Browser
- [ ] Umlaute korrekt (ä, ö, ü, ß)
- [ ] Tabellen-Borders korrekt
- [ ] Schriftgrößen lesbar
- [ ] Keine abgeschnittenen Inhalte
- [ ] Page-Breaks an richtigen Stellen
### Performance-Tests
- [ ] **Generierungs-Dauer:**
- [ ] 1-seitiger Character: < 2 Sekunden
- [ ] 4-seitiger Character: < 5 Sekunden
- [ ] 10+ Seiten: < 10 Sekunden
- [ ] Memory-Leak-Test (100 PDFs generieren)
### Deployment-Tests
- [ ] **Docker-Integration:**
- [ ] Chromium in Container verfügbar
- [ ] Fonts installiert (dejavu)
- [ ] Environment-Variables gesetzt
- [ ] PDF-Generierung in Container funktioniert
- [ ] Keine Permission-Probleme mit Chrome-Binary
---
## Dateien zum Erstellen/Ändern (Chromedp Approach)
### Zu erstellende Dateien:
**Backend:**
- `backend/pdfrender/chromedp.go` (Chromedp-Wrapper, PDF-Generierung)
- `backend/pdfrender/templates.go` (Template-Loader, Renderer)
- `backend/pdfrender/pagination.go` (Pagination-Logik, Config)
- `backend/pdfrender/viewmodel.go` (ViewModel-Structs, Mapping-Funktionen)
- `backend/character/pdf_handler.go` (API-Endpoint-Handler)
**Templates:**
- `backend/templates/Default_A4_Quer/page1_stats.html` (Seite 1: Stats)
- `backend/templates/Default_A4_Quer/page2_play.html` (Seite 2: Play)
- `backend/templates/Default_A4_Quer/page3_spell.html` (Seite 3: Spells)
- `backend/templates/Default_A4_Quer/page4_equip.html` (Seite 4: Equipment)
- `backend/templates/Default_A4_Quer/shared/export_format_a4_quer.css` (Shared CSS)
- `backend/templates/Default_A4_Quer/shared/images/` (Statische Bilder)
**Tests:**
- `backend/pdfrender/chromedp_test.go`
- `backend/pdfrender/pagination_test.go`
- `backend/pdfrender/viewmodel_test.go`
- `backend/character/pdf_handler_test.go`
### Zu ändernde Dateien:
**Backend:**
- `backend/character/routes.go` (Route für PDF-Endpoint hinzufügen)
- `backend/go.mod` (chromedp-Dependency)
**Frontend:**
- `frontend/src/views/CharacterDetail.vue` (oder ähnlich - Download-Button)
- `frontend/src/services/characterService.js` (API-Call für PDF)
**Docker:**
- `docker/Dockerfile.backend` (Chromium + Fonts installieren)
- `docker/docker-compose.yml` (Environment-Variables)
**Dokumentation:**
- `backend/doc/plan-pdfExportCharacterSheets.md` (dieses Dokument)
- `README.md` (PDF-Export-Feature dokumentieren)
-199
View File
@@ -1,199 +0,0 @@
# Belohnungssystem API
Das Belohnungssystem ermöglicht es, verschiedene Arten von Belohnungen auf Lern- und Verbesserungskosten anzuwenden.
## Belohnungstypen
### 1. `free_learning` - Kostenlose Fertigkeiten
- **Anwendung**: Beim Lernen neuer Fertigkeiten (`action: "learn", type: "skill"`)
- **Effekt**: Die Geldkosten für das Lernen der Fertigkeit entfallen (Gold = 0)
- **EP/LE**: Bleiben unverändert
**Beispiel:**
```json
{
"name": "Stichwaffen",
"type": "skill",
"action": "learn",
"reward": {
"type": "free_learning"
}
}
```
### 2. `free_spell_learning` - Kostenlose Zauber
- **Anwendung**: Beim Lernen neuer Zauber (`action: "learn", type: "spell"`)
- **Effekt**: Die Lehrzeit für das Lernen des Zaubers entfällt (LE = 0)
- **EP/Gold**: Bleiben unverändert
**Beispiel:**
```json
{
"name": "Licht",
"type": "spell",
"action": "learn",
"reward": {
"type": "free_spell_learning"
}
}
```
### 3. `half_ep_improvement` - Halbe EP für Verbesserungen
- **Anwendung**: Bei Fertigkeitsverbesserungen (`action: "improve"`)
- **Effekt**: Die EP-Kosten werden halbiert
- **LE/Gold**: Bleiben unverändert
**Beispiel:**
```json
{
"name": "Stichwaffen",
"type": "skill",
"action": "improve",
"current_level": 8,
"reward": {
"type": "half_ep_improvement"
}
}
```
### 4. `gold_for_ep` - Gold statt EP verwenden
- **Anwendung**: Bei allen Aktionen mit EP-Kosten
- **Effekt**: EP werden durch Gold ersetzt (Wechselkurs: 10 GS = 1 EP)
- **Parameter**:
- `use_gold_for_ep`: Muss `true` sein
- `max_gold_ep`: Maximale EP die durch Gold ersetzt werden (optional, Standard: Hälfte der EP-Kosten)
**Beispiel:**
```json
{
"name": "Schwert",
"type": "skill",
"action": "improve",
"current_level": 10,
"reward": {
"type": "gold_for_ep",
"use_gold_for_ep": true,
"max_gold_ep": 5
}
}
```
## Kombination mit Praxispunkten
Belohnungen können mit Praxispunkten kombiniert werden:
```json
{
"name": "Stichwaffen",
"type": "skill",
"action": "improve",
"current_level": 12,
"use_pp": 2,
"reward": {
"type": "half_ep_improvement"
}
}
```
**Anwendungsreihenfolge:**
1. Zunächst werden Belohnungen angewendet
2. Dann werden Praxispunkte angewendet
## Response-Struktur
Die API-Response enthält zusätzliche Informationen über die angewendeten Belohnungen:
```json
{
"ep": 15,
"le": 0,
"money": 50,
"skill_name": "Stichwaffen",
"action": "improve",
"can_afford": true,
// Belohnungsdetails
"reward_applied": "half_ep_improvement",
"original_cost_struct": {
"ep": 30,
"le": 0,
"money": 50
},
"savings": {
"ep": 15,
"le": 0,
"money": 0
},
"gold_used_for_ep": 0,
// Praxispunkt-Details
"pp_used": 2,
"pp_available": 5,
"pp_reduction": 2,
"original_cost": 30,
"final_cost": 13
}
```
## Felderbeschreibung
- `reward_applied`: Art der angewendeten Belohnung
- `original_cost_struct`: Ursprüngliche Kosten ohne Belohnung
- `savings`: Ersparnisse durch die Belohnung
- `gold_used_for_ep`: Anzahl EP die durch Gold ersetzt wurden
- `original_cost`: EP-Kosten vor PP-Reduktion
- `final_cost`: Finale EP-Kosten nach allen Reduktionen
## Curl-Beispiele
**Wichtig**: Alle API-Aufrufe benötigen einen Authorization Header.
### Kostenlose Fertigkeit lernen
```bash
curl -X POST http://localhost:8180/api/characters/20/skill-cost \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 0fec2f7.1:2f9ecb4206b1fc311d91b5530" \
-d '{
"name": "Stichwaffen",
"type": "skill",
"action": "learn",
"reward": {
"type": "free_learning"
}
}'
```
### Halbe EP für Verbesserung mit Praxispunkten
```bash
curl -X POST http://localhost:8180/api/characters/20/skill-cost \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 0fec2f7.1:2f9ecb4206b1fc311d91b5530" \
-d '{
"name": "Stichwaffen",
"type": "skill",
"action": "improve",
"current_level": 12,
"use_pp": 2,
"reward": {
"type": "half_ep_improvement"
}
}'
```
### Gold statt EP verwenden
```bash
curl -X POST http://localhost:8180/api/characters/20/skill-cost \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 0fec2f7.1:2f9ecb4206b1fc311d91b5530" \
-d '{
"name": "Schwert",
"type": "skill",
"action": "improve",
"current_level": 10,
"reward": {
"type": "gold_for_ep",
"use_gold_for_ep": true,
"max_gold_ep": 5
}
}'
```
-154
View File
@@ -1,154 +0,0 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"bamort/character"
)
func main() {
fmt.Println("=== Belohnungssystem API Tests ===\n")
// Warten bis Server gestartet ist
time.Sleep(2 * time.Second)
// Base URL für lokale API
baseURL := "http://localhost:8080/api/characters/1/skill-cost"
fmt.Println("1. Kostenlose Fertigkeit lernen (free_learning)")
testRequest1 := character.SkillCostRequest{
Name: "Dolch",
Type: "skill",
Action: "learn",
Reward: &character.RewardOptions{
Type: "free_learning",
},
}
testRewardRequest(baseURL, testRequest1)
fmt.Println("\n2. Kostenloser Zauber lernen (free_spell_learning)")
testRequest2 := character.SkillCostRequest{
Name: "Licht",
Type: "spell",
Action: "learn",
Reward: &character.RewardOptions{
Type: "free_spell_learning",
},
}
testRewardRequest(baseURL, testRequest2)
fmt.Println("\n3. Halbe EP für Verbesserung (half_ep_improvement)")
testRequest3 := character.SkillCostRequest{
Name: "Dolch",
Type: "skill",
Action: "improve",
CurrentLevel: 8,
Reward: &character.RewardOptions{
Type: "half_ep_improvement",
},
}
testRewardRequest(baseURL, testRequest3)
fmt.Println("\n4. Gold statt EP verwenden (gold_for_ep)")
testRequest4 := character.SkillCostRequest{
Name: "Schwert",
Type: "skill",
Action: "improve",
CurrentLevel: 10,
Reward: &character.RewardOptions{
Type: "gold_for_ep",
UseGoldForEP: true,
MaxGoldEP: 0, // Automatisch die Hälfte
},
}
testRewardRequest(baseURL, testRequest4)
fmt.Println("\n5. Kombination: Belohnung + Praxispunkte")
testRequest5 := character.SkillCostRequest{
Name: "Dolch",
Type: "skill",
Action: "improve",
CurrentLevel: 12,
UsePP: 2,
Reward: &character.RewardOptions{
Type: "half_ep_improvement",
},
}
testRewardRequest(baseURL, testRequest5)
}
func testRewardRequest(url string, request character.SkillCostRequest) {
// Convert request to JSON
requestBody, err := json.Marshal(request)
if err != nil {
log.Printf("Error marshaling request: %v\n", err)
return
}
// Create HTTP request
req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
if err != nil {
log.Printf("Error creating request: %v\n", err)
return
}
req.Header.Set("Content-Type", "application/json")
// Execute request
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
log.Printf("Error making request: %v\n", err)
return
}
defer resp.Body.Close()
// Check status code
if resp.StatusCode != http.StatusOK {
log.Printf("API returned status %d\n", resp.StatusCode)
return
}
// Parse response
var response character.SkillCostResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
log.Printf("Error parsing response: %v\n", err)
return
}
// Print results
fmt.Printf("Fertigkeit: %s (%s)\n", response.SkillName, response.Action)
if response.RewardApplied != "" {
fmt.Printf("Belohnung: %s\n", response.RewardApplied)
}
if response.OriginalCostStruct != nil {
fmt.Printf("Ursprüngliche Kosten: %d EP, %d LE, %d GS\n",
response.OriginalCostStruct.Ep,
response.OriginalCostStruct.LE,
response.OriginalCostStruct.Money)
}
fmt.Printf("Finale Kosten: %d EP, %d LE, %d GS\n",
response.Ep, response.LE, response.Money)
if response.Savings != nil && (response.Savings.Ep > 0 || response.Savings.LE > 0 || response.Savings.Money > 0) {
fmt.Printf("Ersparnisse: %d EP, %d LE, %d GS\n",
response.Savings.Ep, response.Savings.LE, response.Savings.Money)
}
if response.GoldUsedForEP > 0 {
fmt.Printf("Gold für EP verwendet: %d EP = %d GS\n",
response.GoldUsedForEP, response.GoldUsedForEP*10)
}
if response.PPUsed > 0 {
fmt.Printf("Praxispunkte verwendet: %d\n", response.PPUsed)
}
fmt.Printf("Kann sich leisten: %t\n", response.CanAfford)
}
-52
View File
@@ -1,52 +0,0 @@
# Skill-Kategorie und Schwierigkeit Konsolidierung
## Problemanalyse
1. Skills in der JSON-Datei haben keine Category/Difficulty
2. Fallback-Funktionen sind inkonsistent mit den Lerntabellen
3. Manche Kategorien/Schwierigkeiten existieren nicht in den BaseLearnCost
## Konsolidierte Skill-Definitionen (basierend auf Midgard KOD5)
### Alltag-Fertigkeiten
- **Erste Hilfe**: normal (1 LE)
- **Wahrnehmung**: leicht (1 LE)
- **Hören**: leicht (1 LE)
- **Sehen**: leicht (1 LE)
- **Nachtsicht**: leicht (1 LE)
- **Riechen**: leicht (1 LE)
- **Sechster Sinn**: leicht (1 LE)
### Freiland-Fertigkeiten
- **Geländelauf**: normal (2 LE)
### Körper-Fertigkeiten
- **Athletik**: normal (1 LE)
- **Klettern**: schwer (2 LE)
- **Laufen**: normal (1 LE)
- **Robustheit**: normal (1 LE)
### Kampf-Fertigkeiten
- **Kampf in Vollrüstung**: schwer (10 LE)
### Wissen-Fertigkeiten
- **Landeskunde**: normal (2 LE)
- **Sprache**: normal (2 LE)
### Waffen-Fertigkeiten
- **Stichwaffen**: normal (2 LE)
- **Einhandschlagwaffen**: normal (2 LE)
- **Zweihandschlagwaffen**: normal (2 LE)
- **Spießwaffen**: normal (2 LE)
- **Waffenloser Kampf**: normal (2 LE)
- **Armbrüste**: normal (2 LE)
- **Stielwurfwaffen**: normal (2 LE)
- **Schilde**: leicht (1 LE)
## Fehlende Kategorien in BaseLearnCost
- Wissen: leicht fehlt (sollte 1 LE sein)
- Waffen: sehr_schwer fehlt (wird normalerweise nicht verwendet)
## Empfohlene Korrekturen
1. BaseLearnCost um fehlende Einträge erweitern
2. Skill-Mapping korrigieren
3. Konsistente Kategoriezuordnung
-887
View File
@@ -1,887 +0,0 @@
template:
Id: a4_quer_char_sheet_v1
name: "A4 Querformat Charakterbogen V1"
layout:
pageSizeMm:
width: 297
height: 210
marginsMm:
top: 6
bottom: 6
left: 6
right: 6
columns:
count: 4
equalWidth: true
gutterMm: 4
font:
name: "DejaVu Sans"
sizePt:
default: 16
small: 14
large: 18
lineHeightFactor:
default: 1.2
small: 1.1
large: 1.3
spacingMm:
vertical: 0.5
horizontal: 0.5
ptToMm: 0.3528
boxTypes:
- id: 01
name: header
type: labelValue
fontSize: default
style: {}
- id: 02
name: stats
type: labelValue
fontSize: large
style:
boxed: true
- id: 03
name: table
type: table
fontSize: default
style:
nobr: true
shorten: true
pages:
# ---------------------------------------------------------
# Seite 1 Statistikseite
# ---------------------------------------------------------
- pageType: page1_stats
blocks:
- blockId: 1
colStart: 1
colSpan: 4
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- label: "Name des Charakters"
binding: "" # TODO
- label: "Name des Spielers"
binding: "" # TODO
- blockId: 2
colStart: 1
colSpan: 1
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- label: "Typ, Grad"
binding: "" # TODO (z.B. character.type, character.level)
- label: "Spezialisierung"
binding: "" # TODO
- blockId: 3
colStart: 1
colSpan: 1
boxTypeId: 02 # stats, Box Label:Wert
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- # Zeile 1
- label: "St"
binding: "character.attributes.st.base" # Stärke
- label: "Gs"
binding: "character.attributes.gs.base" # Geschicklichkeit
- label: "Gw"
binding: "character.attributes.gw.base" # Gewandheit
- # Zeile 2
- label: "Ko"
binding: "character.attributes.ko.base" # Konstitution
- label: "In"
binding: "character.attributes.in.base" # Intelligenz
- label: "Zt"
binding: "character.attributes.zt.base" # Zaubertalent
- # Zeile 3
- label: "Au"
binding: "character.attributes.au.base" # Ausdauer
- label: "pA"
binding: "character.attributes.pa.base" # persönliche Ausstrahlung
- label: "Wk"
binding: "character.attributes.wk.base" # Willenskraft
- # Zeile 4
- label: "B"
binding: "character.attributes.b.base" # Bewegungsweite / Basis-Bewegung
- label: "Raufen"
binding: "character.skills.raufen.base" # Grundwert Raufen (als Fertigkeit)
- blockId: 4
colStart: 1
colSpan: 1
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- label: "persönliche Boni für:"
binding: "" # rein beschreibend
- label: "Ausdauer"
binding: "" # TODO
- label: "Schaden"
binding: "" # TODO
- label: "Angriff"
binding: "" # TODO
- label: "Abwehr"
binding: "" # TODO
- label: "Resistenz"
binding: "" # TODO
- label: "Zaubern"
binding: "" # TODO
- blockId: 5
colStart: 1
colSpan: 2
boxTypeId: 02
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- label: "LP-Max"
binding: "" # TODO
- label: "AP-Max"
binding: "" # TODO
- label: "GG"
binding: "" # TODO
- label: "SG"
binding: "" # TODO
- blockId: 6
colStart: 1
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: fixed
type: table
table:
columns:
- key: "date"
label: "Datum"
- key: "es"
label: "ES"
- key: "ep"
label: "EP"
- key: "geld"
label: "Geld"
- key: "c1"
label: ""
- key: "c2"
label: ""
- key: "c3"
label: ""
- key: "c4"
label: ""
- key: "c5"
label: ""
- key: "c6"
label: ""
- key: "c7"
label: ""
binding:
list: "" # TODO (z.B. character.gameResults)
- blockId: 7
colStart: 2
colSpan: 1
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- label: "Geburtsdatum"
binding: "" # TODO
- label: "Alter / Händigkeit"
binding: "" # TODO
- label: "Größe"
binding: "" # TODO
- label: "Gestalt / Gewicht"
binding: "" # TODO
- label: "Stand"
binding: "" # TODO
- label: "Heimat"
binding: "" # TODO
- label: "Glaube"
binding: "" # TODO
- label: "Besondere Merkmale"
binding: "" # TODO
- blockId: 8
colStart: 3
colSpan: 2
boxTypeId: 01
anchor: top
heightMode: fixed
type: staticText
text: "Liste der gelernten und angeborenen Fertigkeiten"
- blockId: 9
colStart: 3
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (z.B. character.skills.stats)
- blockId: 10
colStart: 3
colSpan: 2
boxTypeId: 01
anchor: bottom
heightMode: fixed
type: labelValue
binding:
rows:
- label: "Sehen"
binding: "" # TODO
- label: "Nachtsicht"
binding: "" # TODO
- label: "Hören"
binding: "" # TODO
- label: "Riechen/Schmecken"
binding: "" # TODO
- label: "Sechster Sinn"
binding: "" # TODO
- blockId: 11
colStart: 4
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (Fortsetzung Statistik-Fertigkeiten)
# ---------------------------------------------------------
# Seite 2 Spielbogen
# ---------------------------------------------------------
- pageType: page2_play
blocks:
- blockId: 1
colStart: 1
colSpan: 4
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding:
rows:
- label: "Name des Charakters, Grad"
binding: "" # TODO
- label: "Typ, GG, SG"
binding: "" # TODO
- blockId: 2
colStart: 1
colSpan: 2
boxTypeId: 02
anchor: top
heightMode: fixed
type: labelValue
binding: {} # St, Gs, Gw, Ko, In, Zt, Au, pA, Wk, B TODO
- blockId: 3
colStart: 1
colSpan: 2
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # Abwehr / Resistenz / Zaubern TODO
- blockId: 4
colStart: 1
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (Spiel-Fertigkeiten)
- blockId: 5
colStart: 2
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: fixed
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
binding:
list: "" # TODO (ungelernte Fertigkeiten)
- blockId: 6
colStart: 2
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (Fortsetzung Spiel-Fertigkeiten)
- blockId: 7
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: fixed
type: table
table:
columns:
- key: "lp"
label: "LP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
- key: "c1"
label: ""
- key: "c2"
label: ""
- key: "c3"
label: ""
- key: "c4"
label: ""
- key: "c5"
label: ""
- key: "c6"
label: ""
binding:
list: "" # TODO (LP/AP-Tabelle)
- blockId: 8
colStart: 3
colSpan: 1
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # Bonus für Schaden/Angriff/Abwehr TODO
- blockId: 9
colStart: 3
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Waffe"
- key: "ew"
label: "EW"
- key: "damage"
label: "Schaden"
- key: "range"
label: "Nah"
binding:
list: "" # TODO (Waffen)
- blockId: 10
colStart: 3
colSpan: 2
boxTypeId: 01
anchor: bottom
heightMode: fixed
type: labelValue
binding: {} # Sinne (wie Seite 1 Block 10) TODO
- blockId: 11
colStart: 4
colSpan: 1
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # RK / mit Rüstung, B, Gw TODO
- blockId: 12
colStart: 4
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Waffe"
- key: "ew"
label: "EW"
- key: "damage"
label: "Schaden"
- key: "range"
label: "Nah"
binding:
list: "" # TODO (Fortsetzung Waffen)
- blockId: 13
colStart: 4
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: fixed
type: table
table:
columns:
- key: "spellClass"
label: "Zauberklasse"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (PP-Zauber)
# ---------------------------------------------------------
# Seite 3 Zauber
# ---------------------------------------------------------
- pageType: page3_spells
blocks:
- blockId: 1
colStart: 1
colSpan: 4
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # Name/Grad/Typ/Zaubern TODO
- blockId: 2
colStart: 1
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "ap_prozess"
label: "AP/Prozess"
multiline:
linesPerCell: 2
lines:
- binding: "ap" # erste Zeile: AP
- binding: "prozess" # zweite Zeile: Prozess
- key: "name"
label: "Zauber"
- key: "zd_rw"
label: "Zauberdauer/Reichweite"
multiline:
linesPerCell: 2
lines:
- binding: "zd" # erste Zeile: Zd
- binding: "rw" # zweite Zeile: Rw
- key: "wb_wd"
label: "Wirkungsbereich/Wirkungsdauer"
multiline:
linesPerCell: 2
lines:
- binding: "wb" # erste Zeile: Wb
- binding: "wd" # zweite Zeile: Wd
- key: "effect"
label: "Wirkung"
- key: "target"
label: "WirkZiel/Art"
multiline:
linesPerCell: 2
lines:
- binding: "Wirkziel" # erste Zeile: Wirkziel
- binding: "Art" # zweite Zeile: Art
binding:
list: "" # TODO (Zauber)
- blockId: 3
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "ap_prozess"
label: "AP/Prozess"
multiline:
linesPerCell: 2
lines:
- binding: "ap" # erste Zeile: AP
- binding: "prozess" # zweite Zeile: Prozess
- key: "name"
label: "Zauber"
- key: "zd_rw"
label: "Zauberdauer/Reichweite"
multiline:
linesPerCell: 2
lines:
- binding: "zd" # erste Zeile: Zd
- binding: "rw" # zweite Zeile: Rw
- key: "wb_wd"
label: "Wirkungsbereich/Wirkungsdauer"
multiline:
linesPerCell: 2
lines:
- binding: "wb" # erste Zeile: Wb
- binding: "wd" # zweite Zeile: Wd
- key: "effect"
label: "Wirkung"
- key: "target"
label: "WirkZiel/Art"
multiline:
linesPerCell: 2
lines:
- binding: "Wirkziel" # erste Zeile: Wirkziel
- binding: "Art"
binding:
list: "" # TODO (Fortsetzung Zauber)
- blockId: 4
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "item"
label: "Gegenstand"
- key: "description"
label: "Wirkung, Inhalt, Erläuterungen"
binding:
list: "" # TODO (magische Gegenstände/Ausrüstung)
# ---------------------------------------------------------
# Seite 4 Ausrüstung
# ---------------------------------------------------------
- pageType: page4_equipment
blocks:
- blockId: 1
colStart: 1
colSpan: 4
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # Name/Grad/Typ TODO
- blockId: 2
colStart: 1
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "item"
label: "Gegenstand"
- key: "description"
label: "Wirkung / Notizen"
binding:
list: "" # TODO (Ausrüstung am Körper)
- blockId: 3
colStart: 1
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "item"
label: "Gegenstand"
- key: "description"
label: "Wirkung / Notizen"
binding:
list: "" # TODO (Behälter 1 Im Wagen)
- blockId: 4
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "item"
label: "Gegenstand"
- key: "description"
label: "Wirkung / Notizen"
binding:
list: "" # TODO (Behälter 2)
- blockId: 5
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "item"
label: "Gegenstand"
- key: "description"
label: "Wirkung / Notizen"
binding:
list: "" # TODO (weiterer Behälter)
# ---------------------------------------------------------
# Seite 2.1 ff. Fortsetzung Spielbogen (Fertigkeiten/Waffen)
# ---------------------------------------------------------
- pageType: page2_play_cont
repeatable: true
blocks:
- blockId: 1
colStart: 1
colSpan: 4
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # Kopf wie Seite 2 TODO
- blockId: 2
colStart: 1
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (Fortsetzung Spiel-Fertigkeiten)
- blockId: 3
colStart: 2
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (Fortsetzung Spiel-Fertigkeiten)
- blockId: 4
colStart: 3
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Fertigkeit"
- key: "ew"
label: "EW"
- key: "pp"
label: "PP"
style:
backgroundColor: "#fff0b3" # hellgelb als Beispiel
binding:
list: "" # TODO (Fortsetzung Spiel-Fertigkeiten)
- blockId: 5
colStart: 4
colSpan: 1
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "name"
label: "Waffe"
- key: "ew"
label: "EW"
- key: "damage"
label: "Schaden"
- key: "range"
label: "Nah"
binding:
list: "" # TODO (Fortsetzung Waffen)
# ---------------------------------------------------------
# Seite 3.1 ff. Fortsetzung Zauber
# ---------------------------------------------------------
- pageType: page3_spells_cont
repeatable: true
blocks:
- blockId: 1
colStart: 1
colSpan: 4
boxTypeId: 01
anchor: top
heightMode: fixed
type: labelValue
binding: {} # Kopf wie Seite 3 TODO
- blockId: 2
colStart: 1
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "ap_prozess"
label: "AP/Prozess"
multiline:
linesPerCell: 2
lines:
- binding: "ap"
- binding: "prozess"
- key: "name"
label: "Zauber"
- key: "zd_rw"
label: "Zd/Rw"
multiline:
linesPerCell: 2
lines:
- binding: "zd"
- binding: "rw"
- key: "wbWd"
label: "Wb/Wd"
- key: "effect"
label: "Wirkung"
- key: "target"
label: "WirkZiel/Art"
binding:
list: "" # TODO (Fortsetzung Zauber)
- blockId: 3
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "ap_prozess"
label: "AP/Prozess"
multiline:
linesPerCell: 2
lines:
- binding: "ap"
- binding: "prozess"
- key: "name"
label: "Zauber"
- key: "zd_rw"
label: "Zd/Rw"
multiline:
linesPerCell: 2
lines:
- binding: "zd"
- binding: "rw"
- key: "wbWd"
label: "Wb/Wd"
- key: "effect"
label: "Wirkung"
- key: "target"
label: "WirkZiel/Art"
binding:
list: "" # TODO (Fortsetzung Zauber)
- blockId: 4
colStart: 3
colSpan: 2
boxTypeId: 03
anchor: top
heightMode: rows
type: table
table:
columns:
- key: "item"
label: "Gegenstand"
- key: "description"
label: "Wirkung, Inhalt, Erläuterungen"
binding:
list: "" # TODO (magische Gegenstände/Ausrüstung)
-80
View File
@@ -1,80 +0,0 @@
#!/bin/bash
# Konsolidierte Character Skill Cost API Examples
# Diese Skripts zeigen, wie die einheitliche API verwendet werden kann
BASE_URL="http://localhost:8080/api/characters"
CHAR_ID="1" # Beispiel Charakter-ID
echo "=== Konsolidierte Character Skill Cost API Examples ==="
echo
# 1. Fertigkeit lernen
echo "1. Fertigkeit lernen:"
curl -s -X POST "$BASE_URL/$CHAR_ID/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Stehlen",
"type": "skill",
"action": "learn"
}' | jq .
echo
echo "2. Fertigkeit verbessern (nächste Stufe):"
curl -s -X POST "$BASE_URL/$CHAR_ID/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Menschenkenntnis",
"type": "skill",
"action": "improve",
"current_level": 14
}' | jq .
echo
echo "3. Multi-Level Verbesserung:"
curl -s -X POST "$BASE_URL/$CHAR_ID/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Menschenkenntnis",
"type": "skill",
"action": "improve",
"current_level": 14,
"target_level": 16
}' | jq .
echo
echo "4. Mit Praxispunkten:"
curl -s -X POST "$BASE_URL/$CHAR_ID/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Menschenkenntnis",
"type": "skill",
"action": "improve",
"current_level": 14,
"use_pp": 1
}' | jq .
echo
echo "5. Zauber lernen:"
curl -s -X POST "$BASE_URL/$CHAR_ID/skill-cost" \
-H "Content-Type: application/json" \
-d '{
"name": "Macht über das Selbst",
"type": "spell",
"action": "learn"
}' | jq .
echo
echo "6. Legacy: Nächste Verbesserungsstufe:"
curl -s -X GET "$BASE_URL/$CHAR_ID/improve" | jq .
echo
echo "7. System-Info: Charakterklassen:"
curl -s -X GET "$BASE_URL/character-classes" | jq .
echo
echo "8. System-Info: Fertigkeitskategorien:"
curl -s -X GET "$BASE_URL/skill-categories" | jq .
echo
echo "=== Konsolidierte API-Beispiele abgeschlossen ==="
-9
View File
@@ -1,9 +0,0 @@
transformiere die Struktur von LearningCostsTable2 so so das die Daten in einer Datenbank verwaltet werden können. Normalisiere die Tabellen soweit sinnvoll.
Beachte das Charakterklassen einen Code(Hx) und einen Namen Hexer haben.
Beachte das Fertigkeiten in mehreren Katagorien enthalten sein können und das die Fetigkeiten je nach Kategorie eine unterschiedliche Schwierigkeit aufweisen können was zu unterschiedlichen Lernkosten führen kann.
Erstelle nur die neue Struktur, erstelle keine Inhalte
erstelle eine Funktion die den Inhalt der bestehenden learningCostsData Variablen in die Datenbank überträgt
Erstelle eine Funktion die die Daten der neuen Tabellen mit denen der schon zuvor vorhandenen Tabellen ergänzt
@@ -1,127 +0,0 @@
# Continuation Pages Feature - Complete Implementation
## Overview
The continuation pages feature is **FULLY IMPLEMENTED AND WORKING**. When character data exceeds template capacity, continuation pages are automatically generated as separate PDF files.
## Proof of Implementation
Test run output from `TestIntegration_ContinuationPages_ActualFiles`:
```
✓ Generated 5 PDF pages (1 main + 4 continuations)
✓ Saved /tmp/bamort_continuation_test/page1_stats.pdf (539429 bytes)
✓ Saved /tmp/bamort_continuation_test/page1_stats_continuation_1.pdf (539022 bytes)
✓ Saved /tmp/bamort_continuation_test/page1_stats_continuation_2.pdf (539837 bytes)
✓ Saved /tmp/bamort_continuation_test/page1_stats_continuation_3.pdf (539427 bytes)
✓ Saved /tmp/bamort_continuation_test/page1_stats_continuation_4.pdf (539351 bytes)
✓ Combined all 5 pages into: /tmp/bamort_continuation_test/page1_stats_combined.pdf (600511 bytes)
```
## How It Works
### 1. Template Naming Convention
Continuation pages follow the naming pattern:
- Main page: `page1_stats.html`
- Continuation 2: `page1.2_stats.html`
- Continuation 3: `page1.3_stats.html`
- And so on...
### 2. Automatic PDF Generation
The `RenderPageWithContinuations()` function:
- Detects when data exceeds template capacity
- Automatically paginates data across multiple pages
- Renders each page as a separate PDF
- Returns a slice of PDF byte arrays
### 3. Usage Example
```go
// Load your character view model
viewModel, err := MapCharacterToViewModel(char)
if err != nil {
return err
}
// Load templates
loader := NewTemplateLoader("templates/Default_A4_Quer")
if err = loader.LoadTemplates(); err != nil {
return err
}
renderer := NewPDFRenderer()
// Render page with automatic continuation handling
pdfs, err := RenderPageWithContinuations(
viewModel,
"page1_stats.html", // Template name
1, // Starting page number
"20.12.2025", // Date
loader,
renderer,
)
if err != nil {
return err
}
// pdfs now contains:
// - pdfs[0]: Main page PDF
// - pdfs[1]: First continuation page PDF (if needed)
// - pdfs[2]: Second continuation page PDF (if needed)
// - etc.
// Save individual PDFs
for i, pdf := range pdfs {
filename := fmt.Sprintf("page1_stats_%d.pdf", i+1)
os.WriteFile(filename, pdf, 0644)
}
// Or merge into single PDF using pdfcpu
api.MergeCreateFile(filePaths, "combined.pdf", false, nil)
```
## Supported Template Types
Continuation pages work for all template types:
- ✅ **page1_stats.html** - Skills (tested with 50 skills → 5 pages)
- ✅ **page2_play.html** - Weapons
- ✅ **page3_spell.html** - Spells
- ✅ **page4_equip.html** - Equipment
## Key Features
1. **No Template Files Needed**: Continuation pages reuse the base template structure
2. **Dynamic Capacity**: Reads capacity from template metadata comments
3. **Automatic Pagination**: Handles any number of continuation pages
4. **PDF Merging**: Can combine all pages into single PDF
5. **Fully Tested**: Integration tests verify actual PDF generation
## Files Added/Modified
### New Files
- `render_with_continuation.go` - Main implementation
- `render_with_continuation_test.go` - Unit tests
- `continuation_integration_test.go` - Integration test with file output
- `pagination_utils_test.go` - Template name utility tests
### Modified Files
- `pagination.go` - Template name generation
- `templates.go` - Continuation template fallback
- `todo.md` - Documentation
## Test Coverage
All tests pass (32.7s runtime):
- Unit tests for pagination logic
- Unit tests for template naming
- Integration test with actual PDF generation
- Visual inspection test still works
## Status
✅ **COMPLETE AND WORKING**
Continuation pages are automatically generated when data exceeds template capacity. The feature has been thoroughly tested and verified with actual PDF file output.
-129
View File
@@ -1,129 +0,0 @@
# Pagination and Template Metadata Fixes
## Summary
Fixed all issues from todo.md related to template metadata, pagination capacities, and empty row filling.
## Changes Made
### 1. Dynamic Capacity Loading (✓ COMPLETED)
**Problem**: PreparePaginatedPageData hardcoded capacity values that didn't match template MAX values.
**Solution**:
- Added `GetBlockCapacity()` helper function to read MAX from template metadata
- Updated PreparePaginatedPageData() to dynamically load capacities for all pages
- Removed all hardcoded capacity values (24, 11, 5, 30, 20, 10, etc.)
**Files Modified**:
- `pagination_helper.go`: Added GetBlockCapacity(), updated all page handlers
### 2. Page2 Pagination Fixed (✓ COMPLETED)
**Problem**:
- skills_learned used capacity 24 instead of template's MAX:18
- skills_languages used capacity 11 instead of template's MAX:5
- weapons_main capacity was inconsistent
**Solution**:
- Page2 now reads correct capacities from template:
- skills_learned: MAX:18 (was 24)
- skills_unlearned: MAX:15 (correct)
- skills_languages: MAX:5 (was 11)
- weapons_main: MAX:24 (was 30)
**Template Values** (from page2_play.html):
```html
<!-- BLOCK: skills_learned, TYPE: skills, MAX: 18, FILTER: learned -->
<!-- BLOCK: skills_unlearned, TYPE: skills, MAX: 15, FILTER: unlearned -->
<!-- BLOCK: skills_languages, TYPE: skills, MAX: 5, FILTER: language -->
<!-- BLOCK: weapons_main, TYPE: weapons, MAX: 24 -->
```
### 3. Page3 Magic Items Fixed (✓ COMPLETED)
**Problem**: magic_items used capacity 5 instead of template's MAX:8
**Solution**: Updated to read from template (MAX:8)
**Template Values** (from page3_spell.html):
```html
<!-- BLOCK: spells_left, TYPE: spells, MAX: 26 -->
<!-- BLOCK: spells_right, TYPE: spells, MAX: 15 -->
<!-- BLOCK: magic_items, TYPE: magicItems, MAX: 8 -->
```
### 4. Page4 Equipment Fixed (✓ COMPLETED)
**Problem**: Used wrong block name ("equipment" instead of "equipment_worn")
**Solution**: Updated to use correct block name from template
**Template Values** (from page4_equip.html):
```html
<!-- BLOCK: equipment_worn, TYPE: equipment, MAX: 10, FILTER: worn -->
```
### 5. Tests Updated (✓ COMPLETED)
**Problem**: Tests hardcoded expected MAX values instead of reading from templates
**Solution**: Updated all tests to dynamically read capacities:
- `TestPaginationUsesTemplateMetadata`: Verifies template parsing works
- `TestPage2PaginationWithCorrectCapacities`: Uses GetBlockCapacity()
- `TestPage3MagicItemsCapacity`: Uses GetBlockCapacity()
- `TestPreparePaginatedPageData_Page3Spell`: Updated expectations
- `TestPreparePaginatedPageData_Page4Equipment`: Fixed block name
- `TestCalculatePagesNeeded`: Updated to match template capacities (24 not 30, 41 not 30)
- `TestPaginateSpells_MultiPage`: Updated to 26+15=41
- `TestPaginateWeapons_MultiPage`: Updated to 24 capacity (3 pages for 50 weapons)
- `TestIntegration_TemplateMetadata`: Updated expected values
- `templates_test.go`: Updated TestGetTemplateMetadata expectations
- `template_metadata_loader_test.go`: Updated expected values
**Files Modified**:
- `todo_fixes_test.go`: New test file with TDD tests
- `pagination_helper_test.go`: Updated expectations
- `pagination_test.go`: Updated test cases
- `integration_test.go`: Updated expected block MAX values
- `templates_test.go`: Updated to expect 26/15 not 20/10
- `template_metadata_loader_test.go`: Updated to expect 26 not 20
## Test Results
```
70 tests passing
0 tests failing
All pdfrender tests pass:
- 13 pagination tests
- 8 template metadata tests
- 6 fill_capacity tests
- 9 mapper tests
- 17 integration tests
- 4 new todo_fixes tests
- 13 other tests
```
## Visual Verification
Generated test PDFs confirm:
- ✓ Page1: Skills split correctly across 2 columns (29+29)
- ✓ Page2: 18 learned skills, 5 language skills, 24 weapons
- ✓ Page3: 26 left spells, 15 right spells, 8 magic items
- ✓ Page4: 10 equipment items
- ✓ All empty rows render correctly
- ✓ Combined PDF merges all pages successfully
## Weapons Implementation Note
The current implementation correctly uses `Waffenfertigkeiten` (weapon skills) for the weapons_main list. Each weapon skill already contains:
- Name: Weapon name (e.g., "Schwert", "Bogen")
- Value (EW): Fertigkeitswert (skill effectiveness value)
- This is the correct data for character sheet display
The `Equipment.Weapons` (EqWaffe) contains physical weapon objects with different metadata (Abwb, Schb, weight, etc.) which is not needed for the weapons_main table on page2.
## Architecture Improvements
1. **Separation of Concerns**: Template metadata is now the single source of truth
2. **Maintainability**: Adding/changing template capacities only requires HTML comment updates
3. **Type Safety**: GetBlockCapacity() provides consistent interface
4. **Testability**: Tests verify against actual templates, not hardcoded values
5. **DRY Principle**: No duplication between template definitions and code
## Future Enhancements
All current issues resolved. System ready for:
- Multi-page pagination (already working for >58 skills, >24 weapons, >41 spells)
- Additional template blocks (just add HTML comments)
- Different template sets (already supported via LoadTemplateSetFromFiles)
-209
View File
@@ -1,209 +0,0 @@
# PDF Export Implementation Summary
## Overview
Successfully implemented a complete PDF export system for character sheets following TDD and KISS principles.
## Components Implemented
### 1. View Model (viewmodel.go)
- `CharacterSheetViewModel`: Main data structure for character sheet rendering
- `CharacterInfo`: Basic character information (name, player, type, grade, etc.)
- `AttributeValues`: Character attributes (St, Gs, Gw, Ko, In, Zt, Au, PA, Wk, B)
- `DerivedValueSet`: Calculated values (LP, AP, bonuses, resistances)
- `SkillViewModel`, `WeaponViewModel`, `SpellViewModel`: Skill representations
- `EquipmentViewModel`: Equipment and container data
- `PageMeta`, `PageData`: Page metadata for rendering
### 2. Mapper (mapper.go)
- `MapCharacterToViewModel()`: Main conversion function
- Converts `models.Char` to `CharacterSheetViewModel`
- Maps attributes, derived values, skills, weapons, spells, equipment
- **All 8 mapper tests passing**
### 3. Template Metadata (template_metadata.go, template_parser.go)
- `BlockMetadata`: Defines list capacities (MAX) and filters (FILTER)
- `ParseTemplateMetadata()`: Extracts metadata from HTML comments
- Format: `<!-- BLOCK: name, TYPE: type, MAX: 12, FILTER: learned -->`
- Self-documenting templates store their own capacity constraints
- **All 4 parser tests passing**
### 4. Template Loader (templates.go)
- `TemplateLoader`: Manages HTML template loading and rendering
- `LoadTemplates()`: Loads all .html files from template directory
- `RenderTemplate()`: Renders templates with view model data
- Custom template functions: `iterate` for fixed-size loops
- **All 5 template tests passing**
### 5. PDF Renderer (chromedp.go)
- `PDFRenderer`: Converts HTML to PDF using chromedp
- `RenderHTMLToPDF()`: Browser-based HTML to PDF conversion
- A4 landscape format (11.69" x 8.27")
- Includes background colors and images
- `ImageToBase64DataURI()`: Helper for image embedding
- **All 5 chromedp tests passing**
### 6. Pagination System (pagination.go)
- `Paginator`: Core pagination engine with template awareness
- `PageDistribution`: Represents data distribution for a single page
- `PaginateSkills()`: Splits skills across columns and pages (64 per page)
- `PaginateSpells()`: Handles spell pagination (24 per page)
- `PaginateWeapons()`: Distributes weapons (30 per page)
- `PaginateEquipment()`: Manages equipment pagination
- `CalculatePagesNeeded()`: Pre-calculates required pages
- **All 13 pagination tests passing**
### 7. Integration Tests (integration_test.go)
- `TestIntegration_FullPDFGeneration`: End-to-end workflow test
- Character → ViewModel → Template → HTML → PDF
- Successfully generates ~31KB PDF
- `TestIntegration_TemplateMetadata`: Verifies all templates have metadata
- `TestIntegration_PaginationWithPDF`: Tests 100 skills across 2 pages with PDF generation
- Page 1: 64 skills, ~45KB PDF
- Page 2: 36 skills
- `TestIntegration_MultiPageSpellList`: Tests 30 spells across 2 pages
- Page 1: 24 spells (12+12 columns)
- Page 2: 6 spells
- **All 4 integration tests passing**
## Templates Converted
All 4 HTML templates converted to Go template syntax:
1. **page1_stats.html**: Character stats, attributes, skills, history
- Metadata: `skills_column1 MAX:32`, `skills_column2 MAX:32`
2. **page2_play.html**: Adventure sheet, combat stats, weapons
- Metadata: `skills_learned MAX:24 FILTER:learned`, `skills_unlearned MAX:15 FILTER:unlearned`, `weapons_main MAX:30`
3. **page3_spell.html**: Spell lists and magic items
- Metadata: `spells_left MAX:12`, `spells_right MAX:10`, `magic_items MAX:5`
- Different capacities for left vs right columns
4. **page4_equip.html**: Equipment, containers, currency
- Metadata: `equipment_worn MAX:10 FILTER:worn`
## Test Results
**Total: 39/39 tests passing** ✓
- Mapper: 8/8 ✓
- Parser: 4/4 ✓
- Templates: 5/5 ✓
- Chromedp: 5/5 ✓
- Pagination: 13/13 ✓
- Integration: 4/4 ✓
## Dependencies Added
- `github.com/chromedp/chromedp v0.14.2`
- `github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d`
## Next Steps (Not Yet Implemented)
1. **API Endpoint**: Create HTTP endpoint to trigger PDF generation
2. **Image Loading**: Load character icons from filesystem/database
3. **PDF Merging**: Combine multiple pages into single PDF document
4. **Error Handling**: Add comprehensive error handling and logging
5. **Caching**: Consider template caching for performance
6. **Frontend Integration**: Connect to Vue.js frontend
7. **Download Handler**: Implement PDF download endpoint with proper headers
## Usage Example
### Basic Single Page
```go
// 1. Map character to view model
viewModel, err := pdfrender.MapCharacterToViewModel(char)
// 2. Load templates
loader := pdfrender.NewTemplateLoader("templates/Default_A4_Quer")
loader.LoadTemplates()
// 3. Render template to HTML
pageData := &pdfrender.PageData{
Character: viewModel.Character,
Skills: viewModel.Skills,
// ... other data
}
html, err := loader.RenderTemplate("page1_stats.html", pageData)
// 4. Convert to PDF
renderer := pdfrender.NewPDFRenderer()
pdfBytes, err := renderer.RenderHTMLToPDF(html)
```
### With Pagination (Multiple Pages)
```go
// 1. Map character to view model
viewModel, err := pdfrender.MapCharacterToViewModel(char)
// 2. Paginate skills (100 skills -> 2 pages)
templateSet := pdfrender.DefaultA4QuerTemplateSet()
paginator := pdfrender.NewPaginator(templateSet)
pages, err := paginator.PaginateSkills(viewModel.Skills, "page1_stats.html", "")
// 3. Load templates and renderer
loader := pdfrender.NewTemplateLoader("templates/Default_A4_Quer")
loader.LoadTemplates()
renderer := pdfrender.NewPDFRenderer()
// 4. Generate PDF for each page
var pdfFiles [][]byte
for _, page := range pages {
// Extract data for this page
col1 := page.Data["skills_column1"].([]pdfrender.SkillViewModel)
col2 := page.Data["skills_column2"].([]pdfrender.SkillViewModel)
pageData := &pdfrender.PageData{
Character: viewModel.Character,
Attributes: viewModel.Attributes,
DerivedValues: viewModel.DerivedValues,
Skills: append(col1, col2...),
Meta: pdfrender.PageMeta{
Date: time.Now().Format("02.01.2006"),
PageNumber: page.PageNumber,
},
}
// Render and convert
html, _ := loader.RenderTemplate(page.TemplateName, pageData)
pdfBytes, _ := renderer.RenderHTMLToPDF(html)
pdfFiles = append(pdfFiles, pdfBytes)
}
// 5. Save or merge PDFs
for i, pdf := range pdfFiles {
os.WriteFile(fmt.Sprintf("character_page%d.pdf", i+1), pdf, 0644)
}
```
## Architecture Decisions
1. **TDD Approach**: All components developed test-first
2. **KISS Principle**: Simple slices instead of complex generic wrappers
3. **Self-Documenting**: Templates contain their own capacity metadata
4. **Separation of Concerns**: Clear boundaries between mapper, template, PDF rendering
5. **Type Safety**: Strong typing throughout with Go structs
6. **Browser-Based Rendering**: chromedp ensures accurate HTML/CSS rendering
## Files Created/Modified
- `backend/pdfrender/viewmodel.go` (new)
- `backend/pdfrender/mapper.go` (new)
- `backend/pdfrender/mapper_test.go` (new)
- `backend/pdfrender/pagination.go` (new)
- `backend/pdfrender/template_metadata.go` (new)
- `backend/pdfrender/template_parser.go` (new)
- `backend/pdfrender/template_parser_test.go` (new)
- `backend/pdfrender/templates.go` (new)
- `backend/pdfrender/templates_test.go` (new)
- `backend/pdfrender/chromedp.go` (new)
- `backend/pdfrender/chromedp_test.go` (new)
- `backend/pdfrender/integration_test.go` (new)
- `backend/templates/Default_A4_Quer/page1_stats.html` (modified)
- `backend/templates/Default_A4_Quer/page2_play.html` (modified)
- `backend/templates/Default_A4_Quer/page3_spell.html` (modified)
- `backend/templates/Default_A4_Quer/page4_equip.html` (modified)
- `backend/go.mod` (modified - added chromedp)
-208
View File
@@ -1,208 +0,0 @@
# Pagination Implementation - Completion Report
## ✅ Implementation Complete
Successfully implemented a comprehensive pagination system that intelligently splits character data across multiple columns and pages based on template capacity constraints.
## Summary
**All 40 tests passing** with **79.8% code coverage**
### What Was Built
1. **Core Pagination Engine** (`pagination.go`)
- `Paginator` struct with template-aware distribution logic
- `PageDistribution` to represent data for each page
- Methods for skills, spells, weapons, and equipment pagination
- Capacity calculation for planning
2. **Comprehensive Test Suite** (`pagination_test.go`)
- 13 pagination tests covering all scenarios
- Single/multi-column distribution
- Multi-page overflow handling
- Edge cases (empty lists, invalid templates)
3. **Integration Tests** (`integration_test.go`)
- 2 new integration tests with PDF generation
- Complete workflow test with 70 skills → 2 pages
- Real PDF generation and validation
4. **Documentation**
- Detailed pagination guide with examples
- Updated implementation summary
- Usage patterns and best practices
## Key Features
### ✓ Multi-Column Support
- Automatically distributes items across columns
- Example: 40 skills → Column 1 (32) + Column 2 (8)
### ✓ Multi-Page Overflow
- Generates additional pages when capacity exceeded
- Example: 100 skills → Page 1 (64) + Page 2 (36)
### ✓ Template-Aware
- Reads capacity from template metadata
- Different capacities per template (skills: 64, spells: 24, weapons: 30)
### ✓ Type-Safe
- Strongly typed with generics where possible
- Type assertions for data extraction
### ✓ Thoroughly Tested
- Unit tests for core logic
- Integration tests with PDF generation
- Edge case coverage
## Test Results
```
=== Test Summary ===
Mapper Tests: 8/8 ✓
Parser Tests: 4/4 ✓
Template Tests: 5/5 ✓
Chromedp Tests: 5/5 ✓
Pagination Tests: 13/13 ✓
Integration Tests: 5/5 ✓
─────────────────────────
Total: 40/40 ✓
Coverage: 79.8%
```
## Performance
### Complete Workflow Test Results
- **70 skills** distributed across **2 pages**
- **Page 1**: 64 skills → 34,617 bytes PDF
- **Page 2**: 6 skills → 31,562 bytes PDF
- **Total time**: 2.3 seconds (includes chromedp startup)
- **Memory**: Minimal overhead with slice operations
### Pagination Performance
- **Complexity**: O(n) - linear with item count
- **Memory**: Minimal - creates slices, no copying
- **Speed**: Sub-millisecond for typical datasets
## Usage Pattern
```go
// 1. Map character
viewModel, _ := MapCharacterToViewModel(char)
// 2. Initialize paginator
templateSet := DefaultA4QuerTemplateSet()
paginator := NewPaginator(templateSet)
// 3. Paginate data
pages, _ := paginator.PaginateSkills(viewModel.Skills, "page1_stats.html", "")
// 4. Render each page
for _, page := range pages {
col1 := page.Data["skills_column1"].([]SkillViewModel)
col2 := page.Data["skills_column2"].([]SkillViewModel)
pageData := &PageData{
Skills: append(col1, col2...),
Meta: PageMeta{PageNumber: page.PageNumber},
}
html, _ := loader.RenderTemplate(page.TemplateName, pageData)
pdf, _ := renderer.RenderHTMLToPDF(html)
}
```
## Template Capacities Reference
| Template | List Type | Capacity | Notes |
|-------------------|------------|----------|----------------------|
| page1_stats.html | skills | 64 | 2 columns (32+32) |
| page2_play.html | weapons | 30 | Single block |
| page2_play.html | skills | 50 | Multiple blocks |
| page3_spell.html | spells | 24 | 2 columns (12+12) |
| page3_spell.html | magicItems | 5 | Single block |
| page4_equip.html | equipment | 20 | Single block |
## Files Created/Modified
### New Files
- `backend/pdfrender/pagination_test.go` - 13 comprehensive tests
- `backend/pdfrender/PAGINATION_GUIDE.md` - Complete usage documentation
### Modified Files
- `backend/pdfrender/pagination.go` - Added full pagination system
- `backend/pdfrender/integration_test.go` - Added 3 integration tests
- `backend/pdfrender/IMPLEMENTATION_SUMMARY.md` - Updated with pagination info
## Architecture
```
┌─────────────────┐
│ Character │
│ (Domain) │
└────────┬────────┘
┌─────────────────┐
│ Mapper │
│ ViewModel │
└────────┬────────┘
┌─────────────────┐ ┌──────────────────┐
│ Paginator │────→│ PageDistribution │
│ (Split by Cap) │ │ (Per Page) │
└────────┬────────┘ └──────────────────┘
┌─────────────────┐
│ Template Loader │
│ (Render HTML) │
└────────┬────────┘
┌─────────────────┐
│ PDF Renderer │
│ (Chromedp) │
└────────┬────────┘
PDF Files
```
## What's Next
The pagination system is **production-ready**. Recommended next steps:
1. **API Endpoint**: Create REST endpoint for PDF generation
2. **PDF Merging**: Combine multiple page PDFs into single document
3. **Batch Processing**: Generate all character pages in one request
4. **Caching**: Cache rendered HTML for identical data
5. **Frontend**: Integrate with Vue.js character sheet viewer
## Lessons Learned
1. **Template Metadata is Key**: Self-documenting templates with capacity info worked perfectly
2. **Type Assertions**: Necessary but manageable with good error handling
3. **Testing First**: TDD approach caught edge cases early
4. **Chromedp is Solid**: Reliable PDF generation with proper HTML/CSS support
5. **KISS Principle**: Simple slice operations beat complex generic wrappers
## Conclusion
**Pagination system fully implemented and tested**
**79.8% code coverage**
**40/40 tests passing**
**Production-ready**
**Well-documented**
The system successfully handles:
- ✓ Multi-column layouts
- ✓ Multi-page overflow
- ✓ Template capacity constraints
- ✓ Type-safe data distribution
- ✓ Integration with PDF generation
- ✓ Edge cases and error handling
**Ready for integration with API endpoints and frontend.**
-290
View File
@@ -1,290 +0,0 @@
# Pagination Implementation Guide
## Overview
The pagination system intelligently splits character data across multiple columns and pages based on template capacity metadata. It ensures proper distribution of skills, spells, weapons, and equipment without exceeding template limits.
## Core Components
### 1. Paginator (`pagination.go`)
The `Paginator` handles all pagination logic:
```go
paginator := NewPaginator(templateSet)
```
#### Main Methods
- **PaginateSkills**: Splits skills across columns and pages
- **PaginateSpells**: Handles spell list pagination with column support
- **PaginateWeapons**: Distributes weapons across multiple pages
- **PaginateEquipment**: Manages equipment pagination
- **CalculatePagesNeeded**: Pre-calculates page count for planning
### 2. PageDistribution
Represents how data is distributed for a single page:
```go
type PageDistribution struct {
TemplateName string // Template to use
PageNumber int // 1-indexed page number
Data map[string]interface{} // Block name -> data slice
}
```
## Usage Examples
### Example 1: Paginate Skills Across Multiple Pages
```go
// Create 100 skills (exceeds 64 capacity of page1_stats)
skills := make([]SkillViewModel, 100)
for i := 0; i < 100; i++ {
skills[i] = SkillViewModel{
Name: "Skill " + strconv.Itoa(i),
Value: 10 + i%20,
}
}
// Initialize paginator
templateSet := DefaultA4QuerTemplateSet()
paginator := NewPaginator(templateSet)
// Paginate skills
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
if err != nil {
log.Fatal(err)
}
// Result: 2 pages
// Page 1: skills_column1 (32) + skills_column2 (32) = 64 skills
// Page 2: skills_column1 (32) + skills_column2 (4) = 36 skills
// Load templates
loader := NewTemplateLoader("templates/Default_A4_Quer")
loader.LoadTemplates()
// Render each page
renderer := NewPDFRenderer()
for _, page := range pages {
pageData := &PageData{
Character: viewModel.Character,
Attributes: viewModel.Attributes,
DerivedValues: viewModel.DerivedValues,
Meta: PageMeta{
Date: time.Now().Format("02.01.2006"),
PageNumber: page.PageNumber,
},
}
// Add skills from this page's distribution
col1 := page.Data["skills_column1"].([]SkillViewModel)
col2 := page.Data["skills_column2"].([]SkillViewModel)
pageData.Skills = append(col1, col2...)
// Render to HTML
html, _ := loader.RenderTemplate(page.TemplateName, pageData)
// Generate PDF
pdfBytes, _ := renderer.RenderHTMLToPDF(html)
// Save or return PDF
os.WriteFile(fmt.Sprintf("character_page%d.pdf", page.PageNumber), pdfBytes, 0644)
}
```
### Example 2: Paginate Spells with Two Columns
```go
// Create 30 spells (exceeds 24 capacity of page3_spell)
spells := make([]SpellViewModel, 30)
for i := 0; i < 30; i++ {
spells[i] = SpellViewModel{
Name: "Zauber " + strconv.Itoa(i),
AP: 5,
Duration: "1 Minute",
}
}
// Paginate
pages, err := paginator.PaginateSpells(spells, "page3_spell.html")
// Result: 2 pages
// Page 1: spells_column1 (12) + spells_column2 (12) = 24 spells
// Page 2: spells_column1 (6) + spells_column2 (0) = 6 spells
for _, page := range pages {
col1Spells := page.Data["spells_column1"].([]SpellViewModel)
col2Spells := page.Data["spells_column2"].([]SpellViewModel)
pageData := &PageData{
Character: viewModel.Character,
Spells: append(col1Spells, col2Spells...),
Meta: PageMeta{
PageNumber: page.PageNumber,
},
}
// Render and generate PDF...
}
```
### Example 3: Pre-Calculate Page Count
```go
// Check how many pages will be needed before pagination
pagesNeeded, err := paginator.CalculatePagesNeeded(
"page1_stats.html",
"skills",
len(skills),
)
fmt.Printf("Will need %d pages for %d skills\n", pagesNeeded, len(skills))
```
### Example 4: Paginate Weapons
```go
// Create 50 weapons (exceeds 30 capacity of page2_play)
weapons := make([]WeaponViewModel, 50)
for i := 0; i < 50; i++ {
weapons[i] = WeaponViewModel{
Name: "Waffe " + strconv.Itoa(i),
Value: 10 + i,
Damage: "1W6+2",
}
}
// Paginate weapons
pages, err := paginator.PaginateWeapons(weapons, "page2_play.html")
// Result: 2 pages
// Page 1: weapons_main (30)
// Page 2: weapons_main (20)
for _, page := range pages {
weaponsData := page.Data["weapons_main"].([]WeaponViewModel)
pageData := &PageData{
Character: viewModel.Character,
Weapons: weaponsData,
Meta: PageMeta{
PageNumber: page.PageNumber,
},
}
// Render and generate PDF...
}
```
## Template Capacities
### page1_stats.html (Statistics Page)
- **skills_column1**: MAX 32 (left column)
- **skills_column2**: MAX 32 (right column)
- **Total**: 64 skills per page
### page2_play.html (Adventure Page)
- **skills_learned**: MAX 24 (FILTER: learned)
- **skills_unlearned**: MAX 15 (FILTER: unlearned)
- **skills_languages**: MAX 11 (FILTER: languages)
- **weapons_main**: MAX 30
### page3_spell.html (Spell Page)
- **spells_column1**: MAX 12 (left column)
- **spells_column2**: MAX 12 (right column)
- **magic_items**: MAX 5
- **Total**: 24 spells per page
### page4_equip.html (Equipment Page)
- **equipment_sections**: MAX 20
- **game_results**: MAX 10
## How Pagination Works
1. **Capacity Calculation**: Paginator reads template metadata to determine capacity
2. **Distribution**: Items are distributed across blocks according to MaxItems
3. **Page Creation**: Multiple pages are created when capacity is exceeded
4. **Column Handling**: Items fill first column, then overflow to second column
5. **Page Overflow**: Remaining items continue on next page
## Algorithm Details
### Single Column Distribution
```
Items: 50, Capacity per block: 30
Result:
Page 1: Block 1 (30 items)
Page 2: Block 1 (20 items)
```
### Multi-Column Distribution
```
Items: 100, Column 1: 32, Column 2: 32 (64 per page)
Result:
Page 1: Column 1 (32) + Column 2 (32) = 64
Page 2: Column 1 (32) + Column 2 (4) = 36
```
## Best Practices
1. **Always check errors** when paginating
2. **Pre-calculate page count** for UI/UX planning
3. **Loop through all pages** to generate complete PDF sets
4. **Maintain page numbers** in metadata for proper labeling
5. **Type assert carefully** when extracting data from PageDistribution.Data
## Error Handling
```go
pages, err := paginator.PaginateSkills(skills, "page1_stats.html", "")
if err != nil {
// Handle errors:
// - Template not found
// - No capacity defined for list type
// - Invalid template configuration
log.Printf("Pagination failed: %v", err)
return
}
if len(pages) == 0 {
log.Println("No pages needed (empty list)")
return
}
```
## Testing
All pagination logic is thoroughly tested:
- ✓ Single page scenarios
- ✓ Multi-column distribution
- ✓ Multi-page overflow
- ✓ Empty lists
- ✓ Invalid templates
- ✓ Capacity calculations
- ✓ Integration with PDF generation
Run tests:
```bash
go test -v ./pdfrender/... -run TestPaginate
```
## Performance Considerations
- **Memory**: Pagination creates slices, minimal memory overhead
- **Speed**: O(n) complexity, very fast even for large lists
- **Caching**: Template metadata is parsed once and reused
- **Scalability**: Handles thousands of items efficiently
## Future Enhancements
Potential improvements for consideration:
1. **Filter-based pagination**: Separate learned/unlearned skills
2. **Custom sorting**: Order items before pagination
3. **Dynamic capacity**: Adjust based on font size or layout
4. **Partial rendering**: Generate only specific pages on demand
5. **Merge PDFs**: Combine multiple page PDFs into single document
-137
View File
@@ -1,137 +0,0 @@
# Pagination Integration Summary
## Problem
The pagination system was fully implemented but not being used during PDF rendering. When the user changed the MAX value from 32 to 29 in the template, it had no effect because:
1. The integration test was passing the full skills list directly to the template
2. The template was trying to render all skills with `{{range .Skills}}`
3. The pagination logic was never invoked
## Solution
Integrated the pagination system into the rendering workflow:
### 1. Created Pagination Helper (`pagination_helper.go`)
- **PreparePaginatedPageData**: Prepares data for rendering with proper pagination
- Takes full view model and template name
- Returns PageData with lists split according to template capacity
- Handles all 4 page types (stats, play, spell, equipment)
- **SplitSkillsForColumns**: Utility function to split skills into two columns
- Takes skills list and column capacities
- Returns (column1Skills, column2Skills)
- Properly truncates if total exceeds capacity
### 2. Updated Template Structure
**Modified:** [page1_stats.html](backend/templates/Default_A4_Quer/page1_stats.html)
- Changed `{{range .Skills}}` to `{{range .SkillsColumn1}}` for first column
- Changed empty iteration to `{{range .SkillsColumn2}}` for second column
- Now properly renders skills in two separate columns
### 3. Updated Data Model
**Modified:** [viewmodel.go](backend/pdfrender/viewmodel.go)
- Added `SkillsColumn1 []SkillViewModel` to PageData struct
- Added `SkillsColumn2 []SkillViewModel` to PageData struct
- Keeps original `Skills` field for backward compatibility
### 4. Updated Template Metadata
**Modified:** [template_metadata.go](backend/pdfrender/template_metadata.go)
- Changed `MaxItems` from 32 to 29 for both skill columns
- Now matches the template comment `<!-- MAX: 29 -->`
- Ensures pagination uses correct capacity
### 5. Fixed All Tests
Updated tests to use the new pagination workflow:
**Integration Tests:**
- `TestIntegration_FullPDFGeneration`: Now uses `PreparePaginatedPageData`
- `TestIntegration_PaginationWithPDF`: Uses SkillsColumn1/Column2
- `TestIntegration_TemplateMetadata`: Expects MAX=29
- `TestVisualInspection_AllPages`: Uses helper for all 4 pages
**Pagination Tests:**
- `TestPaginateSkills_MultiColumn`: Expects 29+11 instead of 32+8
- `TestPaginateSkills_MultiPage`: Expects 58/42 split instead of 64/36
- `TestCalculatePagesNeeded`: Updated 64→58 capacity test case
**Template Tests:**
- `TestRenderTemplate_WithSkills`: Now uses SkillsColumn1/Column2
### 6. Created Comprehensive Tests
**New file:** [pagination_helper_test.go](backend/pdfrender/pagination_helper_test.go)
- Tests for all 4 page types
- Validates capacity limits are enforced
- Tests column splitting logic with edge cases
## Results
### Test Coverage
- **All 46 tests passing**
- Added 5 new tests for pagination helper
- Updated 8 existing tests to use new workflow
### Capacity Enforcement
Page1 (Stats):
- Column 1: MAX 29 skills
- Column 2: MAX 29 skills
- Total: 58 skills per page
Page2 (Play):
- Weapons: MAX 30
- Skills: Various categories with separate limits
Page3 (Spells):
- Spells: MAX 24 (12+12)
- Magic Items: MAX 5
Page4 (Equipment):
- Equipment: MAX 20
### Generated Output
Test output available in `/tmp/bamort_pdf_test/`:
- `page1_stats.pdf` - 528KB
- `page2_play.pdf` - 531KB
- `page3_spell.pdf` - 531KB
- `page4_equip.pdf` - 512KB
- `character_sheet_complete.pdf` - 612KB (merged)
## Impact
### Before
```go
// Old approach - no pagination
pageData := &PageData{
Skills: viewModel.Skills, // All skills passed directly
}
```
- All 32 skills rendered in first column
- Second column empty
- Changing MAX in template had no effect
### After
```go
// New approach - with pagination
pageData, err := PreparePaginatedPageData(viewModel, "page1_stats.html", 1, "18.12.2025")
// pageData.SkillsColumn1 has 29 skills
// pageData.SkillsColumn2 has 3 skills (32 total - 29 = 3 remaining)
```
- Skills properly split: 29 in column 1, 3 in column 2
- MAX value in metadata controls distribution
- Pagination system now fully integrated
## Files Modified
1. `backend/pdfrender/pagination_helper.go` - NEW
2. `backend/pdfrender/pagination_helper_test.go` - NEW
3. `backend/pdfrender/viewmodel.go` - Added SkillsColumn1/Column2 fields
4. `backend/pdfrender/template_metadata.go` - Updated MAX values
5. `backend/templates/Default_A4_Quer/page1_stats.html` - Use column-specific data
6. `backend/pdfrender/integration_test.go` - Use pagination helper
7. `backend/pdfrender/pagination_test.go` - Updated expectations
8. `backend/pdfrender/templates_test.go` - Use column-specific data
## Next Steps
The pagination system is now fully functional. Future enhancements could include:
1. Auto-generate multiple pages when data exceeds one page capacity
2. Add overflow indicators (e.g., "Continued on next page")
3. Support for different page layouts with varying column counts
4. Template-driven pagination rules in HTML comments