Welcome to the Dungeons and Dragons Monster Generator website! We are a team of enthusiasts...
diff --git a/.gitignore b/.gitignore index cde0123..9fdf7ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ dist/ +test_data/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..34428ea --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/go.mod b/go.mod index 8a6ca08..dfe5d47 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa4b6e6 --- /dev/null +++ b/go.sum @@ -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= diff --git a/handlers/about_handler.go b/handlers/about_handler.go index 12c02f8..1d628e0 100644 --- a/handlers/about_handler.go +++ b/handlers/about_handler.go @@ -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) } } } diff --git a/handlers/add_monster_handler.go b/handlers/add_monster_handler.go index bd84af3..38cb1a1 100644 --- a/handlers/add_monster_handler.go +++ b/handlers/add_monster_handler.go @@ -5,87 +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.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) } } -// 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")}, + }, + }, + } +} diff --git a/handlers/contact_handler.go b/handlers/contact_handler.go index a683377..611abc2 100644 --- a/handlers/contact_handler.go +++ b/handlers/contact_handler.go @@ -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 { diff --git a/handlers/form_handler.go b/handlers/form_handler.go index 878ba66..1232463 100644 --- a/handlers/form_handler.go +++ b/handlers/form_handler.go @@ -8,25 +8,49 @@ import ( "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") + + // 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", "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 { + log.Printf("Template parsing error: %v\n", err) http.Error(w, err.Error(), http.StatusInternalServerError) 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", "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 mit %d Monstern gerendert\n", len(*monsters)) + log.Printf("Template rendered with %d Monsters\n", len(*monsters)) } } diff --git a/handlers/main_handler.go b/handlers/main_handler.go index 7f25b6a..e4b575e 100644 --- a/handlers/main_handler.go +++ b/handlers/main_handler.go @@ -8,20 +8,27 @@ import ( "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 { - log.Print("MainHandler called") 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() - 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", "Monsters": *monsters, }) diff --git a/handlers/monster_table_handler.go b/handlers/monster_table_handler.go index ebba144..f227962 100644 --- a/handlers/monster_table_handler.go +++ b/handlers/monster_table_handler.go @@ -8,16 +8,23 @@ import ( "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("AboutHandler called") + 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 } - 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", "Monsters": *monsters, }) diff --git a/handlers/skill_calculation_handler.go b/handlers/skill_calculation_handler.go new file mode 100644 index 0000000..48935cf --- /dev/null +++ b/handlers/skill_calculation_handler.go @@ -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 +} diff --git a/handlers/submit_handler.go b/handlers/submit_handler.go index 02a6fde..4a7b50e 100644 --- a/handlers/submit_handler.go +++ b/handlers/submit_handler.go @@ -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) diff --git a/main.go b/main.go index 2060623..d163e79 100644 --- a/main.go +++ b/main.go @@ -20,40 +20,54 @@ var ( Monsters []model.Monster ) +// main is the entry point of the program. func main() { 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) - http.HandleFunc("/", handlers.FormHandler(content, &Monsters, 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("/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 + // Load the CSS file. css, err := loadCSS(static) if err != nil { log.Fatal(err) } - // Füge eine Route für die CSS-Datei hinzu - http.HandleFunc("/static/darkly_bulmawatch.css", func(w http.ResponseWriter, r *http.Request) { + // 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 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) { + // 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 } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..d3acb55 --- /dev/null +++ b/main_test.go @@ -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 +} diff --git a/model/model.go b/model/model.go index 1ad1bb0..0c8ec00 100644 --- a/model/model.go +++ b/model/model.go @@ -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 { @@ -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 } diff --git a/templates/about.html b/templates/about.html index 9d29c17..6133b97 100644 --- a/templates/about.html +++ b/templates/about.html @@ -1,15 +1,19 @@ {{ define "about" }} +
About Us
Welcome to the Dungeons and Dragons Monster Generator website! We are a team of enthusiasts...
Monster Form
-Existing Monsters
-| Name | -Source | -Size | -Type | -Alignment | -AC | -AC Form | -HP Average | -HP Formula | -Speed | -Str | -Dex | -Con | -Int | -Wis | -Cha | -Save Dex | -Save Con | -Savve Wis | -Perception | -Stealth | -Damage Resistance | -Senses | -Languages | -CR | -Trait Name | -Trait Entry | -Action Name | -Action Entry | -
|---|
Existing Monsters
+| Name | +Source | +Size | +Type | +Alignment | +AC | +AC Form | +HP Average | +HP Formula | +Walk | +Swim | +Burrow | +Climb | +Fly | +Str | +Dex | +Con | +Int | +Wis | +Cha | +Save Dex | +Save Con | +Save Wis | +Save Str | +Save Con | +Save Cha | +Perception | +Stealth | +Acrobatics | +AnimalHandling | +Arcana | +Athletics | +Deception | +History | +Insight | +Intimidation | +Investigation | +Medicine | +Nature | +Performance | +Persuasion | +SleightOfHand | +Survival | +Religion | +Damage Resistance | +Damage Immune | +Vulnerable | +Condition Immune | +Senses | +Languages | +CR | +Trait Name | +Trait Entry | +Action Name | +Action Entry | +
|---|
+ Speed +
++ Save +
++ Skill +
+