Compare commits
2 Commits
4288619520
...
0c756c425f
Author | SHA1 | Date | |
---|---|---|---|
0c756c425f | |||
78d0e5debe |
BIN
data/dk.db
BIN
data/dk.db
Binary file not shown.
92
internal/actions/town.go
Normal file
92
internal/actions/town.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"dk/internal/database"
|
||||||
|
"dk/internal/helpers"
|
||||||
|
"dk/internal/models/items"
|
||||||
|
"dk/internal/models/towns"
|
||||||
|
"dk/internal/models/users"
|
||||||
|
"fmt"
|
||||||
|
"maps"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RestAtInn handles the inn resting logic
|
||||||
|
func RestAtInn(user *users.User, town *towns.Town) error {
|
||||||
|
if user.Gold < town.InnCost {
|
||||||
|
return fmt.Errorf("you can't afford to stay here tonight")
|
||||||
|
}
|
||||||
|
|
||||||
|
return database.Transaction(func() error {
|
||||||
|
return database.Update("users", map[string]any{
|
||||||
|
"gold": user.Gold - town.InnCost,
|
||||||
|
"hp": user.MaxHP,
|
||||||
|
"mp": user.MaxMP,
|
||||||
|
"tp": user.MaxTP,
|
||||||
|
}, "id", user.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuyShopItem handles purchasing an item from a town shop
|
||||||
|
func BuyShopItem(user *users.User, town *towns.Town, itemID int) error {
|
||||||
|
// Validate item exists in shop
|
||||||
|
if !town.HasShopItem(itemID) {
|
||||||
|
return fmt.Errorf("the item doesn't exist in this shop")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get item details
|
||||||
|
item, err := items.Find(itemID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error purchasing item: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user has enough gold
|
||||||
|
if user.Gold < item.Value {
|
||||||
|
return fmt.Errorf("you don't have enough gold to buy %s", item.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get equipment updates from existing actions
|
||||||
|
equipUpdates, err := UserEquipItem(user, item)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot equip item: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute purchase transaction
|
||||||
|
return database.Transaction(func() error {
|
||||||
|
// Start with gold deduction
|
||||||
|
updates := map[string]any{
|
||||||
|
"gold": user.Gold - item.Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add equipment updates
|
||||||
|
maps.Copy(updates, equipUpdates)
|
||||||
|
|
||||||
|
return database.Update("users", updates, "id", user.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuyTownMap handles purchasing a town map
|
||||||
|
func BuyTownMap(user *users.User, townID int) error {
|
||||||
|
// Get the town being mapped
|
||||||
|
mappedTown, err := towns.Find(townID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error purchasing map: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user has enough gold
|
||||||
|
if user.Gold < mappedTown.MapCost {
|
||||||
|
return fmt.Errorf("you don't have enough gold to buy the map to %s", mappedTown.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current town IDs and add new one
|
||||||
|
townIDs := user.GetTownIDs()
|
||||||
|
townIDs = append(townIDs, townID)
|
||||||
|
newTownsString := helpers.IntsToString(townIDs)
|
||||||
|
|
||||||
|
// Execute purchase transaction
|
||||||
|
return database.Transaction(func() error {
|
||||||
|
return database.Update("users", map[string]any{
|
||||||
|
"gold": user.Gold - mappedTown.MapCost,
|
||||||
|
"towns": newTownsString,
|
||||||
|
}, "id", user.ID)
|
||||||
|
})
|
||||||
|
}
|
@ -5,6 +5,8 @@ import (
|
|||||||
"dk/internal/database"
|
"dk/internal/database"
|
||||||
"dk/internal/helpers"
|
"dk/internal/helpers"
|
||||||
"dk/internal/models/news"
|
"dk/internal/models/news"
|
||||||
|
"dk/internal/models/spells"
|
||||||
|
"dk/internal/models/towns"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -34,6 +36,16 @@ func RegisterAdminRoutes(app *sushi.App) {
|
|||||||
group.Get("/users", adminUsersIndex)
|
group.Get("/users", adminUsersIndex)
|
||||||
group.Get("/users/:id", adminUserEdit)
|
group.Get("/users/:id", adminUserEdit)
|
||||||
group.Post("/users/:id", adminUserUpdate)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func adminIndex(ctx sushi.Ctx) {
|
func adminIndex(ctx sushi.Ctx) {
|
||||||
@ -211,6 +223,393 @@ func adminUserUpdate(ctx sushi.Ctx) {
|
|||||||
ctx.Redirect("/admin/users")
|
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 bToMb(b uint64) uint64 {
|
func bToMb(b uint64) uint64 {
|
||||||
return b / 1024 / 1024
|
return b / 1024 / 1024
|
||||||
}
|
}
|
||||||
|
@ -3,9 +3,6 @@ package routes
|
|||||||
import (
|
import (
|
||||||
"dk/internal/actions"
|
"dk/internal/actions"
|
||||||
"dk/internal/components"
|
"dk/internal/components"
|
||||||
"dk/internal/database"
|
|
||||||
"dk/internal/helpers"
|
|
||||||
"dk/internal/models/items"
|
|
||||||
"dk/internal/models/towns"
|
"dk/internal/models/towns"
|
||||||
"dk/internal/models/users"
|
"dk/internal/models/users"
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -95,22 +92,9 @@ func rest(ctx sushi.Ctx) {
|
|||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
user := ctx.GetCurrentUser().(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
|
|
||||||
if user.Gold < town.InnCost {
|
err := actions.RestAtInn(user, town)
|
||||||
sess.SetFlash("error", "You can't afford to stay here tonight.")
|
|
||||||
ctx.Redirect("/town/inn")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := database.Transaction(func() error {
|
|
||||||
return database.Update("users", map[string]any{
|
|
||||||
"gold": user.Gold - town.InnCost,
|
|
||||||
"hp": user.MaxHP,
|
|
||||||
"mp": user.MaxMP,
|
|
||||||
"tp": user.MaxTP,
|
|
||||||
}, "id", user.ID)
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.SetFlash("error", "Failed to rest at inn.")
|
sess.SetFlash("error", err.Error())
|
||||||
ctx.Redirect("/town/inn")
|
ctx.Redirect("/town/inn")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -144,54 +128,13 @@ func showShop(ctx sushi.Ctx) {
|
|||||||
|
|
||||||
func buyItem(ctx sushi.Ctx) {
|
func buyItem(ctx sushi.Ctx) {
|
||||||
sess := ctx.GetCurrentSession()
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
id := ctx.Param("id").Int()
|
id := ctx.Param("id").Int()
|
||||||
|
|
||||||
town := ctx.UserValue("town").(*towns.Town)
|
town := ctx.UserValue("town").(*towns.Town)
|
||||||
if !town.HasShopItem(id) {
|
|
||||||
sess.SetFlash("error", "The item doesn't exist in this shop.")
|
|
||||||
ctx.Redirect("/town/shop")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item, err := items.Find(id)
|
|
||||||
if err != nil {
|
|
||||||
sess.SetFlash("error", "Error purchasing item; "+err.Error())
|
|
||||||
ctx.Redirect("/town/shop")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.GetCurrentUser().(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
if user.Gold < item.Value {
|
|
||||||
sess.SetFlash("error", "You don't have enough gold to buy "+item.Name)
|
|
||||||
ctx.Redirect("/town/shop")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get equipment updates from actions
|
err := actions.BuyShopItem(user, town, id)
|
||||||
equipUpdates, err := actions.UserEquipItem(user, item)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.SetFlash("error", "Cannot equip item: "+err.Error())
|
sess.SetFlash("error", err.Error())
|
||||||
ctx.Redirect("/town/shop")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = database.Transaction(func() error {
|
|
||||||
// Start with gold deduction
|
|
||||||
updates := map[string]any{
|
|
||||||
"gold": user.Gold - item.Value,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add equipment updates
|
|
||||||
for field, value := range equipUpdates {
|
|
||||||
updates[field] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
return database.Update("users", updates, "id", user.ID)
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
sess.SetFlash("error", "Failed to purchase item.")
|
|
||||||
ctx.Redirect("/town/shop")
|
ctx.Redirect("/town/shop")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -224,36 +167,12 @@ func showMaps(ctx sushi.Ctx) {
|
|||||||
|
|
||||||
func buyMap(ctx sushi.Ctx) {
|
func buyMap(ctx sushi.Ctx) {
|
||||||
sess := ctx.GetCurrentSession()
|
sess := ctx.GetCurrentSession()
|
||||||
|
|
||||||
id := ctx.Param("id").Int()
|
id := ctx.Param("id").Int()
|
||||||
|
|
||||||
mapped, err := towns.Find(id)
|
|
||||||
if err != nil {
|
|
||||||
sess.SetFlash("error", "Error purchasing map; "+err.Error())
|
|
||||||
ctx.Redirect("/town/maps")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user := ctx.GetCurrentUser().(*users.User)
|
user := ctx.GetCurrentUser().(*users.User)
|
||||||
if user.Gold < mapped.MapCost {
|
|
||||||
sess.SetFlash("error", "You don't have enough gold to buy the map to "+mapped.Name)
|
|
||||||
ctx.Redirect("/town/maps")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
townIDs := user.GetTownIDs()
|
|
||||||
townIDs = append(townIDs, id)
|
|
||||||
newTownsString := helpers.IntsToString(townIDs)
|
|
||||||
|
|
||||||
err = database.Transaction(func() error {
|
|
||||||
return database.Update("users", map[string]any{
|
|
||||||
"gold": user.Gold - mapped.MapCost,
|
|
||||||
"towns": newTownsString,
|
|
||||||
}, "id", user.ID)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
err := actions.BuyTownMap(user, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
sess.SetFlash("error", "Failed to purchase map.")
|
sess.SetFlash("error", err.Error())
|
||||||
ctx.Redirect("/town/maps")
|
ctx.Redirect("/town/maps")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
47
templates/admin/spells/edit.html
Normal file
47
templates/admin/spells/edit.html
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
{include "admin/layout.html"}
|
||||||
|
|
||||||
|
{block "content"}
|
||||||
|
<h1>{if spell.ID}Edit Spell: {spell.Name}{else}Add New Spell{/if}</h1>
|
||||||
|
|
||||||
|
<form class="standard" method="post">
|
||||||
|
{csrf}
|
||||||
|
<div>
|
||||||
|
<label for="name">Spell Name:</label>
|
||||||
|
<input type="text" name="name" id="name" value="{spell.Name}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="type">Spell Type:</label>
|
||||||
|
<select name="type" id="type" required>
|
||||||
|
<option value="0"{if spell.Type == 0} selected{/if}>Heal</option>
|
||||||
|
<option value="1"{if spell.Type == 1} selected{/if}>Damage</option>
|
||||||
|
<option value="2"{if spell.Type == 2} selected{/if}>Sleep</option>
|
||||||
|
<option value="3"{if spell.Type == 3} selected{/if}>Uber Attack</option>
|
||||||
|
<option value="4"{if spell.Type == 4} selected{/if}>Uber Defense</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="mp">MP Cost:</label>
|
||||||
|
<input type="number" name="mp" id="mp" value="{spell.MP}" min="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="power">Power:</label>
|
||||||
|
<input type="number" name="power" id="power" value="{spell.Power}" min="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="icon">Icon:</label>
|
||||||
|
<input type="text" name="icon" id="icon" value="{spell.Icon}" placeholder="spell_icon.png">
|
||||||
|
<small>Optional icon filename or path</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="lore">Lore/Description:</label>
|
||||||
|
<textarea name="lore" id="lore" rows="3" placeholder="Spell description or lore text">{spell.Lore}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/spells"><button type="button" class="btn">Cancel</button></a>
|
||||||
|
<button type="submit" class="btn btn-primary">{if spell.ID}Update{else}Create{/if} Spell</button>
|
||||||
|
{if spell.ID}
|
||||||
|
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this spell?')">Delete Spell</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/block}
|
60
templates/admin/spells/index.html
Normal file
60
templates/admin/spells/index.html
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
{include "admin/layout.html"}
|
||||||
|
|
||||||
|
{block "content"}
|
||||||
|
<h1>Spell Management</h1>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<a href="/admin/spells/new" class="btn btn-primary">Add New Spell</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Total spells: {#spells} | Page {currentPage} of {totalPages}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{if #spells > 0}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>MP Cost</th>
|
||||||
|
<th>Power</th>
|
||||||
|
<th>Icon</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{for spell in spells}
|
||||||
|
<tr>
|
||||||
|
<td>{spell.ID}</td>
|
||||||
|
<td>{spell.Name}</td>
|
||||||
|
<td>{spell.TypeName}</td>
|
||||||
|
<td>{spell.MP}</td>
|
||||||
|
<td>{spell.Power}</td>
|
||||||
|
<td>{if spell.Icon}{spell.Icon}{else}-{/if}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/spells/{spell.ID}">Edit</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/for}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{if totalPages > 1}
|
||||||
|
<div class="pagination">
|
||||||
|
{if hasPrev}
|
||||||
|
<a href="/admin/spells?page={currentPage - 1}">← Previous</a>
|
||||||
|
{/if}
|
||||||
|
{if hasNext}
|
||||||
|
<a href="/admin/spells?page={currentPage + 1}">Next →</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{else}
|
||||||
|
<div>
|
||||||
|
No spells found.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/block}
|
45
templates/admin/towns/edit.html
Normal file
45
templates/admin/towns/edit.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{include "admin/layout.html"}
|
||||||
|
|
||||||
|
{block "content"}
|
||||||
|
<h1>{if town.ID}Edit Town: {town.Name}{else}Add New Town{/if}</h1>
|
||||||
|
|
||||||
|
<form class="standard" method="post">
|
||||||
|
{csrf}
|
||||||
|
<div>
|
||||||
|
<label for="name">Town Name:</label>
|
||||||
|
<input type="text" name="name" id="name" value="{town.Name}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="x">X Position:</label>
|
||||||
|
<input type="number" name="x" id="x" value="{town.X}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="y">Y Position:</label>
|
||||||
|
<input type="number" name="y" id="y" value="{town.Y}" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="inn_cost">Inn Cost (gold):</label>
|
||||||
|
<input type="number" name="inn_cost" id="inn_cost" value="{town.InnCost}" min="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="map_cost">Map Cost (gold):</label>
|
||||||
|
<input type="number" name="map_cost" id="map_cost" value="{town.MapCost}" min="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="tp_cost">Teleport Cost (gold):</label>
|
||||||
|
<input type="number" name="tp_cost" id="tp_cost" value="{town.TPCost}" min="0" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="shop_list">Shop Items (comma-separated item IDs):</label>
|
||||||
|
<input type="text" name="shop_list" id="shop_list" value="{town.ShopList}" placeholder="1,2,3">
|
||||||
|
<small>Enter item IDs separated by commas (e.g. "1,2,3,4")</small>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="/admin/towns"><button type="button" class="btn">Cancel</button></a>
|
||||||
|
<button type="submit" class="btn btn-primary">{if town.ID}Update{else}Create{/if} Town</button>
|
||||||
|
{if town.ID}
|
||||||
|
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this town?')">Delete Town</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/block}
|
62
templates/admin/towns/index.html
Normal file
62
templates/admin/towns/index.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{include "admin/layout.html"}
|
||||||
|
|
||||||
|
{block "content"}
|
||||||
|
<h1>Town Management</h1>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 1rem;">
|
||||||
|
<a href="/admin/towns/new" class="btn btn-primary">Add New Town</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Total towns: {#towns} | Page {currentPage} of {totalPages}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{if #towns > 0}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Inn Cost</th>
|
||||||
|
<th>Map Cost</th>
|
||||||
|
<th>TP Cost</th>
|
||||||
|
<th>Shop Items</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{for town in towns}
|
||||||
|
<tr>
|
||||||
|
<td>{town.ID}</td>
|
||||||
|
<td>{town.Name}</td>
|
||||||
|
<td>({town.X}, {town.Y})</td>
|
||||||
|
<td>{town.InnCost}g</td>
|
||||||
|
<td>{town.MapCost}g</td>
|
||||||
|
<td>{town.TPCost}g</td>
|
||||||
|
<td>{town.ShopItemCount}</td>
|
||||||
|
<td>
|
||||||
|
<a href="/admin/towns/{town.ID}">Edit</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/for}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{if totalPages > 1}
|
||||||
|
<div class="pagination">
|
||||||
|
{if hasPrev}
|
||||||
|
<a href="/admin/towns?page={currentPage - 1}">← Previous</a>
|
||||||
|
{/if}
|
||||||
|
{if hasNext}
|
||||||
|
<a href="/admin/towns?page={currentPage + 1}">Next →</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{else}
|
||||||
|
<div>
|
||||||
|
No towns found.
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/block}
|
Loading…
x
Reference in New Issue
Block a user