Compare commits

...

17 commits
v0.1.0 ... 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
Patryk Hegenberg
faf7c2f782 frontend: remove error causing hx-boost attribute on form and make title responsive and centered 2023-12-06 13:09:43 +01:00
Patryk Hegenberg
309ade3e72 frontend: fix bugs according to monsterTable 2023-12-06 11:42:44 +01:00
Patryk Hegenberg
b82e138c4d added content to about and contact page and added main-handler to achive spa-functionality 2023-12-06 09:12:49 +01:00
Patryk Hegenberg
d60d1df51e add dark-mode and theme switcher 2023-12-06 08:38:23 +01:00
Patryk Hegenberg
4f69762b1f improve usability and styling
change table layout for Form to flexbox using bulma tiles
2023-12-05 19:58:13 +01:00
Patryk Hegenberg
e230c0e10d switch classes from daisyui to bulma 2023-12-05 14:26:03 +01:00
Patryk Hegenberg
b8b51f1870 frontend: add tableview to display added monsters 2023-12-05 12:47:57 +01:00
26 changed files with 1663 additions and 304 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
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
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"
)
// 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 {
log.Print("AboutHandler called")
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 {
http.Error(w, err.Error(), http.StatusInternalServerError)
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusBadRequest)
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",
})
}
err = tmpl.ExecuteTemplate(w, "about", data)
if err != nil {
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,83 +5,140 @@ import (
"log"
"net/http"
"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 {
log.Print("AddMonster called")
return func(w http.ResponseWriter, r *http.Request) {
// TODO
// Check if the request method is POST
if r.Method != http.MethodPost {
log.Print("Method not allowed")
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse the form data
err := r.ParseForm()
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
}
monster := model.Monster{
Name: r.FormValue("name"),
Source: r.FormValue("source"),
Size: []string{r.FormValue("size")},
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")},
},
},
}
// Create a new monster with the form data
monster := parseMonster(r)
// Lock the Monsters slice, append the monster, and unlock the slice
mu.Lock()
defer mu.Unlock()
*Monsters = append(*Monsters, monster)
// 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)
}
}
// 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 {
// 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)
// If there is an error in the conversion, return 0 and log the error
if err != nil {
log.Println("Conversion error:", err)
return 0
}
// Log the converted integer
log.Println("Converted integer:", i)
// Return the converted integer
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"
)
// 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 {
log.Print("ContactHandler called")
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")
if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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",
})
if err != nil {

View file

@ -1,27 +1,56 @@
package handlers
import (
"ddServer/model"
"embed"
"html/template"
"log"
"net/http"
)
func FormHandler(content embed.FS, 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")
// Lock the mutex to ensure exclusive access to the monsters slice.
mu.Lock()
defer mu.Unlock()
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")
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 {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = tmpl.ExecuteTemplate(w, "base", map[string]interface{}{
"Title": "Dungeons & Dragons Monster Generator",
})
// Execute the template and render the response.
data := map[string]any{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
}
err = tmpl.ExecuteTemplate(w, "base", data)
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
log.Printf("Template rendered with %d Monsters\n", len(*monsters))
}
}

40
handlers/main_handler.go Normal file
View file

@ -0,0 +1,40 @@
package handlers
import (
"ddServer/model"
"embed"
"html/template"
"log"
"net/http"
)
// 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 {
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")
if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Lock the mutex to ensure exclusive access to the monsters slice
mu.Lock()
defer mu.Unlock()
// Execute the main template with the provided data
err = tmpl.ExecuteTemplate(w, "main", map[string]any{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
})
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

View file

@ -0,0 +1,36 @@
package handlers
import (
"ddServer/model"
"embed"
"html/template"
"log"
"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 {
log.Print("MonsterTableHandler called")
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")
if err != nil {
log.Printf("Template parsing error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Execute the template and pass the necessary data
err = tmpl.ExecuteTemplate(w, "monsterTable", map[string]any{
"Title": "Dungeons & Dragons Monster Generator",
"Monsters": *monsters,
})
if err != nil {
log.Printf("Template execution error: %v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}

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
// submitHandler verarbeitet die Formulardaten
// SubmitHandler processes the form data.
func SubmitHandler(content embed.FS, chars *[]model.Character, Monsters *[]model.Monster, filename string) http.HandlerFunc {
log.Print("SubmitHandler called")
return func(w http.ResponseWriter, r *http.Request) {
log.Print("SubmitHandler called")
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Formulardaten parsen
// Parse form data.
err := r.ParseForm()
if err != nil {
log.Printf("Error parsing form data: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Monster-Objekt erstellen
// Create monster object.
filename := r.FormValue("filename")
// Charakter-Objekt erstellen oder aktualisieren
// Create or update character object.
mu.Lock()
defer mu.Unlock()
char := model.GetOrCreateCharacter(filename, *chars)
char.Monster = append(char.Monster, *Monsters...)
// Charakterdaten in JSON umwandeln
// Convert character data to JSON.
charJSON, err := json.Marshal(char)
if err != nil {
log.Printf("Error marshalling character data to JSON: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// JSON-Daten in die Datei schreiben
// Write JSON data to file.
err = model.WriteToFile(filename, charJSON)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Dateiinhalt lesen
fileContent, err := os.ReadFile(filename)
if err != nil {
log.Printf("Error writing JSON data to file: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
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-Type", "application/json")
w.Write(fileContent)

56
main.go
View file

@ -14,20 +14,60 @@ var (
chars []model.Character
//go:embed templates/*.html
//go:embed images/*
content embed.FS
content embed.FS
//go:embed static/*
static embed.FS
Monsters []model.Monster
)
// main is the entry point of the program.
func main() {
filename := ""
http.HandleFunc("/", handlers.FormHandler(content, filename))
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("/about", handlers.AboutHandler(content))
http.HandleFunc("/contact", handlers.ContactHandler(content))
// 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)
// Load the CSS file.
css, err := loadCSS(static)
if err != nil {
log.Fatal(err)
}
// Add a route for the CSS file
routes.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
w.Write([]byte(css))
})
// Print the message indicating that the server has started.
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 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) {
// Read the CSS file "static/darkly_bulmawatch.css" from the embedded filesystem
file, err := content.ReadFile("static/darkly_bulmawatch.css")
if err != nil {
return "", err
}
// Convert the file content to a string and return
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 (
"fmt"
"log"
"os"
"time"
)
// Monster struct für die Daten des Monsters
type Monster struct {
Save Save `json:"save"`
Skill Skill `json:"skill"`
HP HP `json:"hp"`
Source string `json:"source"`
CR string `json:"cr"`
Type string `json:"type"`
Name string `json:"name"`
DamageRes []string `json:"damageResistances"`
Traits []Trait `json:"trait"`
AC []AC `json:"ac"`
Alignment []string `json:"alignment"`
Senses []string `json:"senses"`
Languages []string `json:"languages"`
Size []string `json:"size"`
Actions []Action `json:"action"`
Speed Speed `json:"speed"`
Str int `json:"str"`
Dex int `json:"dex"`
Con int `json:"con"`
Int int `json:"int"`
Wis int `json:"wis"`
Cha int `json:"cha"`
Save Save `json:"save"`
Skill Skill `json:"skill"`
HP HP `json:"hp"`
Source string `json:"source"`
CR string `json:"cr"`
Type string `json:"type"`
Name string `json:"name"`
Vulnerable []string `json:"vulnerable"`
ConditionImmune []string `json:"conditionImmune"`
Resist []string `json:"resist"`
Immune []string `json:"immune"`
Traits []Trait `json:"trait"`
AC []AC `json:"ac"`
Alignment []string `json:"alignment"`
Senses []string `json:"senses"`
Languages []string `json:"languages"`
Size []string `json:"size"`
Actions []Action `json:"action"`
Speed Speed `json:"speed"`
Str int `json:"str"`
Dex int `json:"dex"`
Con int `json:"con"`
Int int `json:"int"`
Wis int `json:"wis"`
Cha int `json:"cha"`
}
type AC struct {
@ -43,18 +47,41 @@ type HP 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 {
Dex string `json:"dex"`
Con string `json:"con"`
Wis string `json:"wis"`
Cha string `json:"cha"`
Str string `json:"str"`
Int string `json:"int"`
}
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 {
@ -69,8 +96,8 @@ type Action struct {
// Character struct für die Daten des Charakters
type Character struct {
Monster []Monster `json:"monster"`
Meta Meta `json:"_meta"`
Monster []Monster `json:"monster"`
}
// Meta struct für Meta-Informationen
@ -89,32 +116,47 @@ type Source struct {
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 {
log.Println("Writing data to file:", filename)
// Create a file with the given filename
file, err := os.Create(filename)
if err != nil {
log.Println("Error creating file:", 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 {
log.Println("Error writing to file:", err)
return err
}
log.Printf("Successfully wrote %d bytes to file", n)
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 {
// Check if there is an empty character object
for _, char := range chars {
if char.Meta.DateLastModified == 0 {
// Ein leeres Charakterobjekt wurde gefunden
// Return the empty character object
log.Println("Returning existing character object")
return char
}
}
// Erstelle ein neues Charakterobjekt
// Create a new character object
now := time.Now().Unix()
newChar := Character{
Meta: Meta{
@ -134,7 +176,10 @@ func GetOrCreateCharacter(filename string, chars []Character) Character {
Monster: []Monster{},
}
// Append the new character object to the list of characters
chars = append(chars, newChar)
// Return the newly created character object
log.Println("Returning newly created character object")
return newChar
}

File diff suppressed because one or more lines are too long

View file

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

View file

@ -6,72 +6,49 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css">
<link href="https://cdn.jsdelivr.net/npm/daisyui@2.31.0/dist/full.css" rel="stylesheet" type="text/css">
<script src="https://unpkg.com/htmx.org@1.9.9"
integrity="sha384-QFjmbokDn2DjBjq+fM+8LUIVrAgqcNW2s0PjAxHETgRn9l4fvX31ZxDxvwQnyMOX"
crossorigin="anonymous"></script>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
font-family: 'arial', sans-serif;
transition: background-color 0.3s, color 0.3s;
}
.form-box {
max-width: 700px;
width: 100%;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
}
.form-item {
display: grid;
grid-template-columns: auto auto auto;
gap: 10px;
}
.form-item2 {
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.banner {
position: relative;
text-align: center;
}
.centered {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
</style>
<link id="customStylesheet" rel="stylesheet" href="/static/darkly_bulmawatch.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container is-widescreen">
{{ template "header" . }}
</div>
<div class="container is-widescreen" id="main-content">
{{ template "main" . }}
</div>
{{ template "header" . }}
{{ template "main" . }}
{{ template "footer" . }}
<div class="container is-widescreen">
{{ template "footer" . }}
</div>
</body>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0);
// Add a click event on each of them
$navbarBurgers.forEach(el => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target;
const $target = document.getElementById(target);
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active');
$target.classList.toggle('is-active');
});
});
});
</script>
</html>
{{ end }}

View file

@ -1,2 +1,57 @@
{{ define "contact" }}
<div class="tile is-parent">
<div class="tile is-child card ">
<div class="card-content">
<div class="media-content">
<p class="title is-4">Contact Us</p>
</div>
<div class="content">
<div class="contact-info">
<h2>Our Contact Information</h2>
<p>You can reach us through the following channels:</p>
<ul>
<li>Email: example@example.com</li>
<li>Phone: +123456789</li>
</ul>
</div>
<div class="contact-form">
<h2>Contact Form</h2>
<form action="/submitContact" method="post">
<div class="field">
<label for="name">Your Name:</label>
<div class="control">
<input type="text" name="name" required placeholder="Your name"
class="input input-bordered w-full max-w-xs">
</div>
</div>
<div class="field">
<label for="email">Your Email:</label>
<div class="control">
<input type="email" name="email" required placeholder="Your email"
class="input input-bordered w-full max-w-xs">
</div>
</div>
<div class="field">
<label for="message">Your Message:</label>
<div class="control">
<textarea name="message" required placeholder="Type your message here" class="textarea"></textarea>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-primary">Send Message</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
{{ end }}

View file

@ -1,7 +1,9 @@
{{ define "footer" }}
<footer class="footer footer-center p-4 bg-base-300 text-base-content">
<aside>
<p class="text-gray-600">&copy; {{.Year}} Dungeons and Dragons Monster Generator. Alle Rechte vorbehalten.</p>
</aside>
<footer class="footer">
<div class="content has-text-centered is-widescreen">
<p>
&copy; {{.Year}} Dungeons and Dragons Monster Generator. Alle Rechte vorbehalten.
</p>
</div>
</footer>
{{ end }}

View file

@ -1,14 +1,81 @@
{{ define "header" }}
<header>
<nav class="navbar bg-base-100">
<a href="/" class="btn btn-ghost text-xl">Dungeons & Dragons</a>
<a href="/about" class="btn btn-ghost text-xl">About</a>
<a href="/contact" class="btn btn-ghost text-xl">Contact</a>
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarBasicExample">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a href="/" class="navbar-item" hx-get="/main" hx-target="#main-content" hx-boost="true">
Dungeons & Dragons
</a>
<a href="/about" class="navbar-item" hx-get="/about" hx-target="#main-content" hx-boost="true">
About
</a>
<a href="/contact" class="navbar-item" hx-get="/contact" hx-target="#main-content" hx-boost="true">
Contact
</a>
</div>
<div class="navbar-end">
<div class="navbar-item">
<button id="switchButton" class="button is-light">
<span id="stylesheetIcon" class="icon">
<i class="fas fa-sun"></i>
</span>
</button>
</div>
</div>
</div>
</nav>
<div id="banner" class="banner bg-gray-800 p-8 text-white">
<img src="/images/images/banner.jpg" alt="Dungeons-and-Dragons-Banner">
<a href="https://dnd.wizards.com/resources/press-assets">image credits</a>
<h1 class="centered text-4xl font-bold">Dungeons and Dragons Monster Generator</h1>
<div class="tile is-ancestor">
<div class="tile is-parent">
<section class="tile is-child hero box is-widescreen">
<div class="hero-body is-flex is-justify-content-center is-align-items-center">
<div class="container has-text-centered">
<div class="hero-background">
<img src="/images/images/banner.jpg" alt="Dungeons-and-Dragons-Banner">
</div>
<div class="hero-content is-overlay">
<p class="title is-size-5-mobile is-size-1-desktop"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">
Dungeons and Dragons Monster Generator
</p>
</div>
</div>
</div>
</section>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const switchButton = document.getElementById("switchButton");
const stylesheetIcon = document.getElementById("stylesheetIcon");
if (switchButton) {
switchButton.addEventListener('click', switchStylesheet);
}
function switchStylesheet() {
const link = document.getElementById("customStylesheet");
// Ändere das Icon basierend auf dem aktuellen Stylesheet
if (link.getAttribute("href") === "/static/darkly_bulmawatch.css") {
link.setAttribute("href", "https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css");
stylesheetIcon.innerHTML = '<i class="fas fa-moon"></i>'; // Zum Mond-Icon wechseln
} else {
link.setAttribute("href", "/static/darkly_bulmawatch.css");
stylesheetIcon.innerHTML = '<i class="fas fa-sun"></i>'; // Zum Sonnen-Icon wechseln
}
}
});
</script>
</header>
{{ end }}

View file

@ -1,22 +1,109 @@
{{ define "main" }}
<div class="card-body form-box w-4/5 h-full">
<h2 class="card-title">Monster Form</h2>
<form action="/submit" method="post" class="grid grid-columns-5 space-y-4 grid place-items-center">
<table>
<tr>
<td><label for="filename">Filename:</label></td>
<td><input type="text" name="filename" required placeholder="Dateiname"
class="input input-bordered w-full max-w-xs"><br></td>
</tr>
</table>
<div class="grid grid-cols-1">
<button type="button" hx-post="/addMonster" class="btn">Add Monster</button>
<div class="tile is-parent">
<div class="tile is-child card ">
<div class="card-content">
<div class="media-content">
<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 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>
{{ template "monsterform" . }}
<input type="hidden" name="filename" value="{{.Filename}}">
<div class="card-actions justify-end">
<input type="submit" value="Submit" class="btn">
</div>
<div class="tile is-parent">
<div class="tile is-child card">
<div class="card-content">
<div class="media-content">
<p class="title is-4 is-centered">Existing Monsters</p>
</div>
<div class="table-container">
<table class="table">
<colgroup>
<col style="width: 100px;">
<col style="width: 100px;">
</colgroup>
<thead>
<tr>
<th>Name</th>
<th>Source</th>
<th>Size</th>
<th>Type</th>
<th>Alignment</th>
<th>AC</th>
<th>AC Form</th>
<th>HP Average</th>
<th>HP Formula</th>
<th>Walk</th>
<th>Swim</th>
<th>Burrow</th>
<th>Climb</th>
<th>Fly</th>
<th>Str</th>
<th>Dex</th>
<th>Con</th>
<th>Int</th>
<th>Wis</th>
<th>Cha</th>
<th>Save Dex</th>
<th>Save Con</th>
<th>Save Wis</th>
<th>Save Str</th>
<th>Save Con</th>
<th>Save Cha</th>
<th>Perception</th>
<th>Stealth</th>
<th>Acrobatics</th>
<th>AnimalHandling</th>
<th>Arcana</th>
<th>Athletics</th>
<th>Deception</th>
<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>
</form>
</div>
</div>
{{ end }}

59
templates/monster.html Normal file
View file

@ -0,0 +1,59 @@
{{ define "monster" }}
<tr>
<td>{{.Name}}</td>
<td>{{.Source}}</td>
<td>{{range .Size}}{{.}}{{end}}</td>
<td>{{.Type}}</td>
<td>{{range .Alignment}}{{.}}{{end}}</td>
<td>{{range .AC}}{{.AC}}{{end}}</td>
<td>{{range .AC}}{{.From}}{{end}}</td>
<td>{{.HP.Average}}</td>
<td>{{.HP.Formula}}</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>{{.Dex}}</td>
<td>{{.Con}}</td>
<td>{{.Int}}</td>
<td>{{.Wis}}</td>
<td>{{.Cha}}</td>
<td>{{.Save.Dex}}</td>
<td>{{.Save.Con}}</td>
<td>{{.Save.Wis}}</td>
<td>{{.Save.Str}}</td>
<td>{{.Save.Con}}</td>
<td>{{.Save.Cha}}</td>
<td>{{.Skill.Perception}}</td>
<td>{{.Skill.Stealth}}</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 .Languages}}{{.}}{{end}}</td>
<td>{{.CR}}</td>
<td>{{range .Traits}}{{.Name}}{{end}}</td>
<td>{{range .Traits}}{{range .Entries}}{{.}}{{end}}{{end}}</td>
<td>{{range .Actions}}{{.Name}}{{end}}</td>
<td>{{range .Actions}}{{range .Entries}}{{.}}{{end}}{{end}}</td>
</tr>
{{ end }}

View file

@ -1,101 +1,378 @@
{{ define "monsterform" }}
<table>
<tr>
<td><label for="name">Monster Name:</label></td>
<td><input type="text" name="name" required placeholder="Type here"
class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="source">Monster Source:</label></td>
<td><input type="text" name="source" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="size">Size:</label></td>
<td><input type="text" name="size" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="type">Type:</label></td>
<td><input type="text" name="type" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="alignment">Alignment:</label></td>
<td><input type="text" name="alignment" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="ac">AC:</label></td>
<td><input type="number" name="ac" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="acFrom">AC From:</label></td>
<td><input type="text" name="acFrom" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="hpAverage">HP Average:</label></td>
<td><input type="number" name="hpAverage" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="hpFormula">HP Formula:</label></td>
<td><input type="text" name="hpFormula" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="speed">Speed:</label></td>
<td><input type="number" name="speed" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="str">Str:</label></td>
<td><input type="number" name="str" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="dex">Dex:</label></td>
<td><input type="number" name="dex" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="con">Con:</label></td>
<td><input type="number" name="con" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="int">Int:</label></td>
<td><input type="number" name="int" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="wis">Wis:</label></td>
<td><input type="number" name="wis" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="cha">Cha:</label></td>
<td><input type="number" name="cha" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="saveDex">Save Dex:</label></td>
<td><input type="text" name="saveDex" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="saveCon">Save Con:</label></td>
<td><input type="text" name="saveCon" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="saveWis">Save Wis:</label></td>
<td><input type="text" name="saveWis" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="perception">Perception:</label></td>
<td><input type="text" name="perception" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="stealth">Stealth:</label></td>
<td><input type="text" name="stealth" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="damageRes">Damage Resistances:</label></td>
<td><input type="text" name="damageRes" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="senses">Senses:</label></td>
<td><input type="text" name="senses" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="languages">Languages:</label></td>
<td><input type="text" name="languages" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="cr">CR:</label></td>
<td><input type="text" name="cr" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="traitName">Trait Name:</label></td>
<td><input type="text" name="traitName" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="traitEntry">Trait Entry:</label></td>
<td><input type="text" name="traitEntry" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
<tr>
<td><label for="actionName">Action Name:</label></td>
<td><input type="text" name="actionName" required class="input input-bordered w-full max-w-xs"><br></td>
<td><label for="actionEntry">Action Entry:</label></td>
<td><input type="text" name="actionEntry" required class="input input-bordered w-full max-w-xs"><br></td>
</tr>
</table>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="name">Monster Name:</label>
<div class="control">
<input type="text" name="name" required placeholder="Type here" 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="source">Monster Source:</label>
<div class="control">
<input type="text" name="source" 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="size">Size:</label>
<div class="control">
<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 class="tile is-parent">
<div class="tile is-child field">
<label for="type">Type:</label>
<div class="control">
<input type="text" name="type" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="tile is-vertical is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="alignment">Alignment:</label>
<div class="control">
<input type="text" name="alignment" 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="ac">AC:</label>
<div class="control">
<input type="number" name="ac" 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="acFrom">AC From:</label>
<div class="control">
<input type="text" name="acFrom" 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="hpAverage">HP Average:</label>
<div class="control">
<input type="number" name="hpAverage" 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="hpFormula">HP Formula:</label>
<div class="control">
<input type="text" name="hpFormula" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
<div class="card tile is-ancestor is-vertical">
<header class="card-header">
<p class="card-header-title">
Speed
</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="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 class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="str">Str:</label>
<div class="control">
<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 class="tile is-parent">
<div class="tile is-child field">
<label for="dex">Dex:</label>
<div class="control">
<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 class="tile is-parent">
<div class="tile is-child field">
<label for="con">Con:</label>
<div class="control">
<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 class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="int">Int:</label>
<div class="control">
<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 class="tile is-parent">
<div class="tile is-child field">
<label for="wis">Wis:</label>
<div class="control">
<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 class="tile is-parent">
<div class="tile is-child field">
<label for="cha">Cha:</label>
<div class="control">
<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 class="card tile is-ancestor is-vertical">
<header class="card-header">
<p class="card-header-title">
Save
</p>
</header>
<div class="card-content">
<div class="content">
<div class="tile is-ancestor">
<div class="tile is-parent gap">
<div class="tile is-child field">
<label for="saveDex">Dex:</label>
<div class="control">
<input type="text" name="saveDex" 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="saveCon">Con:</label>
<div class="control">
<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 id="skills" class="card tile is-ancestor is-vertical">
{{ template "skills" }}
</div>
<div class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="resist">Damage Resistances:</label>
<div class="control">
<input type="text" name="resist" 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="immune">Damage Immunity:</label>
<div class="control">
<input type="text" name="immune" 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="vulnerable">Vulnerability:</label>
<div class="control">
<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 class="tile is-ancestor">
<div class="tile is-parent">
<div class="tile is-child field">
<label for="senses">Senses:</label>
<div class="control">
<input type="text" name="senses" 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="languages">Languages:</label>
<div class="control">
<input type="text" name="languages" 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="traitName">Trait Name:</label>
<div class="control">
<input type="text" name="traitName" 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="traitEntry">Trait Entry:</label>
<div class="control">
<input type="text" name="traitEntry" 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="actionName">Action Name:</label>
<div class="control">
<input type="text" name="actionName" 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="actionEntry">Action Entry:</label>
<div class="control">
<input type="text" name="actionEntry" required class="input input-bordered w-full max-w-xs">
</div>
</div>
</div>
</div>
{{end}}

View file

@ -0,0 +1,7 @@
{{ define "monsterTable" }}
<tbody id="monster-table">
{{ range .Monsters }}
{{ template "monster" . }}
</tbody>
{{ end }}
{{ end }}

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 }}