diff --git a/assets/scripts/spell-search.js b/assets/scripts/spell-search.js new file mode 100644 index 0000000..e8d08db --- /dev/null +++ b/assets/scripts/spell-search.js @@ -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 = '
No spells found
'; + } else { + resultsDiv.innerHTML = scored.map((spell, index) => + `
+ ${spell.Name} - ${getSpellTypeName(spell.Type)} + (${spell.MP} MP, ${spell.Power} Power) +
` + ).join(''); + } + + resultsDiv.style.display = 'block'; +} + +function selectSpell(spell) { + selectedDiv.innerHTML = ` + ${spell.Name} - ${getSpellTypeName(spell.Type)}
+ MP Cost: ${spell.MP} | Power: ${spell.Power} + ${spell.Lore ? `
${spell.Lore}` : ''} + `; + 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'; + } +}); \ No newline at end of file diff --git a/data/dk.db b/data/dk.db index eda1cbc..1d08f6c 100644 Binary files a/data/dk.db and b/data/dk.db differ diff --git a/internal/models/classes/class.go b/internal/models/classes/class.go index 67b0626..1180fac 100644 --- a/internal/models/classes/class.go +++ b/internal/models/classes/class.go @@ -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 +} diff --git a/internal/models/items/items.go b/internal/models/items/items.go index e306aad..11b9051 100644 --- a/internal/models/items/items.go +++ b/internal/models/items/items.go @@ -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 diff --git a/internal/routes/admin.go b/internal/routes/admin.go index ead9f93..a0d38c2 100644 --- a/internal/routes/admin.go +++ b/internal/routes/admin.go @@ -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 } diff --git a/sql/1_create_database.sql b/sql/1_create_database.sql index cd7a57c..2d549c4 100644 --- a/sql/1_create_database.sql +++ b/sql/1_create_database.sql @@ -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, diff --git a/templates/admin/classes/edit.html b/templates/admin/classes/edit.html new file mode 100644 index 0000000..bd087a8 --- /dev/null +++ b/templates/admin/classes/edit.html @@ -0,0 +1,80 @@ +{include "admin/layout.html"} + +{block "content"} +

{if class.ID}Edit Class: {class.Name}{else}Add New Class{/if}

+ +
+ {csrf} +
+ + +
+
+ + +
+ +
+ Base Stats (Level 1) +
+
+ + + Starting health points +
+
+ + + Starting magic points +
+
+ + + Starting strength +
+
+ + + Starting dexterity +
+
+
+ +
+ Growth Rates (Per Level) +
+
+ + + HP gained per level +
+
+ + + MP gained per level +
+
+ + + Strength gained per level +
+
+ + + Dexterity gained per level +
+
+
+ +
+ + {if class.ID} + + {/if} + + {if class.ID} + + {/if} +
+
+{/block} \ No newline at end of file diff --git a/templates/admin/classes/index.html b/templates/admin/classes/index.html new file mode 100644 index 0000000..5fe1da7 --- /dev/null +++ b/templates/admin/classes/index.html @@ -0,0 +1,62 @@ +{include "admin/layout.html"} + +{block "content"} +

Class Management

+ +
+ Add New Class +
+ +

+ Total classes: {#classes} | Page {currentPage} of {totalPages} +

+ +{if #classes > 0} + + + + + + + + + + + + {for class in classes} + + + + + + + + {/for} + +
IDNameBase StatsGrowth RatesActions
{class.ID}{class.Name} + HP: {class.BaseHP} | MP: {class.BaseMP}
+ STR: {class.BaseSTR} | DEX: {class.BaseDEX} +
+ HP: +{class.RateHP} | MP: +{class.RateMP}
+ STR: +{class.RateSTR} | DEX: +{class.RateDEX} +
+ Edit +
+ +{if totalPages > 1} + +{/if} + +{else} +
+ No classes found. +
+{/if} +{/block} \ No newline at end of file diff --git a/templates/admin/classes/spells.html b/templates/admin/classes/spells.html new file mode 100644 index 0000000..d384ec5 --- /dev/null +++ b/templates/admin/classes/spells.html @@ -0,0 +1,81 @@ +{include "admin/layout.html"} + +{block "content"} +

Spell Learning: {class.Name}

+ +
+ ← Back to Class +
+ + +

Current Spell Unlocks

+{if #currentUnlocks > 0} + + + + + + + + + + + + + {for unlock in currentUnlocks} + + + + + + + + + {/for} + +
LevelSpellTypeMPPowerActions
{unlock.Level}{unlock.SpellName}{unlock.SpellType}{unlock.SpellMP}{unlock.SpellPower} +
+ {csrf} + + + + +
+
+{else} +

No spells currently unlocked for this class.

+{/if} + + +

Add Spell Unlock

+
+ {csrf} + + +
+ + + +
+ +
+ +
+ No spell selected +
+ +
+ +
+ + + Level at which this spell becomes available +
+ +
+ +
+
+ + +{/block} diff --git a/templates/admin/items/edit.html b/templates/admin/items/edit.html new file mode 100644 index 0000000..dacef5d --- /dev/null +++ b/templates/admin/items/edit.html @@ -0,0 +1,82 @@ +{include "admin/layout.html"} + +{block "content"} +

{if item.ID}Edit Item: {item.Name}{else}Add New Item{/if}

+ +
+ {csrf} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ Stat Bonuses +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + + Optional special properties or unique effects +
+ +
+ + + {if item.ID} + + {/if} +
+
+{/block} \ No newline at end of file diff --git a/templates/admin/items/index.html b/templates/admin/items/index.html new file mode 100644 index 0000000..681dde8 --- /dev/null +++ b/templates/admin/items/index.html @@ -0,0 +1,72 @@ +{include "admin/layout.html"} + +{block "content"} +

Item Management

+ +
+ Add New Item +
+ +

+ Total items: {#items} | Page {currentPage} of {totalPages} +

+ +{if #items > 0} + + + + + + + + + + + + + + + + + + {for item in items} + + + + + + + + + + + + + + {/for} + +
IDNameTypeValueAttackDefenseStrengthDexterityHP/MPSpecialActions
{item.ID}{item.Name}{item.TypeName}{item.Value}g{if item.Attack != 0}{item.Attack}{else}-{/if}{if item.Defense != 0}{item.Defense}{else}-{/if}{if item.Strength != 0}{item.Strength}{else}-{/if}{if item.Dexterity != 0}{item.Dexterity}{else}-{/if} + {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} + {if item.Special}{item.Special}{else}-{/if} + Edit +
+ +{if totalPages > 1} + +{/if} + +{else} +
+ No items found. +
+{/if} +{/block} \ No newline at end of file diff --git a/templates/admin/layout.html b/templates/admin/layout.html index 9306d1b..78ff8ce 100644 --- a/templates/admin/layout.html +++ b/templates/admin/layout.html @@ -27,6 +27,7 @@ Edit Towns Edit Monsters Edit Spells + Edit Classes