diff --git a/data/dk.db b/data/dk.db index fa290bb..eda1cbc 100644 Binary files a/data/dk.db and b/data/dk.db differ diff --git a/internal/models/monsters/monsters.go b/internal/models/monsters/monsters.go index e8f53cb..55e9893 100644 --- a/internal/models/monsters/monsters.go +++ b/internal/models/monsters/monsters.go @@ -105,6 +105,15 @@ func ByImmunity(immunityType int) ([]*Monster, error) { return monsters, err } +func ByName(name string) (*Monster, error) { + var monster Monster + err := database.Get(&monster, "SELECT * FROM monsters WHERE name = %s COLLATE NOCASE", name) + if err != nil { + return nil, fmt.Errorf("monster with name '%s' not found", name) + } + return &monster, nil +} + // Helper methods func (m *Monster) IsHurtImmune() bool { return m.Immune == ImmuneHurt diff --git a/internal/routes/admin.go b/internal/routes/admin.go index 8dd8583..ead9f93 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -4,6 +4,7 @@ import ( "dk/internal/components" "dk/internal/database" "dk/internal/helpers" + "dk/internal/models/monsters" "dk/internal/models/news" "dk/internal/models/spells" "dk/internal/models/towns" @@ -46,6 +47,11 @@ func RegisterAdminRoutes(app *sushi.App) { group.Post("/spells/new", adminSpellCreate) group.Get("/spells/:id", adminSpellEdit) group.Post("/spells/:id", adminSpellUpdate) + group.Get("/monsters", adminMonstersIndex) + group.Get("/monsters/new", adminMonsterNew) + group.Post("/monsters/new", adminMonsterCreate) + group.Get("/monsters/:id", adminMonsterEdit) + group.Post("/monsters/:id", adminMonsterUpdate) } func adminIndex(ctx sushi.Ctx) { @@ -610,6 +616,204 @@ func checkSpellNameConflict(name string, excludeID int) error { return nil } +func adminMonstersIndex(ctx sushi.Ctx) { + pagination := helpers.Pagination{ + Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), + PerPage: 30, + } + + type MonsterData struct { + ID int + Name string + Level int + MaxHP int + MaxDmg int + Armor int + MaxExp int + MaxGold int + Immune int + ImmunityName string + } + + var monsterList []*MonsterData + err := database.Select(&monsterList, ` + SELECT id, name, level, max_hp, max_dmg, armor, max_exp, max_gold, immune, + CASE immune + WHEN 0 THEN 'None' + WHEN 1 THEN 'Hurt Spells' + WHEN 2 THEN 'Sleep Spells' + ELSE 'Unknown' + END as immunity_name + FROM monsters + ORDER BY level ASC, id ASC + LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) + + if err != nil { + fmt.Printf("Error getting monster list for admin index: %s", err.Error()) + monsterList = make([]*MonsterData, 0) + } + + type CountResult struct{ Count int } + var result CountResult + database.Get(&result, "SELECT COUNT(*) as count FROM monsters") + pagination.Total = result.Count + + components.RenderAdminPage(ctx, "Monster Management", "admin/monsters/index.html", map[string]any{ + "monsters": monsterList, + "currentPage": pagination.Page, + "totalPages": pagination.TotalPages(), + "hasNext": pagination.HasNext(), + "hasPrev": pagination.HasPrev(), + }) +} + +func adminMonsterNew(ctx sushi.Ctx) { + monster := monsters.New() + components.RenderAdminPage(ctx, "Add New Monster", "admin/monsters/edit.html", map[string]any{ + "monster": monster, + }) +} + +func adminMonsterCreate(ctx sushi.Ctx) { + sess := ctx.GetCurrentSession() + monster := monsters.New() + + if err := populateMonsterFromForm(ctx, monster); err != nil { + sess.SetFlash("error", err.Error()) + ctx.Redirect("/admin/monsters/new") + return + } + + if err := monster.Validate(); err != nil { + sess.SetFlash("error", err.Error()) + ctx.Redirect("/admin/monsters/new") + return + } + + if err := checkMonsterNameConflict(monster.Name, 0); err != nil { + sess.SetFlash("error", err.Error()) + ctx.Redirect("/admin/monsters/new") + return + } + + if err := monster.Insert(); err != nil { + sess.SetFlash("error", "Failed to create monster") + ctx.Redirect("/admin/monsters/new") + return + } + + sess.SetFlash("success", fmt.Sprintf("Monster %s created successfully", monster.Name)) + ctx.Redirect("/admin/monsters") +} + +func adminMonsterEdit(ctx sushi.Ctx) { + sess := ctx.GetCurrentSession() + id := ctx.Param("id").Int() + + monster, err := monsters.Find(id) + if err != nil { + sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id)) + ctx.Redirect("/admin/monsters") + return + } + + components.RenderAdminPage(ctx, fmt.Sprintf("Edit Monster: %s", monster.Name), "admin/monsters/edit.html", map[string]any{ + "monster": monster, + }) +} + +func adminMonsterUpdate(ctx sushi.Ctx) { + sess := ctx.GetCurrentSession() + id := ctx.Param("id").Int() + + monster, err := monsters.Find(id) + if err != nil { + sess.SetFlash("error", fmt.Sprintf("Monster %d not found", id)) + ctx.Redirect("/admin/monsters") + return + } + + // Check if delete was requested + if ctx.Form("delete").String() == "1" { + if err := monster.Delete(); err != nil { + sess.SetFlash("error", "Failed to delete monster") + ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) + return + } + sess.SetFlash("success", fmt.Sprintf("Monster %s deleted successfully", monster.Name)) + ctx.Redirect("/admin/monsters") + return + } + + if err := populateMonsterFromForm(ctx, monster); err != nil { + sess.SetFlash("error", err.Error()) + ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) + return + } + + if err := monster.Validate(); err != nil { + sess.SetFlash("error", err.Error()) + ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) + return + } + + if err := checkMonsterNameConflict(monster.Name, monster.ID); err != nil { + sess.SetFlash("error", err.Error()) + ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) + return + } + + fields := map[string]any{ + "name": monster.Name, + "level": monster.Level, + "max_hp": monster.MaxHP, + "max_dmg": monster.MaxDmg, + "armor": monster.Armor, + "max_exp": monster.MaxExp, + "max_gold": monster.MaxGold, + "immune": monster.Immune, + } + + if err := database.Update("monsters", fields, "id", id); err != nil { + sess.SetFlash("error", "Failed to update monster") + ctx.Redirect(fmt.Sprintf("/admin/monsters/%d", id)) + return + } + + sess.SetFlash("success", fmt.Sprintf("Monster %s updated successfully", monster.Name)) + ctx.Redirect("/admin/monsters") +} + +func populateMonsterFromForm(ctx sushi.Ctx, monster *monsters.Monster) error { + monster.Name = strings.TrimSpace(ctx.Form("name").String()) + monster.Level = ctx.Form("level").Int() + monster.MaxHP = ctx.Form("max_hp").Int() + monster.MaxDmg = ctx.Form("max_dmg").Int() + monster.Armor = ctx.Form("armor").Int() + monster.MaxExp = ctx.Form("max_exp").Int() + monster.MaxGold = ctx.Form("max_gold").Int() + monster.Immune = ctx.Form("immune").Int() + + if monster.Name == "" { + return fmt.Errorf("monster name is required") + } + + return nil +} + +func checkMonsterNameConflict(name string, excludeID int) error { + existingMonster, err := monsters.ByName(name) + if err != nil { + return nil // No conflict if no monster found or database error + } + + if existingMonster != nil && existingMonster.ID != excludeID { + return fmt.Errorf("a monster with the name '%s' already exists", name) + } + + return nil +} + func bToMb(b uint64) uint64 { return b / 1024 / 1024 } diff --git a/templates/admin/monsters/edit.html b/templates/admin/monsters/edit.html new file mode 100644 index 0000000..037cdda --- /dev/null +++ b/templates/admin/monsters/edit.html @@ -0,0 +1,52 @@ +{include "admin/layout.html"} + +{block "content"} +
+ Total monsters: {#monsters} | Page {currentPage} of {totalPages} +
+ +{if #monsters > 0} +ID | +Name | +Level | +HP | +Damage | +Armor | +Exp | +Gold | +Immunity | +Actions | +
---|---|---|---|---|---|---|---|---|---|
{monster.ID} | +{monster.Name} | +{monster.Level} | +{monster.MaxHP} | +{monster.MaxDmg} | +{monster.Armor} | +{monster.MaxExp} | +{monster.MaxGold} | +{monster.ImmunityName} | ++ Edit + | +