1390 lines
36 KiB
Go
1390 lines
36 KiB
Go
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
|
|
}
|