add class management with spell unlocks
This commit is contained in:
parent
59724dee81
commit
82186a5951
141
assets/scripts/spell-search.js
Normal file
141
assets/scripts/spell-search.js
Normal file
@ -0,0 +1,141 @@
|
||||
// All available spells data (loaded from API)
|
||||
let allSpells = [];
|
||||
|
||||
const searchInput = document.getElementById('spell-search');
|
||||
const resultsDiv = document.getElementById('spell-results');
|
||||
const selectedDiv = document.getElementById('selected-spell');
|
||||
const spellIdInput = document.getElementById('spell-id');
|
||||
|
||||
let currentResults = [];
|
||||
|
||||
// Load spells data from API
|
||||
async function loadSpells() {
|
||||
try {
|
||||
const response = await fetch('/admin/api/spells');
|
||||
if (response.ok) {
|
||||
allSpells = await response.json();
|
||||
} else {
|
||||
console.error('Failed to load spells');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading spells:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize data when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', loadSpells);
|
||||
|
||||
// Simple fuzzy search function
|
||||
function fuzzyMatch(search, target) {
|
||||
search = search.toLowerCase();
|
||||
target = target.toLowerCase();
|
||||
|
||||
// Exact match gets highest score
|
||||
if (target.includes(search)) {
|
||||
return 1000 - target.indexOf(search);
|
||||
}
|
||||
|
||||
// Character-by-character fuzzy matching
|
||||
let score = 0;
|
||||
let searchIndex = 0;
|
||||
|
||||
for (let i = 0; i < target.length && searchIndex < search.length; i++) {
|
||||
if (target[i] === search[searchIndex]) {
|
||||
score += 10;
|
||||
searchIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Only return score if we matched all search characters
|
||||
return searchIndex === search.length ? score : 0;
|
||||
}
|
||||
|
||||
function getSpellTypeName(type) {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return 'Heal';
|
||||
case 1:
|
||||
return 'Damage';
|
||||
case 2:
|
||||
return 'Sleep';
|
||||
case 3:
|
||||
return 'Uber Attack';
|
||||
case 4:
|
||||
return 'Uber Defense';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function searchSpells() {
|
||||
const query = searchInput.value.trim();
|
||||
|
||||
if (query.length < 2) {
|
||||
resultsDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
// Search and score results
|
||||
const scored = allSpells.map(spell => ({
|
||||
...spell,
|
||||
score: fuzzyMatch(query, spell.Name)
|
||||
})).filter(spell => spell.score > 0)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 8); // Limit to 8 results
|
||||
|
||||
currentResults = scored;
|
||||
|
||||
if (scored.length === 0) {
|
||||
resultsDiv.innerHTML = '<div style="padding: 0.5rem; color: #666;">No spells found</div>';
|
||||
} else {
|
||||
resultsDiv.innerHTML = scored.map((spell, index) =>
|
||||
`<div class="spell-result" data-index="${index}" style="padding: 0.5rem; cursor: pointer; border-bottom: 1px solid #eee;">
|
||||
<strong>${spell.Name}</strong> - ${getSpellTypeName(spell.Type)}
|
||||
<span style="color: #666;">(${spell.MP} MP, ${spell.Power} Power)</span>
|
||||
</div>`
|
||||
).join('');
|
||||
}
|
||||
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function selectSpell(spell) {
|
||||
selectedDiv.innerHTML = `
|
||||
<strong>${spell.Name}</strong> - ${getSpellTypeName(spell.Type)}<br>
|
||||
<small>MP Cost: ${spell.MP} | Power: ${spell.Power}</small>
|
||||
${spell.Lore ? `<br><em>${spell.Lore}</em>` : ''}
|
||||
`;
|
||||
spellIdInput.value = spell.ID;
|
||||
searchInput.value = spell.Name;
|
||||
resultsDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
searchInput.addEventListener('input', searchSpells);
|
||||
|
||||
searchInput.addEventListener('focus', () => {
|
||||
if (currentResults.length > 0) {
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!searchInput.contains(e.target) && !resultsDiv.contains(e.target)) {
|
||||
resultsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
resultsDiv.addEventListener('click', (e) => {
|
||||
const resultEl = e.target.closest('.spell-result');
|
||||
if (resultEl) {
|
||||
const index = parseInt(resultEl.dataset.index);
|
||||
selectSpell(currentResults[index]);
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
resultsDiv.style.display = 'none';
|
||||
}
|
||||
});
|
BIN
data/dk.db
BIN
data/dk.db
Binary file not shown.
@ -91,3 +91,12 @@ func All() ([]*Class, error) {
|
||||
err := database.Select(&classes, "SELECT * FROM classes ORDER BY id DESC")
|
||||
return classes, err
|
||||
}
|
||||
|
||||
func ByName(name string) (*Class, error) {
|
||||
var class Class
|
||||
err := database.Get(&class, "SELECT * FROM classes WHERE name = %s COLLATE NOCASE", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("class with name '%s' not found", name)
|
||||
}
|
||||
return &class, nil
|
||||
}
|
||||
|
@ -126,6 +126,15 @@ func ByValueRange(minValue, maxValue int) ([]*Item, error) {
|
||||
return items, err
|
||||
}
|
||||
|
||||
func ByName(name string) (*Item, error) {
|
||||
var item Item
|
||||
err := database.Get(&item, "SELECT * FROM items WHERE name = %s COLLATE NOCASE", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("item with name '%s' not found", name)
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (i *Item) IsWeapon() bool {
|
||||
return i.Type == TypeWeapon
|
||||
|
@ -4,11 +4,14 @@ 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"
|
||||
@ -52,6 +55,19 @@ func RegisterAdminRoutes(app *sushi.App) {
|
||||
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) {
|
||||
@ -250,7 +266,7 @@ func adminTownsIndex(ctx sushi.Ctx) {
|
||||
var townList []*TownData
|
||||
err := database.Select(&townList, `
|
||||
SELECT id, name, x, y, inn_cost, map_cost, tp_cost, shop_list,
|
||||
CASE
|
||||
CASE
|
||||
WHEN shop_list = '' THEN 0
|
||||
ELSE (LENGTH(shop_list) - LENGTH(REPLACE(shop_list, ',', '')) + 1)
|
||||
END as shop_item_count
|
||||
@ -414,11 +430,11 @@ func checkTownCoordinateConflict(x, y, excludeID int) error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -608,11 +624,11 @@ func checkSpellNameConflict(name string, excludeID int) error {
|
||||
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
|
||||
}
|
||||
|
||||
@ -806,14 +822,568 @@ func checkMonsterNameConflict(name string, excludeID int) error {
|
||||
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
|
||||
}
|
||||
|
@ -246,10 +246,10 @@ INSERT INTO spells VALUES
|
||||
(17, 4, 'Ward', '', '', 10, 10),
|
||||
(18, 4, 'Fend', '', '', 20, 25),
|
||||
(19, 4, 'Barrier', '', '', 30, 50),
|
||||
(20, 2, 'Spark', 'Small jolt of electric energy.', '', 5, 10),
|
||||
(21, 2, 'Firebolt', 'Blast of concentrated fire.', '', 10, 30),
|
||||
(22, 2, 'Geyser', 'Explosion of high-pressure water.', '', 15, 60),
|
||||
(23, 2, 'Magic Missile', 'Fast, tracking bolt of arcane force.', '', 20, 85);
|
||||
(20, 1, 'Spark', 'Small jolt of electric energy.', '', 5, 10),
|
||||
(21, 1, 'Firebolt', 'Blast of concentrated fire.', '', 10, 30),
|
||||
(22, 1, 'Geyser', 'Explosion of high-pressure water.', '', 15, 60),
|
||||
(23, 1, 'Magic Missile', 'Fast, tracking bolt of arcane force.', '', 20, 85);
|
||||
|
||||
CREATE TABLE spell_unlocks (
|
||||
`spell_id` INTEGER NOT NULL,
|
||||
|
80
templates/admin/classes/edit.html
Normal file
80
templates/admin/classes/edit.html
Normal file
@ -0,0 +1,80 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>{if class.ID}Edit Class: {class.Name}{else}Add New Class{/if}</h1>
|
||||
|
||||
<form class="standard" method="post">
|
||||
{csrf}
|
||||
<div>
|
||||
<label for="name">Class Name:</label>
|
||||
<input type="text" name="name" id="name" value="{class.Name}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lore">Lore/Description:</label>
|
||||
<textarea name="lore" id="lore" rows="3" placeholder="Class description or background lore">{class.Lore}</textarea>
|
||||
</div>
|
||||
|
||||
<fieldset style="margin: 1rem 0;">
|
||||
<legend>Base Stats (Level 1)</legend>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div>
|
||||
<label for="base_hp">Base HP:</label>
|
||||
<input type="number" name="base_hp" id="base_hp" value="{class.BaseHP}" min="1" required>
|
||||
<small>Starting health points</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base_mp">Base MP:</label>
|
||||
<input type="number" name="base_mp" id="base_mp" value="{class.BaseMP}" min="0" required>
|
||||
<small>Starting magic points</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base_str">Base Strength:</label>
|
||||
<input type="number" name="base_str" id="base_str" value="{class.BaseSTR}" min="0" required>
|
||||
<small>Starting strength</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="base_dex">Base Dexterity:</label>
|
||||
<input type="number" name="base_dex" id="base_dex" value="{class.BaseDEX}" min="0" required>
|
||||
<small>Starting dexterity</small>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset style="margin: 1rem 0;">
|
||||
<legend>Growth Rates (Per Level)</legend>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div>
|
||||
<label for="rate_hp">HP Growth:</label>
|
||||
<input type="number" name="rate_hp" id="rate_hp" value="{class.RateHP}" min="0" required>
|
||||
<small>HP gained per level</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rate_mp">MP Growth:</label>
|
||||
<input type="number" name="rate_mp" id="rate_mp" value="{class.RateMP}" min="0" required>
|
||||
<small>MP gained per level</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rate_str">Strength Growth:</label>
|
||||
<input type="number" name="rate_str" id="rate_str" value="{class.RateSTR}" min="0" required>
|
||||
<small>Strength gained per level</small>
|
||||
</div>
|
||||
<div>
|
||||
<label for="rate_dex">Dexterity Growth:</label>
|
||||
<input type="number" name="rate_dex" id="rate_dex" value="{class.RateDEX}" min="0" required>
|
||||
<small>Dexterity gained per level</small>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<a href="/admin/classes"><button type="button" class="btn">Cancel</button></a>
|
||||
{if class.ID}
|
||||
<a href="/admin/classes/{class.ID}/spells"><button type="button" class="btn btn-secondary">Manage Spells</button></a>
|
||||
{/if}
|
||||
<button type="submit" class="btn btn-primary">{if class.ID}Update{else}Create{/if} Class</button>
|
||||
{if class.ID}
|
||||
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this class?')">Delete Class</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/block}
|
62
templates/admin/classes/index.html
Normal file
62
templates/admin/classes/index.html
Normal file
@ -0,0 +1,62 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Class Management</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/classes/new" class="btn btn-primary">Add New Class</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Total classes: {#classes} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
|
||||
{if #classes > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Base Stats</th>
|
||||
<th>Growth Rates</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{for class in classes}
|
||||
<tr>
|
||||
<td>{class.ID}</td>
|
||||
<td>{class.Name}</td>
|
||||
<td>
|
||||
HP: {class.BaseHP} | MP: {class.BaseMP}<br>
|
||||
STR: {class.BaseSTR} | DEX: {class.BaseDEX}
|
||||
</td>
|
||||
<td>
|
||||
HP: +{class.RateHP} | MP: +{class.RateMP}<br>
|
||||
STR: +{class.RateSTR} | DEX: +{class.RateDEX}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/classes/{class.ID}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{if totalPages > 1}
|
||||
<div class="pagination">
|
||||
{if hasPrev}
|
||||
<a href="/admin/classes?page={currentPage - 1}">← Previous</a>
|
||||
{/if}
|
||||
{if hasNext}
|
||||
<a href="/admin/classes?page={currentPage + 1}">Next →</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<div>
|
||||
No classes found.
|
||||
</div>
|
||||
{/if}
|
||||
{/block}
|
81
templates/admin/classes/spells.html
Normal file
81
templates/admin/classes/spells.html
Normal file
@ -0,0 +1,81 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Spell Learning: {class.Name}</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/classes/{class.ID}" class="btn">← Back to Class</a>
|
||||
</div>
|
||||
|
||||
<!-- Current Spell Unlocks -->
|
||||
<h2>Current Spell Unlocks</h2>
|
||||
{if #currentUnlocks > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Level</th>
|
||||
<th>Spell</th>
|
||||
<th>Type</th>
|
||||
<th>MP</th>
|
||||
<th>Power</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{for unlock in currentUnlocks}
|
||||
<tr>
|
||||
<td>{unlock.Level}</td>
|
||||
<td>{unlock.SpellName}</td>
|
||||
<td>{unlock.SpellType}</td>
|
||||
<td>{unlock.SpellMP}</td>
|
||||
<td>{unlock.SpellPower}</td>
|
||||
<td>
|
||||
<form method="post" style="display: inline;">
|
||||
{csrf}
|
||||
<input type="hidden" name="action" value="remove">
|
||||
<input type="hidden" name="spell_id" value="{unlock.SpellID}">
|
||||
<input type="hidden" name="level" value="{unlock.Level}">
|
||||
<button type="submit" class="btn btn-small btn-danger" onclick="return confirm('Remove this spell unlock?')">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
{else}
|
||||
<p>No spells currently unlocked for this class.</p>
|
||||
{/if}
|
||||
|
||||
<!-- Add New Spell Unlock -->
|
||||
<h2>Add Spell Unlock</h2>
|
||||
<form method="post" class="standard">
|
||||
{csrf}
|
||||
<input type="hidden" name="action" value="add">
|
||||
|
||||
<div>
|
||||
<label for="spell-search">Search for Spell:</label>
|
||||
<input type="text" id="spell-search" placeholder="Type spell name..." autocomplete="off">
|
||||
<div id="spell-results" style="border: 1px solid #ccc; background: white; max-height: 200px; overflow-y: auto; display: none; position: relative; z-index: 1000;"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="selected-spell">Selected Spell:</label>
|
||||
<div id="selected-spell" style="padding: 0.5rem; border: 1px solid #ccc; background: #f9f9f9; min-height: 2rem;">
|
||||
<em>No spell selected</em>
|
||||
</div>
|
||||
<input type="hidden" name="spell_id" id="spell-id" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="level">Learn at Level:</label>
|
||||
<input type="number" name="level" id="level" min="1" max="50" value="1" required>
|
||||
<small>Level at which this spell becomes available</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button type="submit" class="btn btn-primary">Add Spell Unlock</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="/assets/scripts/spell-search.js"></script>
|
||||
{/block}
|
82
templates/admin/items/edit.html
Normal file
82
templates/admin/items/edit.html
Normal file
@ -0,0 +1,82 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>{if item.ID}Edit Item: {item.Name}{else}Add New Item{/if}</h1>
|
||||
|
||||
<form class="standard" method="post">
|
||||
{csrf}
|
||||
<div>
|
||||
<label for="name">Item Name:</label>
|
||||
<input type="text" name="name" id="name" value="{item.Name}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="type">Item Type:</label>
|
||||
<select name="type" id="type" required>
|
||||
<option value="1"{if item.Type == 1} selected{/if}>Weapon</option>
|
||||
<option value="2"{if item.Type == 2} selected{/if}>Armor</option>
|
||||
<option value="3"{if item.Type == 3} selected{/if}>Shield</option>
|
||||
<option value="4"{if item.Type == 4} selected{/if}>Accessory</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="value">Value (gold):</label>
|
||||
<input type="number" name="value" id="value" value="{item.Value}" min="0" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="lore">Lore/Description:</label>
|
||||
<textarea name="lore" id="lore" rows="3" placeholder="Item description or lore text">{item.Lore}</textarea>
|
||||
</div>
|
||||
|
||||
<fieldset style="margin: 1rem 0;">
|
||||
<legend>Stat Bonuses</legend>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||
<div>
|
||||
<label for="attack">Attack:</label>
|
||||
<input type="number" name="attack" id="attack" value="{item.Attack}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="defense">Defense:</label>
|
||||
<input type="number" name="defense" id="defense" value="{item.Defense}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="strength">Strength:</label>
|
||||
<input type="number" name="strength" id="strength" value="{item.Strength}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="dexterity">Dexterity:</label>
|
||||
<input type="number" name="dexterity" id="dexterity" value="{item.Dexterity}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_hp">Max HP Bonus:</label>
|
||||
<input type="number" name="max_hp" id="max_hp" value="{item.MaxHP}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="max_mp">Max MP Bonus:</label>
|
||||
<input type="number" name="max_mp" id="max_mp" value="{item.MaxMP}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="exp_bonus">Exp Bonus (%):</label>
|
||||
<input type="number" name="exp_bonus" id="exp_bonus" value="{item.ExpBonus}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="gold_bonus">Gold Bonus (%):</label>
|
||||
<input type="number" name="gold_bonus" id="gold_bonus" value="{item.GoldBonus}">
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div>
|
||||
<label for="special">Special Properties:</label>
|
||||
<input type="text" name="special" id="special" value="{item.Special}" placeholder="Special item properties or effects">
|
||||
<small>Optional special properties or unique effects</small>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<a href="/admin/items"><button type="button" class="btn">Cancel</button></a>
|
||||
<button type="submit" class="btn btn-primary">{if item.ID}Update{else}Create{/if} Item</button>
|
||||
{if item.ID}
|
||||
<button type="submit" name="delete" value="1" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this item?')">Delete Item</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/block}
|
72
templates/admin/items/index.html
Normal file
72
templates/admin/items/index.html
Normal file
@ -0,0 +1,72 @@
|
||||
{include "admin/layout.html"}
|
||||
|
||||
{block "content"}
|
||||
<h1>Item Management</h1>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<a href="/admin/items/new" class="btn btn-primary">Add New Item</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Total items: {#items} | Page {currentPage} of {totalPages}
|
||||
</p>
|
||||
|
||||
{if #items > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Value</th>
|
||||
<th>Attack</th>
|
||||
<th>Defense</th>
|
||||
<th>Strength</th>
|
||||
<th>Dexterity</th>
|
||||
<th>HP/MP</th>
|
||||
<th>Special</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{for item in items}
|
||||
<tr>
|
||||
<td>{item.ID}</td>
|
||||
<td>{item.Name}</td>
|
||||
<td>{item.TypeName}</td>
|
||||
<td>{item.Value}g</td>
|
||||
<td>{if item.Attack != 0}{item.Attack}{else}-{/if}</td>
|
||||
<td>{if item.Defense != 0}{item.Defense}{else}-{/if}</td>
|
||||
<td>{if item.Strength != 0}{item.Strength}{else}-{/if}</td>
|
||||
<td>{if item.Dexterity != 0}{item.Dexterity}{else}-{/if}</td>
|
||||
<td>
|
||||
{if item.MaxHP != 0}{item.MaxHP}HP{/if}
|
||||
{if item.MaxMP != 0}{if item.MaxHP != 0}/{/if}{item.MaxMP}MP{/if}
|
||||
{if item.MaxHP == 0 && item.MaxMP == 0}-{/if}
|
||||
</td>
|
||||
<td>{if item.Special}{item.Special}{else}-{/if}</td>
|
||||
<td>
|
||||
<a href="/admin/items/{item.ID}">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{/for}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{if totalPages > 1}
|
||||
<div class="pagination">
|
||||
{if hasPrev}
|
||||
<a href="/admin/items?page={currentPage - 1}">← Previous</a>
|
||||
{/if}
|
||||
{if hasNext}
|
||||
<a href="/admin/items?page={currentPage + 1}">Next →</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{else}
|
||||
<div>
|
||||
No items found.
|
||||
</div>
|
||||
{/if}
|
||||
{/block}
|
@ -27,6 +27,7 @@
|
||||
<a href="/admin/towns">Edit Towns</a>
|
||||
<a href="/admin/monsters">Edit Monsters</a>
|
||||
<a href="/admin/spells">Edit Spells</a>
|
||||
<a href="/admin/classes">Edit Classes</a>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
|
Loading…
x
Reference in New Issue
Block a user