package routes import ( "dk/internal/components" "dk/internal/database" "dk/internal/helpers" "dk/internal/models/classes" "dk/internal/models/items" "dk/internal/models/monsters" "dk/internal/models/news" "dk/internal/models/spells" "dk/internal/models/towns" "dk/internal/models/users" "encoding/json" "fmt" "runtime" "strconv" "strings" "time" sushi "git.sharkk.net/Sharkk/Sushi" "git.sharkk.net/Sharkk/Sushi/auth" "git.sharkk.net/Sharkk/Sushi/password" ) func RegisterAdminRoutes(app *sushi.App) { group := app.Group("/admin") group.Use(auth.RequireAuth()) group.Use(func(ctx sushi.Ctx, next func()) { if ctx.GetCurrentUser().(*users.User).Auth < 4 { ctx.Redirect("/") return } next() }) group.Get("/", adminIndex) group.Get("/news", adminNewsForm) group.Post("/news", adminNewsCreate) group.Get("/users", adminUsersIndex) group.Get("/users/:id", adminUserEdit) group.Post("/users/:id", adminUserUpdate) group.Get("/towns", adminTownsIndex) group.Get("/towns/new", adminTownNew) group.Post("/towns/new", adminTownCreate) group.Get("/towns/:id", adminTownEdit) group.Post("/towns/:id", adminTownUpdate) group.Get("/spells", adminSpellsIndex) group.Get("/spells/new", adminSpellNew) 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) group.Get("/items", adminItemsIndex) group.Get("/items/new", adminItemNew) group.Post("/items/new", adminItemCreate) group.Get("/items/:id", adminItemEdit) group.Post("/items/:id", adminItemUpdate) group.Get("/classes", adminClassesIndex) group.Get("/classes/new", adminClassNew) group.Post("/classes/new", adminClassCreate) group.Get("/classes/:id", adminClassEdit) group.Post("/classes/:id", adminClassUpdate) group.Get("/classes/:id/spells", adminClassSpells) group.Post("/classes/:id/spells", adminClassSpellsUpdate) group.Get("/api/spells", adminSpellsAPI) } func adminIndex(ctx sushi.Ctx) { var m runtime.MemStats runtime.ReadMemStats(&m) components.RenderAdminPage(ctx, "", "admin/home.html", map[string]any{ "alloc_mb": bToMb(m.Alloc), "total_alloc_mb": bToMb(m.TotalAlloc), "sys_mb": bToMb(m.Sys), "heap_alloc_mb": bToMb(m.HeapAlloc), "heap_sys_mb": bToMb(m.HeapSys), "heap_released_mb": bToMb(m.HeapReleased), "gc_cycles": m.NumGC, "gc_pause_total": m.PauseTotalNs / 1000000, // ms "goroutines": runtime.NumGoroutine(), "cpu_cores": runtime.NumCPU(), "go_version": runtime.Version(), }) } func adminNewsForm(ctx sushi.Ctx) { components.RenderAdminPage(ctx, "", "admin/news.html", map[string]any{}) } func adminNewsCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() content := strings.TrimSpace(ctx.Form("content").String()) if content == "" { sess.SetFlash("error", "Content cannot be empty") ctx.Redirect("/admin/news") return } user := ctx.GetCurrentUser().(*users.User) newsPost := &news.News{ Author: user.ID, Content: content, Posted: time.Now().Unix(), } if err := newsPost.Insert(); err != nil { sess.SetFlash("error", "Failed to create news post") ctx.Redirect("/admin/news") return } sess.SetFlash("success", "News post created successfully") ctx.Redirect("/admin") } func adminUsersIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type UserData struct { ID int Username string Email string Level int Auth int ClassID int ClassName string HP int MaxHP int Registered int64 LastOnline int64 } var userList []*UserData err := database.Select(&userList, ` SELECT u.id, u.username, u.email, u.level, u.auth, u.class_id, COALESCE(c.name, 'Unknown') as class_name, u.hp, u.max_hp, u.registered, u.last_online FROM users u LEFT JOIN classes c ON u.class_id = c.id ORDER BY u.id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting user list for admin index: %s", err.Error()) userList = make([]*UserData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM users") pagination.Total = result.Count components.RenderAdminPage(ctx, "User Management", "admin/users/index.html", map[string]any{ "users": userList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminUserEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() user, err := users.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("User %d not found", id)) ctx.Redirect("/admin/users") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit User: %s", user.Username), "admin/users/edit.html", map[string]any{ "user": user, }) } func adminUserUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() user, err := users.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("User %d not found", id)) ctx.Redirect("/admin/users") return } // Update fields username := strings.TrimSpace(ctx.Form("username").String()) email := strings.TrimSpace(ctx.Form("email").String()) level, _ := strconv.Atoi(ctx.Form("level").String()) auth, _ := strconv.Atoi(ctx.Form("auth").String()) hp, _ := strconv.Atoi(ctx.Form("hp").String()) maxHP, _ := strconv.Atoi(ctx.Form("max_hp").String()) newPassword := strings.TrimSpace(ctx.Form("new_password").String()) if username == "" || email == "" { sess.SetFlash("error", "Username and email are required") ctx.Redirect(fmt.Sprintf("/admin/users/%d", id)) return } user.Username = username user.Email = email user.Level = level user.Auth = auth user.HP = hp user.MaxHP = maxHP if newPassword != "" { user.Password = password.HashPassword(newPassword) } fields := map[string]any{ "username": user.Username, "email": user.Email, "level": user.Level, "auth": user.Auth, "hp": user.HP, "max_hp": user.MaxHP, } if newPassword != "" { fields["password"] = user.Password } if err := database.Update("users", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update user") ctx.Redirect(fmt.Sprintf("/admin/users/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("User %s updated successfully", user.Username)) ctx.Redirect("/admin/users") } func adminTownsIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type TownData struct { ID int Name string X int Y int InnCost int MapCost int TPCost int ShopList string ShopItemCount int } var townList []*TownData err := database.Select(&townList, ` SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list, CASE WHEN shop_list = '' THEN 0 ELSE (LENGTH(shop_list) - LENGTH(REPLACE(shop_list, ',', '')) + 1) END as shop_item_count FROM towns ORDER BY id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting town list for admin index: %s", err.Error()) townList = make([]*TownData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM towns") pagination.Total = result.Count components.RenderAdminPage(ctx, "Town Management", "admin/towns/index.html", map[string]any{ "towns": townList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminTownNew(ctx sushi.Ctx) { town := towns.New() components.RenderAdminPage(ctx, "Add New Town", "admin/towns/edit.html", map[string]any{ "town": town, }) } func adminTownCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() town := towns.New() if err := populateTownFromForm(ctx, town); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/towns/new") return } if err := town.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/towns/new") return } if err := checkTownCoordinateConflict(town.X, town.Y, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/towns/new") return } if err := town.Insert(); err != nil { sess.SetFlash("error", "Failed to create town") ctx.Redirect("/admin/towns/new") return } sess.SetFlash("success", fmt.Sprintf("Town %s created successfully", town.Name)) ctx.Redirect("/admin/towns") } func adminTownEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() town, err := towns.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Town %d not found", id)) ctx.Redirect("/admin/towns") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Town: %s", town.Name), "admin/towns/edit.html", map[string]any{ "town": town, }) } func adminTownUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() town, err := towns.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Town %d not found", id)) ctx.Redirect("/admin/towns") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := town.Delete(); err != nil { sess.SetFlash("error", "Failed to delete town") ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Town %s deleted successfully", town.Name)) ctx.Redirect("/admin/towns") return } if err := populateTownFromForm(ctx, town); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } if err := town.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } if err := checkTownCoordinateConflict(town.X, town.Y, town.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } fields := map[string]any{ "name": town.Name, "x": town.X, "y": town.Y, "inn_cost": town.InnCost, "map_cost": town.MapCost, "tp_cost": town.TPCost, "shop_list": town.ShopList, } if err := database.Update("towns", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update town") ctx.Redirect(fmt.Sprintf("/admin/towns/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Town %s updated successfully", town.Name)) ctx.Redirect("/admin/towns") } func populateTownFromForm(ctx sushi.Ctx, town *towns.Town) error { town.Name = strings.TrimSpace(ctx.Form("name").String()) town.X = ctx.Form("x").Int() town.Y = ctx.Form("y").Int() town.InnCost = ctx.Form("inn_cost").Int() town.MapCost = ctx.Form("map_cost").Int() town.TPCost = ctx.Form("tp_cost").Int() town.ShopList = strings.TrimSpace(ctx.Form("shop_list").String()) if town.Name == "" { return fmt.Errorf("town name is required") } return nil } func checkTownCoordinateConflict(x, y, excludeID int) error { existingTown, err := towns.ByCoords(x, y) if err != nil { return nil // No conflict if no town found or database error } if existingTown != nil && existingTown.ID != excludeID { return fmt.Errorf("a town already exists at coordinates (%d, %d): %s", x, y, existingTown.Name) } return nil } func adminSpellsIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type SpellData struct { ID int Name string Type int TypeName string MP int Power int Icon string Lore string } var spellList []*SpellData err := database.Select(&spellList, ` SELECT id, name, type, mp, power, icon, lore, CASE type WHEN 0 THEN 'Heal' WHEN 1 THEN 'Damage' WHEN 2 THEN 'Sleep' WHEN 3 THEN 'Uber Attack' WHEN 4 THEN 'Uber Defense' ELSE 'Unknown' END as type_name FROM spells ORDER BY type ASC, mp ASC, id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting spell list for admin index: %s", err.Error()) spellList = make([]*SpellData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM spells") pagination.Total = result.Count components.RenderAdminPage(ctx, "Spell Management", "admin/spells/index.html", map[string]any{ "spells": spellList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminSpellNew(ctx sushi.Ctx) { spell := spells.New() components.RenderAdminPage(ctx, "Add New Spell", "admin/spells/edit.html", map[string]any{ "spell": spell, }) } func adminSpellCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() spell := spells.New() if err := populateSpellFromForm(ctx, spell); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/spells/new") return } if err := spell.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/spells/new") return } if err := checkSpellNameConflict(spell.Name, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/spells/new") return } if err := spell.Insert(); err != nil { sess.SetFlash("error", "Failed to create spell") ctx.Redirect("/admin/spells/new") return } sess.SetFlash("success", fmt.Sprintf("Spell %s created successfully", spell.Name)) ctx.Redirect("/admin/spells") } func adminSpellEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() spell, err := spells.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Spell %d not found", id)) ctx.Redirect("/admin/spells") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Spell: %s", spell.Name), "admin/spells/edit.html", map[string]any{ "spell": spell, }) } func adminSpellUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() spell, err := spells.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Spell %d not found", id)) ctx.Redirect("/admin/spells") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := spell.Delete(); err != nil { sess.SetFlash("error", "Failed to delete spell") ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Spell %s deleted successfully", spell.Name)) ctx.Redirect("/admin/spells") return } if err := populateSpellFromForm(ctx, spell); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } if err := spell.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } if err := checkSpellNameConflict(spell.Name, spell.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } fields := map[string]any{ "name": spell.Name, "type": spell.Type, "mp": spell.MP, "power": spell.Power, "icon": spell.Icon, "lore": spell.Lore, } if err := database.Update("spells", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update spell") ctx.Redirect(fmt.Sprintf("/admin/spells/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Spell %s updated successfully", spell.Name)) ctx.Redirect("/admin/spells") } func populateSpellFromForm(ctx sushi.Ctx, spell *spells.Spell) error { spell.Name = strings.TrimSpace(ctx.Form("name").String()) spell.Type = ctx.Form("type").Int() spell.MP = ctx.Form("mp").Int() spell.Power = ctx.Form("power").Int() spell.Icon = strings.TrimSpace(ctx.Form("icon").String()) spell.Lore = strings.TrimSpace(ctx.Form("lore").String()) if spell.Name == "" { return fmt.Errorf("spell name is required") } return nil } func checkSpellNameConflict(name string, excludeID int) error { existingSpell, err := spells.ByName(name) if err != nil { return nil // No conflict if no spell found or database error } if existingSpell != nil && existingSpell.ID != excludeID { return fmt.Errorf("a spell with the name '%s' already exists", name) } 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 adminItemsIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type ItemData struct { ID int Name string Type int TypeName string Value int Attack int Defense int Strength int Dexterity int MaxHP int MaxMP int ExpBonus int GoldBonus int Special string Lore string } var itemList []*ItemData err := database.Select(&itemList, ` SELECT id, name, type, value, attack, defense, strength, dexterity, max_hp, max_mp, exp_bonus, gold_bonus, special, lore, CASE type WHEN 1 THEN 'Weapon' WHEN 2 THEN 'Armor' WHEN 3 THEN 'Shield' WHEN 4 THEN 'Accessory' ELSE 'Unknown' END as type_name FROM items ORDER BY type ASC, value ASC, id ASC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting item list for admin index: %s", err.Error()) itemList = make([]*ItemData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM items") pagination.Total = result.Count components.RenderAdminPage(ctx, "Item Management", "admin/items/index.html", map[string]any{ "items": itemList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminItemNew(ctx sushi.Ctx) { item := items.New() components.RenderAdminPage(ctx, "Add New Item", "admin/items/edit.html", map[string]any{ "item": item, }) } func adminItemCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() item := items.New() if err := populateItemFromForm(ctx, item); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/items/new") return } if err := item.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/items/new") return } if err := checkItemNameConflict(item.Name, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/items/new") return } if err := item.Insert(); err != nil { sess.SetFlash("error", "Failed to create item") ctx.Redirect("/admin/items/new") return } sess.SetFlash("success", fmt.Sprintf("Item %s created successfully", item.Name)) ctx.Redirect("/admin/items") } func adminItemEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() item, err := items.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Item %d not found", id)) ctx.Redirect("/admin/items") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Item: %s", item.Name), "admin/items/edit.html", map[string]any{ "item": item, }) } func adminItemUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() item, err := items.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Item %d not found", id)) ctx.Redirect("/admin/items") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := item.Delete(); err != nil { sess.SetFlash("error", "Failed to delete item") ctx.Redirect(fmt.Sprintf("/admin/items/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Item %s deleted successfully", item.Name)) ctx.Redirect("/admin/items") return } if err := populateItemFromForm(ctx, item); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/items/%d", id)) return } if err := item.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/items/%d", id)) return } if err := checkItemNameConflict(item.Name, item.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/items/%d", id)) return } fields := map[string]any{ "name": item.Name, "type": item.Type, "value": item.Value, "lore": item.Lore, "attack": item.Attack, "defense": item.Defense, "strength": item.Strength, "dexterity": item.Dexterity, "max_hp": item.MaxHP, "max_mp": item.MaxMP, "exp_bonus": item.ExpBonus, "gold_bonus": item.GoldBonus, "special": item.Special, } if err := database.Update("items", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update item") ctx.Redirect(fmt.Sprintf("/admin/items/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Item %s updated successfully", item.Name)) ctx.Redirect("/admin/items") } func populateItemFromForm(ctx sushi.Ctx, item *items.Item) error { item.Name = strings.TrimSpace(ctx.Form("name").String()) item.Type = ctx.Form("type").Int() item.Value = ctx.Form("value").Int() item.Lore = strings.TrimSpace(ctx.Form("lore").String()) item.Attack = ctx.Form("attack").Int() item.Defense = ctx.Form("defense").Int() item.Strength = ctx.Form("strength").Int() item.Dexterity = ctx.Form("dexterity").Int() item.MaxHP = ctx.Form("max_hp").Int() item.MaxMP = ctx.Form("max_mp").Int() item.ExpBonus = ctx.Form("exp_bonus").Int() item.GoldBonus = ctx.Form("gold_bonus").Int() item.Special = strings.TrimSpace(ctx.Form("special").String()) if item.Name == "" { return fmt.Errorf("item name is required") } return nil } func checkItemNameConflict(name string, excludeID int) error { existingItem, err := items.ByName(name) if err != nil { return nil // No conflict if no item found or database error } if existingItem != nil && existingItem.ID != excludeID { return fmt.Errorf("an item with the name '%s' already exists", name) } return nil } func adminClassesIndex(ctx sushi.Ctx) { pagination := helpers.Pagination{ Page: max(int(ctx.QueryArgs().GetUintOrZero("page")), 1), PerPage: 30, } type ClassData struct { ID int Name string Lore string BaseHP int BaseMP int BaseSTR int BaseDEX int RateHP int RateMP int RateSTR int RateDEX int } var classList []*ClassData err := database.Select(&classList, ` SELECT id, name, lore, base_hp, base_mp, base_str, base_dex, rate_hp, rate_mp, rate_str, rate_dex FROM classes ORDER BY id DESC LIMIT %d OFFSET %d`, pagination.PerPage, pagination.Offset()) if err != nil { fmt.Printf("Error getting class list for admin index: %s", err.Error()) classList = make([]*ClassData, 0) } type CountResult struct{ Count int } var result CountResult database.Get(&result, "SELECT COUNT(*) as count FROM classes") pagination.Total = result.Count components.RenderAdminPage(ctx, "Class Management", "admin/classes/index.html", map[string]any{ "classes": classList, "currentPage": pagination.Page, "totalPages": pagination.TotalPages(), "hasNext": pagination.HasNext(), "hasPrev": pagination.HasPrev(), }) } func adminClassNew(ctx sushi.Ctx) { class := classes.New() components.RenderAdminPage(ctx, "Add New Class", "admin/classes/edit.html", map[string]any{ "class": class, }) } func adminClassCreate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() class := classes.New() if err := populateClassFromForm(ctx, class); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/classes/new") return } if err := class.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/classes/new") return } if err := checkClassNameConflict(class.Name, 0); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect("/admin/classes/new") return } if err := class.Insert(); err != nil { sess.SetFlash("error", "Failed to create class") ctx.Redirect("/admin/classes/new") return } sess.SetFlash("success", fmt.Sprintf("Class %s created successfully", class.Name)) ctx.Redirect("/admin/classes") } func adminClassEdit(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() class, err := classes.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Class %d not found", id)) ctx.Redirect("/admin/classes") return } components.RenderAdminPage(ctx, fmt.Sprintf("Edit Class: %s", class.Name), "admin/classes/edit.html", map[string]any{ "class": class, }) } func adminClassUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() class, err := classes.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Class %d not found", id)) ctx.Redirect("/admin/classes") return } // Check if delete was requested if ctx.Form("delete").String() == "1" { if err := class.Delete(); err != nil { sess.SetFlash("error", "Failed to delete class") ctx.Redirect(fmt.Sprintf("/admin/classes/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Class %s deleted successfully", class.Name)) ctx.Redirect("/admin/classes") return } if err := populateClassFromForm(ctx, class); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/classes/%d", id)) return } if err := class.Validate(); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/classes/%d", id)) return } if err := checkClassNameConflict(class.Name, class.ID); err != nil { sess.SetFlash("error", err.Error()) ctx.Redirect(fmt.Sprintf("/admin/classes/%d", id)) return } fields := map[string]any{ "name": class.Name, "lore": class.Lore, "base_hp": class.BaseHP, "base_mp": class.BaseMP, "base_str": class.BaseSTR, "base_dex": class.BaseDEX, "rate_hp": class.RateHP, "rate_mp": class.RateMP, "rate_str": class.RateSTR, "rate_dex": class.RateDEX, } if err := database.Update("classes", fields, "id", id); err != nil { sess.SetFlash("error", "Failed to update class") ctx.Redirect(fmt.Sprintf("/admin/classes/%d", id)) return } sess.SetFlash("success", fmt.Sprintf("Class %s updated successfully", class.Name)) ctx.Redirect("/admin/classes") } func populateClassFromForm(ctx sushi.Ctx, class *classes.Class) error { class.Name = strings.TrimSpace(ctx.Form("name").String()) class.Lore = strings.TrimSpace(ctx.Form("lore").String()) class.BaseHP = ctx.Form("base_hp").Int() class.BaseMP = ctx.Form("base_mp").Int() class.BaseSTR = ctx.Form("base_str").Int() class.BaseDEX = ctx.Form("base_dex").Int() class.RateHP = ctx.Form("rate_hp").Int() class.RateMP = ctx.Form("rate_mp").Int() class.RateSTR = ctx.Form("rate_str").Int() class.RateDEX = ctx.Form("rate_dex").Int() if class.Name == "" { return fmt.Errorf("class name is required") } return nil } func checkClassNameConflict(name string, excludeID int) error { existingClass, err := classes.ByName(name) if err != nil { return nil // No conflict if no class found or database error } if existingClass != nil && existingClass.ID != excludeID { return fmt.Errorf("a class with the name '%s' already exists", name) } return nil } func adminClassSpells(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() class, err := classes.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Class %d not found", id)) ctx.Redirect("/admin/classes") return } // Get current spell unlocks for this class type SpellUnlock struct { SpellID int SpellName string SpellType string SpellMP int SpellPower int Level int } var unlocks []*SpellUnlock err = database.Select(&unlocks, ` SELECT su.spell_id, s.name as spell_name, su.level, s.mp as spell_mp, s.power as spell_power, CASE s.type WHEN 0 THEN 'Heal' WHEN 1 THEN 'Damage' WHEN 2 THEN 'Sleep' WHEN 3 THEN 'Uber Attack' WHEN 4 THEN 'Uber Defense' ELSE 'Unknown' END as spell_type FROM spell_unlocks su JOIN spells s ON su.spell_id = s.id WHERE su.class_id = %d ORDER BY su.level ASC, s.name ASC`, id) if err != nil { fmt.Printf("Error getting spell unlocks: %s", err.Error()) unlocks = make([]*SpellUnlock, 0) } components.RenderAdminPage(ctx, fmt.Sprintf("Spell Learning: %s", class.Name), "admin/classes/spells.html", map[string]any{ "class": class, "currentUnlocks": unlocks, "classID": id, }) } func adminSpellsAPI(ctx sushi.Ctx) { allSpells, err := spells.All() if err != nil { ctx.SetStatusCode(500) ctx.WriteString("Error loading spells") return } ctx.SetContentType("application/json") spellsJSON, _ := json.Marshal(allSpells) ctx.Write(spellsJSON) } func adminClassSpellsUpdate(ctx sushi.Ctx) { sess := ctx.GetCurrentSession() id := ctx.Param("id").Int() class, err := classes.Find(id) if err != nil { sess.SetFlash("error", fmt.Sprintf("Class %d not found", id)) ctx.Redirect("/admin/classes") return } action := ctx.Form("action").String() switch action { case "add": spellID := ctx.Form("spell_id").Int() level := ctx.Form("level").Int() if spellID == 0 { sess.SetFlash("error", "Please select a spell") ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) return } if level < 1 || level > 50 { sess.SetFlash("error", "Level must be between 1 and 50") ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) return } // Check if this spell is already unlocked for this class var count int err = database.Get(&count, "SELECT COUNT(*) FROM spell_unlocks WHERE class_id = %d AND spell_id = %d", id, spellID) if err == nil && count > 0 { sess.SetFlash("error", "This spell is already unlocked for this class") ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) return } // Verify spell exists spell, err := spells.Find(spellID) if err != nil { sess.SetFlash("error", "Spell not found") ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) return } // Add the spell unlock err = database.Exec("INSERT INTO spell_unlocks (spell_id, class_id, level) VALUES (%d, %d, %d)", spellID, id, level) if err != nil { sess.SetFlash("error", "Failed to add spell unlock") ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) return } sess.SetFlash("success", fmt.Sprintf("Added %s to %s at level %d", spell.Name, class.Name, level)) case "remove": spellID := ctx.Form("spell_id").Int() level := ctx.Form("level").Int() err = database.Exec("DELETE FROM spell_unlocks WHERE class_id = %d AND spell_id = %d AND level = %d", id, spellID, level) if err != nil { sess.SetFlash("error", "Failed to remove spell unlock") ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) return } sess.SetFlash("success", "Spell unlock removed") default: sess.SetFlash("error", "Invalid action") } ctx.Redirect(fmt.Sprintf("/admin/classes/%d/spells", id)) } func bToMb(b uint64) uint64 { return b / 1024 / 1024 }