Compare commits

...

10 commits
v0.1.1 ... main

Author SHA1 Message Date
e67cc692ba refactor: replace interface with any 2025-06-20 13:21:08 +02:00
Patryk Hegenberg
445455f341 added README.md 2024-03-07 08:14:27 +01:00
Patryk Hegenberg
206b5bb506 add everything necessary to be able to calculate skills interactive.
added new route to main to handle skill field update
added new handler to work with htmx requests for skill calculation
made skill fields readonly
adapted templates for reloading of skills only
2023-12-12 20:56:55 +01:00
Patryk Hegenberg
7ed139ff97 frontend: add forgotten resistances to resulttable 2023-12-07 16:00:37 +01:00
Patryk Hegenberg
1a4ac687f4 backend: frontend: fix errors in model struct and add new fields to result table view 2023-12-07 15:55:47 +01:00
Patryk Hegenberg
0fd2775aa3 backend: frontend: add missing fields 2023-12-07 13:59:46 +01:00
Patryk Hegenberg
d50f5dab03 backend: frontend: added new fields to model structs and frontend form 2023-12-07 13:24:57 +01:00
Patryk Hegenberg
eda80cf0f0 backend: added unit tests for main.go
Unit tests are testing all routes and Handlers
2023-12-07 08:14:03 +01:00
Patryk Hegenberg
2d0d0697fb change route handling to ServeMux for better performance and readability 2023-12-07 08:08:04 +01:00
Patryk Hegenberg
3a77eb1593 backend: frontend: cleaned up the codebase and added more logging for debuging 2023-12-06 17:24:33 +01:00
21 changed files with 1133 additions and 259 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
dist/ dist/
test_data/

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# ddServer
This is a simple Webserver, which helps zou creating monsters for a dungeons & dragons game
## Installation
Either clone the repo and build it your self.
- You need to have go installed for that
Or download the latest release and start your server with
```bash
./ddServer
```

8
go.mod
View file

@ -1,3 +1,11 @@
module ddServer module ddServer
go 1.21.4 go 1.21.4
require github.com/stretchr/testify v1.8.4
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

10
go.sum Normal file
View file

@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -7,21 +7,31 @@ import (
"net/http" "net/http"
) )
// AboutHandler returns an http.HandlerFunc that handles requests to the /about endpoint.
// It renders the about.html template and passes in the title "Dungeons & Dragons Monster Generator".
func AboutHandler(content embed.FS) http.HandlerFunc { func AboutHandler(content embed.FS) http.HandlerFunc {
log.Print("AboutHandler called") log.Print("AboutHandler called")
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html") log.Print("AboutHandler request received")
// Parse the template files
tmplFiles := []string{"templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html"}
tmpl, err := template.ParseFS(content, tmplFiles...)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
err = tmpl.ExecuteTemplate(w, "about", map[string]interface{}{ // Execute the template with the provided data
data := map[string]any{
"Title": "Dungeons & Dragons Monster Generator", "Title": "Dungeons & Dragons Monster Generator",
}) }
err = tmpl.ExecuteTemplate(w, "about", data)
if err != nil { if err != nil {
log.Printf("Template execution error: %v\n", err) log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusBadRequest)
} }
} }
} }

View file

@ -5,87 +5,140 @@ import (
"log" "log"
"net/http" "net/http"
"strconv" "strconv"
"strings"
) )
// AddMonster is a http.HandlerFunc that adds a new monster to the Monsters slice.
// It expects a POST request with form data containing the details of the monster.
// The monster is then appended to the Monsters slice and a redirect response is sent.
func AddMonster(Monsters *[]model.Monster) http.HandlerFunc { func AddMonster(Monsters *[]model.Monster) http.HandlerFunc {
log.Print("AddMonster called") log.Print("AddMonster called")
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO // Check if the request method is POST
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
log.Print("Method not allowed")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
// Parse the form data
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("Error parsing form data: %s", err.Error())
http.Error(w, err.Error(), http.StatusNoContent)
return return
} }
monster := model.Monster{ // Create a new monster with the form data
Name: r.FormValue("name"), monster := parseMonster(r)
Source: r.FormValue("source"),
Size: []string{r.FormValue("size")}, // Lock the Monsters slice, append the monster, and unlock the slice
Type: r.FormValue("type"),
Alignment: []string{r.FormValue("alignment")},
AC: []model.AC{
{
AC: parseInt(r.FormValue("ac")),
From: []string{r.FormValue("acFrom")},
},
},
HP: model.HP{
Average: parseInt(r.FormValue("hpAverage")),
Formula: r.FormValue("hpFormula"),
},
Speed: model.Speed{
Walk: parseInt(r.FormValue("speed")),
},
Str: parseInt(r.FormValue("str")),
Dex: parseInt(r.FormValue("dex")),
Con: parseInt(r.FormValue("con")),
Int: parseInt(r.FormValue("int")),
Wis: parseInt(r.FormValue("wis")),
Cha: parseInt(r.FormValue("cha")),
Save: model.Save{
Dex: r.FormValue("saveDex"),
Con: r.FormValue("saveCon"),
Wis: r.FormValue("saveWis"),
},
Skill: model.Skill{
Perception: r.FormValue("perception"),
Stealth: r.FormValue("stealth"),
},
DamageRes: []string{r.FormValue("damageRes")},
Senses: []string{r.FormValue("senses")},
Languages: []string{r.FormValue("languages")},
CR: r.FormValue("cr"),
Traits: []model.Trait{
{
Name: r.FormValue("traitName"),
Entries: []string{r.FormValue("traitEntry")},
},
},
Actions: []model.Action{
{
Name: r.FormValue("actionName"),
Entries: []string{r.FormValue("actionEntry")},
},
},
}
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
*Monsters = append(*Monsters, monster) *Monsters = append(*Monsters, monster)
log.Printf("Monster hinzugefügt. Anzahl der Monster jetzt: %d\n", len(*Monsters))
// Log the number of monsters and redirect to the monster table
log.Printf("Monster added. Number of monsters now: %d\n", len(*Monsters))
http.Redirect(w, r, "/monsterTable", http.StatusFound) http.Redirect(w, r, "/monsterTable", http.StatusFound)
} }
} }
// parseInt konvertiert einen String zu einem Integer und gibt 0 zurück, wenn die Konvertierung fehlschlägt // parseInt converts a string to an integer and returns 0 if the conversion fails
func parseInt(s string) int { func parseInt(s string) int {
// Add logging statement to print the input string
log.Println("Input string:", s)
// Atoi is used to convert the string to an integer
i, err := strconv.Atoi(s) i, err := strconv.Atoi(s)
// If there is an error in the conversion, return 0 and log the error
if err != nil { if err != nil {
log.Println("Conversion error:", err)
return 0 return 0
} }
// Log the converted integer
log.Println("Converted integer:", i)
// Return the converted integer
return i return i
} }
// parseMonster parses the Monster from monsterForm.html and return it.
func parseMonster(r *http.Request) model.Monster {
return model.Monster{
Name: r.FormValue("name"),
Source: r.FormValue("source"),
Size: []string{r.FormValue("size")},
Type: strings.ToLower(r.FormValue("type")),
Alignment: []string{r.FormValue("alignment")},
AC: []model.AC{
{
AC: parseInt(r.FormValue("ac")),
From: []string{r.FormValue("acFrom")},
},
},
HP: model.HP{
Average: parseInt(r.FormValue("hpAverage")),
Formula: r.FormValue("hpFormula"),
},
Speed: model.Speed{
Walk: parseInt(r.FormValue("walk")),
Burrow: parseInt(r.FormValue("burrow")),
Fly: parseInt(r.FormValue("fly")),
Swim: parseInt(r.FormValue("swim")),
Climb: parseInt(r.FormValue("climb")),
},
Str: parseInt(r.FormValue("str")),
Dex: parseInt(r.FormValue("dex")),
Con: parseInt(r.FormValue("con")),
Int: parseInt(r.FormValue("int")),
Wis: parseInt(r.FormValue("wis")),
Cha: parseInt(r.FormValue("cha")),
Save: model.Save{
Dex: r.FormValue("saveDex"),
Con: r.FormValue("saveCon"),
Wis: r.FormValue("saveWis"),
Str: r.FormValue("saveStr"),
Cha: r.FormValue("saveCha"),
Int: r.FormValue("saveInt"),
},
Skill: model.Skill{
Perception: r.FormValue("perception"),
Stealth: r.FormValue("stealth"),
Acrobatics: r.FormValue("acrobatics"),
AnimalHandling: r.FormValue("animalHandling"),
Arcana: r.FormValue("arcana"),
Athletics: r.FormValue("athletics"),
Deception: r.FormValue("deception"),
History: r.FormValue("history"),
Insight: r.FormValue("insight"),
Intimidation: r.FormValue("intimidation"),
Investigation: r.FormValue("investigation"),
Medicine: r.FormValue("medicine"),
Nature: r.FormValue("nature"),
Performance: r.FormValue("performance"),
Persuasion: r.FormValue("persuasion"),
SleightOfHand: r.FormValue("sleightOfHand"),
Survival: r.FormValue("survival"),
Religion: r.FormValue("religion"),
},
Resist: []string{r.FormValue("resist")},
ConditionImmune: []string{r.FormValue("conditionImmune")},
Immune: []string{r.FormValue("immune")},
Vulnerable: []string{r.FormValue("vulnerable")},
Senses: []string{r.FormValue("senses")},
Languages: []string{r.FormValue("languages")},
CR: r.FormValue("cr"),
Traits: []model.Trait{
{
Name: r.FormValue("traitName"),
Entries: []string{r.FormValue("traitEntry")},
},
},
Actions: []model.Action{
{
Name: r.FormValue("actionName"),
Entries: []string{r.FormValue("actionEntry")},
},
},
}
}

View file

@ -7,16 +7,23 @@ import (
"net/http" "net/http"
) )
// ContactHandler handles the contact page request.
// It takes the content embed.FS as a parameter and returns an http.HandlerFunc.
// The returned http.HandlerFunc renders the contact page using the provided templates.
func ContactHandler(content embed.FS) http.HandlerFunc { func ContactHandler(content embed.FS) http.HandlerFunc {
log.Print("ContactHandler called")
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Print("ContactHandler called")
// Parse the templates
tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html", "templates/contact.html") tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/about.html", "templates/contact.html")
if err != nil { if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = tmpl.ExecuteTemplate(w, "contact", map[string]interface{}{ // Execute the contact template
err = tmpl.ExecuteTemplate(w, "contact", map[string]any{
"Title": "Dungeons & Dragons Monster Generator", "Title": "Dungeons & Dragons Monster Generator",
}) })
if err != nil { if err != nil {

View file

@ -8,25 +8,49 @@ import (
"net/http" "net/http"
) )
func FormHandler(content embed.FS, monsters *[]model.Monster, filename string) http.HandlerFunc { // FormHandler returns an http.HandlerFunc that handles form submissions.
// It takes the content embed.FS, a pointer to a slice of model.Monster,
// and a filename string as parameters.
// The function parses the template files from the content FS,
// executes the template with the provided data, and renders it as a response.
func FormHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc {
log.Print("FormHandler called") log.Print("FormHandler called")
// Lock the mutex to ensure exclusive access to the monsters slice.
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/monsterForm.html", "templates/monster.html", "templates/monsterTable.html") log.Print("FormHandler handler called")
// Parse the template files.
templateFiles := []string{
"templates/base.html",
"templates/header.html",
"templates/main.html",
"templates/footer.html",
"templates/monsterForm.html",
"templates/monster.html",
"templates/skills.html",
"templates/monsterTable.html",
}
tmpl, err := template.ParseFS(content, templateFiles...)
if err != nil { if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = tmpl.ExecuteTemplate(w, "base", map[string]interface{}{ // Execute the template and render the response.
data := map[string]any{
"Title": "Dungeons & Dragons Monster Generator", "Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters, "Monsters": *monsters,
}) }
err = tmpl.ExecuteTemplate(w, "base", data)
if err != nil { if err != nil {
log.Printf("Template execution error: %v\n", err) log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
log.Printf("Template mit %d Monstern gerendert\n", len(*monsters)) log.Printf("Template rendered with %d Monsters\n", len(*monsters))
} }
} }

View file

@ -8,20 +8,27 @@ import (
"net/http" "net/http"
) )
// MainHandler // MainHandler handles the main HTTP request.
// It returns an http.HandlerFunc that renders the main page
// with the provided content and monsters.
func MainHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { func MainHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc {
log.Print("MainHandler called")
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Print("MainHandler called")
// Parse the templates from the embedded file system
tmpl, err := template.ParseFS(content, "templates/main.html", "templates/monsterForm.html", "templates/monster.html", "templates/monsterTable.html", "templates/base.html") tmpl, err := template.ParseFS(content, "templates/main.html", "templates/monsterForm.html", "templates/monster.html", "templates/monsterTable.html", "templates/base.html")
if err != nil { if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Lock the mutex to ensure exclusive access to the monsters slice
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
err = tmpl.ExecuteTemplate(w, "main", map[string]interface{}{ // Execute the main template with the provided data
err = tmpl.ExecuteTemplate(w, "main", map[string]any{
"Title": "Dungeons & Dragons Monster Generator", "Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters, "Monsters": *monsters,
}) })

View file

@ -8,16 +8,23 @@ import (
"net/http" "net/http"
) )
// MonsterTableHandler returns a http.HandlerFunc that handles requests to display a table of monsters.
func MonsterTableHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc { func MonsterTableHandler(content embed.FS, monsters *[]model.Monster) http.HandlerFunc {
log.Print("AboutHandler called") log.Print("MonsterTableHandler called")
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Print("Handling request for monster table")
// Parse the template files
tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/monsterTable.html", "templates/monster.html") tmpl, err := template.ParseFS(content, "templates/base.html", "templates/header.html", "templates/main.html", "templates/footer.html", "templates/monsterTable.html", "templates/monster.html")
if err != nil { if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]interface{}{ // Execute the template and pass the necessary data
err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]any{
"Title": "Dungeons & Dragons Monster Generator", "Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters, "Monsters": *monsters,
}) })

View file

@ -0,0 +1,142 @@
package handlers
import (
"embed"
"html/template"
"log"
"net/http"
"strconv"
)
// SkillCalculationHandler ist ein http.HandlerFunc, der von htmx getriggert wird,
// wenn der Benutzer Einträge in bestimmten Feldern macht, und dann die Skill-Felder befüllt.
func SkillCalculationHandler(content embed.FS) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Print("SkillCalculationHandler called")
// Überprüfen Sie, ob die Anfrage eine POST-Anfrage ist.
if r.Method != http.MethodPost {
log.Print("Method not allowed")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse Formulardaten.
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form data: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
tmplFiles := []string{"templates/base.html", "templates/header.html", "templates/skills.html", "templates/main.html", "templates/footer.html", "templates/about.html"}
tmpl, err := template.ParseFS(content, tmplFiles...)
if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
str := parseFieldValue(r.FormValue("str"))
dex := parseFieldValue(r.FormValue("dex"))
int := parseFieldValue(r.FormValue("int"))
cha := parseFieldValue(r.FormValue("cha"))
wis := parseFieldValue(r.FormValue("wis"))
cr := parseFieldValue(r.FormValue("cr"))
crBonus := calcBonus(cr)
skillValues := map[string]string{
"acrobatics": strconv.Itoa(calcAbilityScore(dex) + crBonus),
"animalHandling": strconv.Itoa(calcAbilityScore(wis) + crBonus),
"arcana": strconv.Itoa(calcAbilityScore(int) + crBonus),
"athletics": strconv.Itoa(calcAbilityScore(str) + crBonus),
"deception": strconv.Itoa(calcAbilityScore(cha) + crBonus),
"history": strconv.Itoa(calcAbilityScore(int) + crBonus),
"insight": strconv.Itoa(calcAbilityScore(wis) + crBonus),
"intimidation": strconv.Itoa(calcAbilityScore(cha) + crBonus),
"investigation": strconv.Itoa(calcAbilityScore(int) + crBonus),
"medicine": strconv.Itoa(calcAbilityScore(wis) + crBonus),
"nature": strconv.Itoa(calcAbilityScore(int) + crBonus),
"perception": strconv.Itoa(calcAbilityScore(wis) + crBonus),
"performance": strconv.Itoa(calcAbilityScore(cha) + crBonus),
"persuasion": strconv.Itoa(calcAbilityScore(cha) + crBonus),
"religion": strconv.Itoa(calcAbilityScore(int) + crBonus),
"sleightOfHand": strconv.Itoa(calcAbilityScore(dex) + crBonus),
"stealth": strconv.Itoa(calcAbilityScore(dex) + crBonus),
"survival": strconv.Itoa(calcAbilityScore(wis) + crBonus),
}
err = tmpl.ExecuteTemplate(w, "skills", skillValues)
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
}
}
}
func calcBonus(cr int) int {
if cr >= 0 && cr < 5 {
return 2
} else if cr >= 5 && cr < 9 {
return 3
} else if cr >= 9 && cr < 14 {
return 4
} else if cr >= 14 && cr < 18 {
return 5
} else if cr >= 18 && cr < 21 {
return 6
} else if cr >= 21 && cr < 25 {
return 7
} else if cr >= 25 && cr < 28 {
return 8
} else if cr >= 28 && cr < 31 {
return 9
} else {
return 0
}
}
func calcAbilityScore(val int) int {
if val < 2 {
return -5
} else if val >= 2 && val < 4 {
return -4
} else if val >= 4 && val < 6 {
return -3
} else if val >= 6 && val < 8 {
return -2
} else if val >= 8 && val < 10 {
return -1
} else if val >= 10 && val < 12 {
return 0
} else if val >= 12 && val < 14 {
return 1
} else if val >= 14 && val < 16 {
return 2
} else if val >= 16 && val < 18 {
return 3
} else if val >= 18 && val < 20 {
return 4
} else if val >= 20 && val < 22 {
return 5
} else if val >= 22 && val < 24 {
return 6
} else if val >= 24 && val < 26 {
return 7
} else if val >= 26 && val < 28 {
return 8
} else if val >= 28 && val < 30 {
return 9
} else {
return 10
}
}
func parseFieldValue(value string) int {
val, err := strconv.Atoi(value)
if err != nil {
log.Printf("Error converting field value to integer: %v", err)
return 0
}
return val
}

View file

@ -13,53 +13,59 @@ import (
var mu sync.Mutex var mu sync.Mutex
// submitHandler verarbeitet die Formulardaten // SubmitHandler processes the form data.
func SubmitHandler(content embed.FS, chars *[]model.Character, Monsters *[]model.Monster, filename string) http.HandlerFunc { func SubmitHandler(content embed.FS, chars *[]model.Character, Monsters *[]model.Monster, filename string) http.HandlerFunc {
log.Print("SubmitHandler called") log.Print("SubmitHandler called")
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
log.Print("SubmitHandler called")
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return return
} }
// Formulardaten parsen // Parse form data.
err := r.ParseForm() err := r.ParseForm()
if err != nil { if err != nil {
log.Printf("Error parsing form data: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Monster-Objekt erstellen // Create monster object.
filename := r.FormValue("filename") filename := r.FormValue("filename")
// Charakter-Objekt erstellen oder aktualisieren // Create or update character object.
mu.Lock() mu.Lock()
defer mu.Unlock() defer mu.Unlock()
char := model.GetOrCreateCharacter(filename, *chars) char := model.GetOrCreateCharacter(filename, *chars)
char.Monster = append(char.Monster, *Monsters...) char.Monster = append(char.Monster, *Monsters...)
// Charakterdaten in JSON umwandeln // Convert character data to JSON.
charJSON, err := json.Marshal(char) charJSON, err := json.Marshal(char)
if err != nil { if err != nil {
log.Printf("Error marshalling character data to JSON: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// JSON-Daten in die Datei schreiben // Write JSON data to file.
err = model.WriteToFile(filename, charJSON) err = model.WriteToFile(filename, charJSON)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) log.Printf("Error writing JSON data to file: %v", err)
return
}
// Dateiinhalt lesen
fileContent, err := os.ReadFile(filename)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
} }
// Datei zum Download anbieten // Read file contents.
fileContent, err := os.ReadFile(filename)
if err != nil {
log.Printf("Error reading file contents: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Offer file for download.
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename)) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Write(fileContent) w.Write(fileContent)

42
main.go
View file

@ -20,40 +20,54 @@ var (
Monsters []model.Monster Monsters []model.Monster
) )
// main is the entry point of the program.
func main() { func main() {
filename := "" filename := ""
// Create a new ServeMux instance
routes := http.NewServeMux()
// Register the handlers for different routes
routes.HandleFunc("/", handlers.FormHandler(content, &Monsters))
routes.HandleFunc("/submit", handlers.SubmitHandler(content, &chars, &Monsters, filename))
routes.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.FS(content))))
routes.HandleFunc("/addMonster", handlers.AddMonster(&Monsters))
routes.HandleFunc("/main", handlers.MainHandler(content, &Monsters))
routes.HandleFunc("/about", handlers.AboutHandler(content))
routes.HandleFunc("/contact", handlers.ContactHandler(content))
routes.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters))
routes.HandleFunc("/calculate-skills", handlers.SkillCalculationHandler(content))
// Print the message indicating that 'static' has been included.
log.Printf("Eingebunden is %v\n", static) log.Printf("Eingebunden is %v\n", static)
http.HandleFunc("/", handlers.FormHandler(content, &Monsters, filename)) // Load the CSS file.
http.HandleFunc("/submit", handlers.SubmitHandler(content, &chars, &Monsters, filename))
http.Handle("/images/", http.StripPrefix("/images/", http.FileServer(http.FS(content))))
http.HandleFunc("/addMonster", handlers.AddMonster(&Monsters))
http.HandleFunc("/main", handlers.MainHandler(content, &Monsters))
http.HandleFunc("/about", handlers.AboutHandler(content))
http.HandleFunc("/contact", handlers.ContactHandler(content))
http.HandleFunc("/monsterTable", handlers.MonsterTableHandler(content, &Monsters))
// Lade die CSS-Datei
css, err := loadCSS(static) css, err := loadCSS(static)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
// Füge eine Route für die CSS-Datei hinzu // Add a route for the CSS file
http.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) { routes.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css") w.Header().Set("Content-Type", "text/css")
w.Write([]byte(css)) w.Write([]byte(css))
}) })
// Print the message indicating that the server has started.
log.Print("Server gestartet, erreichbar unter http://localhost:8080") log.Print("Server gestartet, erreichbar unter http://localhost:8080")
http.ListenAndServe(":8080", nil)
// Start the server and listen for incoming requests on port 8080.
log.Fatal(http.ListenAndServe(":8080", routes))
} }
// loadCSS liest die CSS-Datei aus dem eingebetteten Dateisystem. // loadCSS reads the CSS file from the embedded filesystem.
// It takes the content embed.FS as input.
// It returns the content of the CSS file as a string and an error if any.
func loadCSS(content embed.FS) (string, error) { func loadCSS(content embed.FS) (string, error) {
// Read the CSS file "static/darkly_bulmawatch.css" from the embedded filesystem
file, err := content.ReadFile("static/darkly_bulmawatch.css") file, err := content.ReadFile("static/darkly_bulmawatch.css")
if err != nil { if err != nil {
return "", err return "", err
} }
// Convert the file content to a string and return
return string(file), nil return string(file), nil
} }

176
main_test.go Normal file
View file

@ -0,0 +1,176 @@
package main
import (
"ddServer/handlers"
"log"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMain(t *testing.T) {
// Test case 1: Check if the root route ("/") returns the expected response.
t.Run("Root Route", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.FormHandler(content, &Monsters))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
// Test case 2: Check if the "/submit" route returns the expected response.
t.Run("Submit Route", func(t *testing.T) {
dir, err := filepath.Abs("test_data")
if err != nil {
log.Fatal(err)
}
filename := filepath.Join(dir, "monster.json")
EnsureDirExists(dir)
formData := url.Values{
"filename": {filename},
"name": {"Monster Name"},
"source": {"Monster Source"},
"size": {"Monster Size"},
"type": {"Monster Type"},
"alignment": {"Monster Alignment"},
"ac": {"15"}, // Beispielwert für AC
"acFrom": {"Natural Armor"}, // Beispielwert für AC From
"hpAverage": {"30"}, // Beispielwert für HP Average
"hpFormula": {"2d10+5"}, // Beispielwert für HP Formula
"speed": {"30"}, // Beispielwert für Speed
"str": {"16"}, // Beispielwert für Str
"dex": {"14"}, // Beispielwert für Dex
"con": {"18"}, // Beispielwert für Con
"int": {"10"}, // Beispielwert für Int
"wis": {"12"}, // Beispielwert für Wis
"cha": {"8"}, // Beispielwert für Cha
"saveDex": {"+2"}, // Beispielwert für Save Dex
"saveCon": {"+4"}, // Beispielwert für Save Con
"saveWis": {"+1"}, // Beispielwert für Save Wis
"perception": {"+3"}, // Beispielwert für Perception
"stealth": {"+2"}, // Beispielwert für Stealth
"damageRes": {"Fire, Cold"}, // Beispielwert für Damage Resistances
"senses": {"Darkvision"}, // Beispielwert für Senses
"languages": {"Common"}, // Beispielwert für Languages
"cr": {"2"}, // Beispielwert für CR
"traitName": {"Trait Name"}, // Beispielwert für Trait Name
"traitEntry": {"Trait Entry"}, // Beispielwert für Trait Entry
"actionName": {"Action Name"}, // Beispielwert für Action Name
"actionEntry": {"Action Entry"}, // Beispielwert für Action Entry
}
log.Println("Writing data to file:", filename)
req, _ := http.NewRequest("POST", "/submit", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.SubmitHandler(content, &chars, &Monsters, filename))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
// Test case 3: Check if the "/images/" route returns the expected response.
t.Run("Images Route", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/images/", nil)
rr := httptest.NewRecorder()
handler := http.StripPrefix("/images/", http.FileServer(http.FS(content)))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
// Test case 4: Check if the "/addMonster" route returns the expected response.
t.Run("AddMonster Route", func(t *testing.T) {
dir, err := filepath.Abs("test_data")
if err != nil {
log.Fatal(err)
}
filename := filepath.Join(dir, "monster.json")
EnsureDirExists(dir)
formData := url.Values{
"filename": {filename},
"name": {"Monster Name"},
"source": {"Monster Source"},
"size": {"Monster Size"},
"type": {"Monster Type"},
"alignment": {"Monster Alignment"},
"ac": {"15"}, // Beispielwert für AC
"acFrom": {"Natural Armor"}, // Beispielwert für AC From
"hpAverage": {"30"}, // Beispielwert für HP Average
"hpFormula": {"2d10+5"}, // Beispielwert für HP Formula
"speed": {"30"}, // Beispielwert für Speed
"str": {"16"}, // Beispielwert für Str
"dex": {"14"}, // Beispielwert für Dex
"con": {"18"}, // Beispielwert für Con
"int": {"10"}, // Beispielwert für Int
"wis": {"12"}, // Beispielwert für Wis
"cha": {"8"}, // Beispielwert für Cha
"saveDex": {"+2"}, // Beispielwert für Save Dex
"saveCon": {"+4"}, // Beispielwert für Save Con
"saveWis": {"+1"}, // Beispielwert für Save Wis
"perception": {"+3"}, // Beispielwert für Perception
"stealth": {"+2"}, // Beispielwert für Stealth
"damageRes": {"Fire, Cold"}, // Beispielwert für Damage Resistances
"senses": {"Darkvision"}, // Beispielwert für Senses
"languages": {"Common"}, // Beispielwert für Languages
"cr": {"2"}, // Beispielwert für CR
"traitName": {"Trait Name"}, // Beispielwert für Trait Name
"traitEntry": {"Trait Entry"}, // Beispielwert für Trait Entry
"actionName": {"Action Name"}, // Beispielwert für Action Name
"actionEntry": {"Action Entry"}, // Beispielwert für Action Entry
}
req, _ := http.NewRequest("POST", "/addMonster", strings.NewReader(formData.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.SubmitHandler(content, &chars, &Monsters, filename))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
// Test case 5: Check if the "/main" route returns the expected response.
t.Run("Main Route", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/main", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.MainHandler(content, &Monsters))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
// Test case 6: Check if the "/about" route returns the expected response.
t.Run("About Route", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/about", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.AboutHandler(content))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
// Test case 7: Check if the "/contact" route returns the expected response.
t.Run("Contact Route", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/contact", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(handlers.ContactHandler(content))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
})
}
func EnsureDirExists(dir string) error {
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
log.Println("Error creating directory:", err)
}
return err
}

View file

@ -2,34 +2,38 @@ package model
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"time" "time"
) )
// Monster struct für die Daten des Monsters // Monster struct für die Daten des Monsters
type Monster struct { type Monster struct {
Save Save `json:"save"` Save Save `json:"save"`
Skill Skill `json:"skill"` Skill Skill `json:"skill"`
HP HP `json:"hp"` HP HP `json:"hp"`
Source string `json:"source"` Source string `json:"source"`
CR string `json:"cr"` CR string `json:"cr"`
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
DamageRes []string `json:"damageResistances"` Vulnerable []string `json:"vulnerable"`
Traits []Trait `json:"trait"` ConditionImmune []string `json:"conditionImmune"`
AC []AC `json:"ac"` Resist []string `json:"resist"`
Alignment []string `json:"alignment"` Immune []string `json:"immune"`
Senses []string `json:"senses"` Traits []Trait `json:"trait"`
Languages []string `json:"languages"` AC []AC `json:"ac"`
Size []string `json:"size"` Alignment []string `json:"alignment"`
Actions []Action `json:"action"` Senses []string `json:"senses"`
Speed Speed `json:"speed"` Languages []string `json:"languages"`
Str int `json:"str"` Size []string `json:"size"`
Dex int `json:"dex"` Actions []Action `json:"action"`
Con int `json:"con"` Speed Speed `json:"speed"`
Int int `json:"int"` Str int `json:"str"`
Wis int `json:"wis"` Dex int `json:"dex"`
Cha int `json:"cha"` Con int `json:"con"`
Int int `json:"int"`
Wis int `json:"wis"`
Cha int `json:"cha"`
} }
type AC struct { type AC struct {
@ -43,18 +47,41 @@ type HP struct {
} }
type Speed struct { type Speed struct {
Walk int `json:"walk"` Walk int `json:"walk"`
Burrow int `json:"burrow"`
Climb int `json:"climb"`
Fly int `json:"fly"`
Swim int `json:"swim"`
} }
type Save struct { type Save struct {
Dex string `json:"dex"` Dex string `json:"dex"`
Con string `json:"con"` Con string `json:"con"`
Wis string `json:"wis"` Wis string `json:"wis"`
Cha string `json:"cha"`
Str string `json:"str"`
Int string `json:"int"`
} }
type Skill struct { type Skill struct {
Perception string `json:"perception"` Stealth string `json:"stealth"`
Stealth string `json:"stealth"` Acrobatics string `json:"acrobatics"`
AnimalHandling string `json:"animalHandling"`
Arcana string `json:"arcana"`
Athletics string `json:"athletics"`
Deception string `json:"deception"`
History string `json:"history"`
Insight string `json:"insight"`
Intimidation string `json:"intimidation"`
Investigation string `json:"investigation"`
Medicine string `json:"medicine"`
Nature string `json:"nature"`
Perception string `json:"perception"`
Performance string `json:"performance"`
Persuasion string `json:"persuasion"`
SleightOfHand string `json:"sleightOfHand"`
Survival string `json:"survival"`
Religion string `json:"religion"`
} }
type Trait struct { type Trait struct {
@ -89,32 +116,47 @@ type Source struct {
ConvertedBy []string `json:"convertedBy"` ConvertedBy []string `json:"convertedBy"`
} }
// writeToFile schreibt Daten in eine Datei // WriteToFile writes data to a file.
// It takes in a filename string and a data byte slice.
// It returns an error if there was an issue writing to the file, otherwise it returns nil.
func WriteToFile(filename string, data []byte) error { func WriteToFile(filename string, data []byte) error {
log.Println("Writing data to file:", filename)
// Create a file with the given filename
file, err := os.Create(filename) file, err := os.Create(filename)
if err != nil { if err != nil {
log.Println("Error creating file:", err)
return err return err
} }
defer file.Close() defer func() {
if err := file.Close(); err != nil {
log.Println("Error closing file:", err)
}
}()
_, err = file.Write(data) // Write the data to the file
n, err := file.Write(data)
if err != nil { if err != nil {
log.Println("Error writing to file:", err)
return err return err
} }
log.Printf("Successfully wrote %d bytes to file", n)
return nil return nil
} }
// getOrCreateCharacter gibt das aktuelle Charakterobjekt zurück oder erstellt ein neues // getOrCreateCharacter returns the current character object or creates a new one
func GetOrCreateCharacter(filename string, chars []Character) Character { func GetOrCreateCharacter(filename string, chars []Character) Character {
// Check if there is an empty character object
for _, char := range chars { for _, char := range chars {
if char.Meta.DateLastModified == 0 { if char.Meta.DateLastModified == 0 {
// Ein leeres Charakterobjekt wurde gefunden // Return the empty character object
log.Println("Returning existing character object")
return char return char
} }
} }
// Erstelle ein neues Charakterobjekt // Create a new character object
now := time.Now().Unix() now := time.Now().Unix()
newChar := Character{ newChar := Character{
Meta: Meta{ Meta: Meta{
@ -134,7 +176,10 @@ func GetOrCreateCharacter(filename string, chars []Character) Character {
Monster: []Monster{}, Monster: []Monster{},
} }
// Append the new character object to the list of characters
chars = append(chars, newChar) chars = append(chars, newChar)
// Return the newly created character object
log.Println("Returning newly created character object")
return newChar return newChar
} }

View file

@ -1,15 +1,19 @@
{{ define "about" }} {{ define "about" }}
<!-- Start of about section -->
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child card "> <div class="tile is-child card ">
<div class="card-content"> <div class="card-content">
<div class="media-content"> <div class="media-content">
<!-- Title -->
<p class="title is-4">About Us</p> <p class="title is-4">About Us</p>
</div> </div>
<div class="content"> <div class="content">
<!-- Description -->
<p>Welcome to the Dungeons and Dragons Monster Generator website! We are a team of enthusiasts...</p> <p>Welcome to the Dungeons and Dragons Monster Generator website! We are a team of enthusiasts...</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- End of about section -->
</div> </div>
{{ end }} {{ end }}

View file

@ -13,7 +13,6 @@
<ul> <ul>
<li>Email: example@example.com</li> <li>Email: example@example.com</li>
<li>Phone: +123456789</li> <li>Phone: +123456789</li>
<!-- Add more contact information -->
</ul> </ul>
</div> </div>

View file

@ -1,83 +1,109 @@
{{ define "main" }} {{ define "main" }}
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child card "> <div class="tile is-child card ">
<div class="card-content"> <div class="card-content">
<div class="media-content"> <div class="media-content">
<p class="title is-4">Monster Form</p> <p class="title is-4">Monster Form</p>
</div>
<div class="content">
<form action="/submit" method="post" class="">
<div class="field">
<td><label for="filename">Filename:</label></td>
<div class="control"><input type="text" name="filename" required placeholder="Dateiname"
class="input input-bordered w-full max-w-xs">
</div> </div>
</div> <div class="content">
<div class="field"> <form action="/submit" method="post" class="">
<div class="control"> <div class="field">
<button type="button" hx-post="/addMonster" hx-trigger="click" hx-target="#monster-table" <td><label for="filename">Filename:</label></td>
hx-swap="outerHTML" class="button is-info" hx-boost="true">Add <div class="control"><input type="text" name="filename" required placeholder="Dateiname"
Monster</button> class="input input-bordered w-full max-w-xs">
</div>
</div>
<div class="field">
<div class="control">
<button type="button" hx-post="/addMonster" hx-trigger="click" hx-target="#monster-table"
hx-swap="outerHTML" class="button is-info" hx-boost="true">Add
Monster</button>
</div>
</div>
{{ template "monsterform" . }}
<input type="hidden" name="filename" value="{{.Filename}}">
<input type="submit" value="Submit" class="button is-primary">
</form>
</div> </div>
</div> </div>
{{ template "monsterform" . }}
<input type="hidden" name="filename" value="{{.Filename}}">
<input type="submit" value="Submit" class="button is-primary">
</form>
</div>
</div> </div>
</div>
</div> </div>
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child card"> <div class="tile is-child card">
<div class="card-content"> <div class="card-content">
<div class="media-content"> <div class="media-content">
<p class="title is-4 is-centered">Existing Monsters</p> <p class="title is-4 is-centered">Existing Monsters</p>
</div> </div>
<div class="table-container"> <div class="table-container">
<table class="table"> <table class="table">
<colgroup> <colgroup>
<col style="width: 100px;"> <col style="width: 100px;">
<col style="width: 100px;"> <col style="width: 100px;">
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Source</th> <th>Source</th>
<th>Size</th> <th>Size</th>
<th>Type</th> <th>Type</th>
<th>Alignment</th> <th>Alignment</th>
<th>AC</th> <th>AC</th>
<th>AC Form</th> <th>AC Form</th>
<th>HP Average</th> <th>HP Average</th>
<th>HP Formula</th> <th>HP Formula</th>
<th>Speed</th> <th>Walk</th>
<th>Str</th> <th>Swim</th>
<th>Dex</th> <th>Burrow</th>
<th>Con</th> <th>Climb</th>
<th>Int</th> <th>Fly</th>
<th>Wis</th> <th>Str</th>
<th>Cha</th> <th>Dex</th>
<th>Save Dex</th> <th>Con</th>
<th>Save Con</th> <th>Int</th>
<th>Savve Wis</th> <th>Wis</th>
<th>Perception</th> <th>Cha</th>
<th>Stealth</th> <th>Save Dex</th>
<th>Damage Resistance</th> <th>Save Con</th>
<th>Senses</th> <th>Save Wis</th>
<th>Languages</th> <th>Save Str</th>
<th>CR</th> <th>Save Con</th>
<th>Trait Name</th> <th>Save Cha</th>
<th>Trait Entry</th> <th>Perception</th>
<th>Action Name</th> <th>Stealth</th>
<th>Action Entry</th> <th>Acrobatics</th>
</tr> <th>AnimalHandling</th>
</thead> <th>Arcana</th>
{{ template "monsterTable" }} <th>Athletics</th>
</table> <th>Deception</th>
</div> <th>History</th>
<th>Insight</th>
<th>Intimidation</th>
<th>Investigation</th>
<th>Medicine</th>
<th>Nature</th>
<th>Performance</th>
<th>Persuasion</th>
<th>SleightOfHand</th>
<th>Survival</th>
<th>Religion</th>
<th>Damage Resistance</th>
<th>Damage Immune</th>
<th>Vulnerable</th>
<th>Condition Immune</th>
<th>Senses</th>
<th>Languages</th>
<th>CR</th>
<th>Trait Name</th>
<th>Trait Entry</th>
<th>Action Name</th>
<th>Action Entry</th>
</tr>
</thead>
{{ template "monsterTable" }}
</table>
</div>
</div>
</div> </div>
</div>
</div> </div>
</div> </div>
{{ end }} {{ end }}

View file

@ -10,6 +10,10 @@
<td>{{.HP.Average}}</td> <td>{{.HP.Average}}</td>
<td>{{.HP.Formula}}</td> <td>{{.HP.Formula}}</td>
<td>{{.Speed.Walk}}</td> <td>{{.Speed.Walk}}</td>
<td>{{.Speed.Swim}}</td>
<td>{{.Speed.Burrow}}</td>
<td>{{.Speed.Climb}}</td>
<td>{{.Speed.Fly}}</td>
<td>{{.Str}}</td> <td>{{.Str}}</td>
<td>{{.Dex}}</td> <td>{{.Dex}}</td>
<td>{{.Con}}</td> <td>{{.Con}}</td>
@ -19,9 +23,31 @@
<td>{{.Save.Dex}}</td> <td>{{.Save.Dex}}</td>
<td>{{.Save.Con}}</td> <td>{{.Save.Con}}</td>
<td>{{.Save.Wis}}</td> <td>{{.Save.Wis}}</td>
<td>{{.Save.Str}}</td>
<td>{{.Save.Con}}</td>
<td>{{.Save.Cha}}</td>
<td>{{.Skill.Perception}}</td> <td>{{.Skill.Perception}}</td>
<td>{{.Skill.Stealth}}</td> <td>{{.Skill.Stealth}}</td>
<td>{{range .DamageRes}}{{.}}{{end}}</td> <td>{{.Skill.Acrobatics}}</td>
<td>{{.Skill.AnimalHandling}}</td>
<td>{{.Skill.Arcana}}</td>
<td>{{.Skill.Athletics}}</td>
<td>{{.Skill.Deception}}</td>
<td>{{.Skill.History}}</td>
<td>{{.Skill.Insight}}</td>
<td>{{.Skill.Intimidation}}</td>
<td>{{.Skill.Investigation}}</td>
<td>{{.Skill.Medicine}}</td>
<td>{{.Skill.Nature}}</td>
<td>{{.Skill.Performance}}</td>
<td>{{.Skill.Persuasion}}</td>
<td>{{.Skill.SleightOfHand}}</td>
<td>{{.Skill.Survival}}</td>
<td>{{.Skill.Religion}}</td>
<td>{{range .Resist}}{{.}}{{end}}</td>
<td>{{range .Immune}}{{.}}{{end}}</td>
<td>{{range .Vulnerable}}{{.}}{{end}}</td>
<td>{{range .ConditionImmune}}{{.}}{{end}}</td>
<td>{{range .Senses}}{{.}}{{end}}</td> <td>{{range .Senses}}{{.}}{{end}}</td>
<td>{{range .Languages}}{{.}}{{end}}</td> <td>{{range .Languages}}{{.}}{{end}}</td>
<td>{{.CR}}</td> <td>{{.CR}}</td>

View file

@ -20,7 +20,15 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="size">Size:</label> <label for="size">Size:</label>
<div class="control"> <div class="control">
<input type="text" name="size" required class="input input-bordered w-full max-w-xs"> <div class="select">
<select name="size">
<option>H</option>
<option>T</option>
<option>S</option>
<option>L</option>
<option>G</option>
</select>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -79,12 +87,57 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tile is-ancestor"> <div class="card tile is-ancestor is-vertical">
<div class="tile is-parent"> <header class="card-header">
<div class="tile is-child field"> <p class="card-header-title">
<label for="speed">Speed:</label> Speed
<div class="control"> </p>
<input type="number" name="speed" required class="input input-bordered w-full max-w-xs"> </header>
<div class="card-content">
<div class="content">
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="walk">Walk:</label>
<div class="control">
<input type="number" name="walk" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="burrow">Burrow:</label>
<div class="control">
<input type="number" name="burrow" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="climb">Climb:</label>
<div class="control">
<input type="number" name="climb" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="fly">Fly:</label>
<div class="control">
<input type="number" name="fly" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="swim">Swim:</label>
<div class="control">
<input type="number" name="swim" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -94,7 +147,8 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="str">Str:</label> <label for="str">Str:</label>
<div class="control"> <div class="control">
<input type="number" name="str" required class="input input-bordered w-full max-w-xs"> <input type="number" name="str" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div> </div>
</div> </div>
</div> </div>
@ -102,7 +156,8 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="dex">Dex:</label> <label for="dex">Dex:</label>
<div class="control"> <div class="control">
<input type="number" name="dex" required class="input input-bordered w-full max-w-xs"> <input type="number" name="dex" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div> </div>
</div> </div>
</div> </div>
@ -110,7 +165,18 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="con">Con:</label> <label for="con">Con:</label>
<div class="control"> <div class="control">
<input type="number" name="con" required class="input input-bordered w-full max-w-xs"> <input type="number" name="con" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="cr">CR:</label>
<div class="control">
<input type="text" name="cr" required class="input input-bordered w-full max-w-xs" hx-post="/calculate-skills"
hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML" hx-target="#skills"
hx-swap="outerHTML">
</div> </div>
</div> </div>
</div> </div>
@ -120,7 +186,8 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="int">Int:</label> <label for="int">Int:</label>
<div class="control"> <div class="control">
<input type="number" name="int" required class="input input-bordered w-full max-w-xs"> <input type="number" name="int" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div> </div>
</div> </div>
</div> </div>
@ -128,7 +195,8 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="wis">Wis:</label> <label for="wis">Wis:</label>
<div class="control"> <div class="control">
<input type="number" name="wis" required class="input input-bordered w-full max-w-xs"> <input type="number" name="wis" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div> </div>
</div> </div>
</div> </div>
@ -136,63 +204,117 @@
<div class="tile is-child field"> <div class="tile is-child field">
<label for="cha">Cha:</label> <label for="cha">Cha:</label>
<div class="control"> <div class="control">
<input type="number" name="cha" required class="input input-bordered w-full max-w-xs"> <input type="number" name="cha" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="passive">Passive:</label>
<div class="control">
<input type="number" name="passive" required class="input input-bordered w-full max-w-xs"
hx-post="/calculate-skills" hx-trigger="keyup changed delay:100ms" hx-target="#skills" hx-swap="innerHTML">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="tile is-ancestor"> <div class="card tile is-ancestor is-vertical">
<div class="tile is-parent gap"> <header class="card-header">
<div class="tile is-child field"> <p class="card-header-title">
<label for="saveDex">Save Dex:</label> Save
<div class="control"> </p>
<input type="text" name="saveDex" required class="input input-bordered w-full max-w-xs"> </header>
</div> <div class="card-content">
</div> <div class="content">
</div> <div class="tile is-ancestor">
<div class="tile is-parent gap"> <div class="tile is-parent gap">
<div class="tile is-child field"> <div class="tile is-child field">
<label for="saveCon">Save Con:</label> <label for="saveDex">Dex:</label>
<div class="control"> <div class="control">
<input type="text" name="saveCon" required class="input input-bordered w-full max-w-xs"> <input type="text" name="saveDex" required class="input input-bordered w-full max-w-xs">
</div> </div>
</div> </div>
</div> </div>
<div class="tile is-parent gap"> <div class="tile is-parent gap">
<div class="tile is-child field"> <div class="tile is-child field">
<label for="saveWis">Save Wis:</label> <label for="saveCon">Con:</label>
<div class="control"> <div class="control">
<input type="text" name="saveWis" required class="input input-bordered w-full max-w-xs"> <input type="text" name="saveCon" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveWis">Wis:</label>
<div class="control">
<input type="text" name="saveWis" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveCha">Cha:</label>
<div class="control">
<input type="text" name="saveCha" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveInt">Int:</label>
<div class="control">
<input type="text" name="saveInt" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveStr">Str:</label>
<div class="control">
<input type="text" name="saveStr" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div id="skills" class="card tile is-ancestor is-vertical">
{{ template "skills" }}
</div>
<div class="tile is-ancestor"> <div class="tile is-ancestor">
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child field"> <div class="tile is-child field">
<label for="perception">Perception:</label> <label for="resist">Damage Resistances:</label>
<div class="control"> <div class="control">
<input type="text" name="perception" required class="input input-bordered w-full max-w-xs"> <input type="text" name="resist" required class="input input-bordered w-full max-w-xs">
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child field"> <div class="tile is-child field">
<label for="stealth">Stealth:</label> <label for="immune">Damage Immunity:</label>
<div class="control"> <div class="control">
<input type="text" name="stealth" required class="input input-bordered w-full max-w-xs"> <input type="text" name="immune" required class="input input-bordered w-full max-w-xs">
</div> </div>
</div> </div>
</div> </div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child field"> <div class="tile is-child field">
<label for="damageRes">Damage Resistances:</label> <label for="vulnerable">Vulnerability:</label>
<div class="control"> <div class="control">
<input type="text" name="damageRes" required class="input input-bordered w-full max-w-xs"> <input type="text" name="vulnerable" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="conditionImmune">Condition Immunity:</label>
<div class="control">
<input type="text" name="conditionImmune" required class="input input-bordered w-full max-w-xs">
</div> </div>
</div> </div>
</div> </div>
@ -217,16 +339,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="cr">CR:</label>
<div class="control">
<input type="text" name="cr" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-ancestor"> <div class="tile is-ancestor">
<div class="tile is-parent"> <div class="tile is-parent">
<div class="tile is-child field"> <div class="tile is-child field">

185
templates/skills.html Normal file
View file

@ -0,0 +1,185 @@
{{ define "skills" }}
<header class="card-header">
<p class="card-header-title">
Skill
</p>
</header>
<div class="card-content">
<div class="content">
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="acrobatics">Acrobatics:</label>
<div class="control">
<input type="text" name="acrobatics" required class="input input-bordered w-full max-w-xs" value="{{
.acrobatics }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="animalHandling">Animal Handling:</label>
<div class="control">
<input type="text" name="animalHandling" required class="input input-bordered w-full max-w-xs"
value="{{ .animalHandling }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="arcana">Arcana:</label>
<div class="control">
<input type="text" name="arcana" required class="input input-bordered w-full max-w-xs" value="{{ .arcana }}"
readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="athletics">Athletics:</label>
<div class="control">
<input type="text" name="athletics" required class="input input-bordered w-full max-w-xs"
value="{{ .athletics }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="deception">Deception:</label>
<div class="control">
<input type="text" name="deception" required class="input input-bordered w-full max-w-xs"
value="{{ .deception }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="history">History:</label>
<div class="control">
<input type="text" name="history" required class="input input-bordered w-full max-w-xs"
value="{{ .history }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="insight">Insight:</label>
<div class="control">
<input type="text" name="insight" required class="input input-bordered w-full max-w-xs"
value="{{ .insight }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="intimidation">Intimidation:</label>
<div class="control">
<input type="text" name="intimidation" required class="input input-bordered w-full max-w-xs"
value="{{ .intimidation }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="investigation">Investigation:</label>
<div class="control">
<input type="text" name="investigation" required class="input input-bordered w-full max-w-xs"
value="{{ .investigation }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="medicine">Medicine:</label>
<div class="control">
<input type="text" name="medicine" required class="input input-bordered w-full max-w-xs"
value="{{ .medicine }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="nature">Nature:</label>
<div class="control">
<input type="text" name="nature" required class="input input-bordered w-full max-w-xs" value="{{ .nature }}"
readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="performance">Performance:</label>
<div class="control">
<input type="text" name="performance" required class="input input-bordered w-full max-w-xs"
value="{{ .performance }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="perception">Perception:</label>
<div class="control">
<input type="text" name="perception" required class="input input-bordered w-full max-w-xs"
value="{{ .perception }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="stealth">Stealth:</label>
<div class="control">
<input type="text" name="stealth" required class="input input-bordered w-full max-w-xs"
value="{{ .stealth }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="persuasion">Persuasion:</label>
<div class="control">
<input type="text" name="persuasion" required class="input input-bordered w-full max-w-xs"
value="{{ .persuasion }}" readonly>
</div>
</div>
</div>
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="sleightOfHand">Sleight of Hand:</label>
<div class="control">
<input type="text" name="sleightOfHand" required class="input input-bordered w-full max-w-xs"
value="{{.sleightOfHand }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="religion">Religion:</label>
<div class="control">
<input type="text" name="religion" required class="input input-bordered w-full max-w-xs"
value="{{ .religion }}" readonly>
</div>
</div>
</div>
<div class="tile is-parent">
<div class="tile is-child field">
<label for="survival">Survival:</label>
<div class="control">
<input type="text" name="survival" required class="input input-bordered w-full max-w-xs"
value="{{ .survival }}" readonly>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}